Add extensions to $q for resolving all promises

This patch provides an extension service for the $q
service which waits for all promises to settle and then
return which promises fail and which promises pass.

This was a result of having similar looking code in
https://review.openstack.org/#/c/231335/
and
https://review.openstack.org/#/c/234408/

The method name was inspired method which exists in $q
from Kris Oswal but not in the Angular implementation of it.
https://github.com/kriskowal/q/blob/v1/q.js#L1676

Co-Authored-By: Errol Pais<epais@thoughtworks.com>
Co-Authored-By: Kyle Olivo<keolivo@thoughtworks.com>

Change-Id: Ic3d36bba1886e06d7e7aee50c1ddfa17b51a1a65
Partially-Implements: blueprint angularize-images-table
This commit is contained in:
Rajat Vig 2015-10-30 09:54:57 -07:00
parent 9010d51484
commit 2d0e491d76
7 changed files with 281 additions and 82 deletions

View File

@ -0,0 +1,130 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function() {
'use strict';
angular
.module('horizon.framework.util.q')
.factory('horizon.framework.util.q.extensions', qExtensions);
qExtensions.$inject = ['$q'];
/**
* @ngdoc factory
* @name horizon.framework.util.q:extensions
* @module horizon.framework.util.q
* @kind function
* @description
*
* Extends the $q from Angular to provide additional functionality.
*
*/
function qExtensions($q) {
var service = {
allSettled: allSettled
};
return service;
/**
* Allow all given promises to settle and returns a collection
* of the successful and failed responses allowing the caller
* to make decisions based on the individual results.
*
* This function is typically used if you need all promises
* to complete and get all of the success and response messages,
* but need to correlate specific success and failure results
* to a particular context. To do this, ensure that the passed
* in promise has a context attribute available on the input
* promise. Each result or failure reason will include the
* context in the fail or pass array.
*
* It will always result in a success callback (resolved),
* but will provide a summary object of the results in a pair
* of arrays ("pass" and "fail"), even if some of the promises fail.
* The "pass" array will contain each of the results of the
* successfully resolved promises and the "fail" array
* will return each of the reasons for the rejected promises.
*
* In contrast to the `$q.all` in Angular which will terminate all
* promises if any promise is rejected, this will wait for all promises
* to settle.
*
* The order of the resolve or rejection reasons is non-deterministic
* and should not be relied upon for correlation to input promises.
*
* @param promiseList
* The list of promises to resolve
*
* @return
* An object with 2 lists, one for promises that got resolved
* and one for promises that got rejected.
*
* @example
* ```
* var settledPromises = qExtenstions.allSettled([
* {promise: promise1, context: context1},
* {promise: promise2, context: context2}
* ]);
* settledPromises.then(onSettled);
*
* function onSettled(data) {
* doSomething(data.pass);
* doSomething(data.fail);
* }
*
* function doSomething(resolvedList) {
* resolvedList.forEach(function (item) {
* console.log("context", item.context, "result", item.data);
* });
* }
*/
function allSettled(promiseList) {
var deferred = $q.defer();
var passList = [];
var failList = [];
var promises = promiseList.map(resolveSingle);
$q.all(promises).then(onComplete);
return deferred.promise;
function resolveSingle(singlePromise) {
var deferredInner = $q.defer();
singlePromise.promise.then(onResolve, onReject);
return deferredInner.promise;
function onResolve(response) {
passList.push(formatResponse(response, singlePromise.context));
deferredInner.resolve();
}
function onReject(response) {
failList.push(formatResponse(response, singlePromise.context));
deferredInner.resolve();
}
function formatResponse(response, context) {
return {
data: response,
context: context
};
}
}
function onComplete() {
deferred.resolve({pass: passList, fail: failList});
}
}
}
})();

View File

@ -0,0 +1,68 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
describe('horizon.framework.util.q.extensions', function () {
var service, $q, $scope;
var failedPromise = function() {
var deferred2 = $q.defer();
deferred2.reject('failed');
return deferred2.promise;
};
var passedPromise = function() {
var deferred1 = $q.defer();
deferred1.resolve('passed');
return deferred1.promise;
};
beforeEach(module('horizon.framework.util.q'));
beforeEach(inject(function($injector, _$rootScope_) {
service = $injector.get('horizon.framework.util.q.extensions');
$q = $injector.get('$q');
$scope = _$rootScope_.$new();
}));
it('should define allSettled', function () {
expect(service.allSettled).toBeDefined();
});
it('should resolve all given promises', function() {
service.allSettled([{
promise: failedPromise(),
context: '1'
}, {
promise: passedPromise(),
context: '2'
}]).then(onAllSettled, failTest);
$scope.$apply();
function onAllSettled(resolvedPromises) {
expect(resolvedPromises.fail.length).toEqual(1);
expect(resolvedPromises.fail[0]).toEqual({data: 'failed', context: '1'});
expect(resolvedPromises.pass.length).toEqual(1);
expect(resolvedPromises.pass[0]).toEqual({data: 'passed', context: '2'});
}
});
function failTest() {
expect(false).toBeTruthy();
}
});
})();

View File

@ -0,0 +1,30 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
/**
* @ngdoc overview
* @name horizon.framework.util.q
* @description
*
* # horizon.framework.util.q
*
* This module provides extensions to the Angular $q service.
*
*/
angular
.module('horizon.framework.util.q', []);
})();

View File

@ -0,0 +1,23 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function () {
'use strict';
describe('horizon.framework.util.q module', function () {
it('should have been defined', function () {
expect(angular.module('horizon.framework.util.q')).toBeDefined();
});
});
})();

View File

@ -8,6 +8,7 @@
'horizon.framework.util.http',
'horizon.framework.util.i18n',
'horizon.framework.util.promise-toggle',
'horizon.framework.util.q',
'horizon.framework.util.tech-debt',
'horizon.framework.util.workflow',
'horizon.framework.util.validators',

View File

@ -24,10 +24,11 @@
'$http',
'$q',
'$templateCache',
'horizon.framework.widgets.basePath'
'horizon.framework.widgets.basePath',
'horizon.framework.util.q.extensions'
];
function actionsService($compile, $http, $q, $templateCache, basePath) {
function actionsService($compile, $http, $q, $templateCache, basePath, $qExtensions) {
return function(spec) {
return createService(spec.scope, spec.element, spec.listType);
};
@ -42,50 +43,23 @@
return service;
function renderActions(allowedActions) {
getPermittedActions(allowedActions).then(renderPermittedActions);
}
allowedActions.forEach(function(allowedAction) {
allowedAction.promise = allowedAction.permissions;
allowedAction.context = allowedAction;
});
/**
* Get the permitted actions from the list of allowed actions
* by resolving the promises in the permissions object.
*/
function getPermittedActions(allowedActions) {
var deferred = $q.defer();
var permittedActions = [];
var promises = allowedActions.map(actionPermitted);
$q.all(promises).then(onResolved);
return deferred.promise;
function actionPermitted(action) {
var deferredInner = $q.defer();
action.permissions.then(onSuccess, onError);
return deferredInner.promise;
function onSuccess() {
permittedActions.push(action);
deferredInner.resolve();
}
function onError() {
deferredInner.resolve();
}
}
function onResolved() {
deferred.resolve(permittedActions);
}
$qExtensions.allSettled(allowedActions).then(renderPermittedActions);
}
/**
* Render permitted actions as per the list type
*/
function renderPermittedActions(permittedActions) {
if (permittedActions.length > 0) {
var templateFetch = $q.all(permittedActions.map(getTemplate));
if (listType === 'batch' || permittedActions.length === 1) {
if (permittedActions.pass.length > 0) {
var templateFetch = $q.all(permittedActions.pass.map(getTemplate));
if (listType === 'batch' || permittedActions.pass.length === 1) {
element.addClass('btn-addon');
templateFetch.then(addButtons);
} else {
@ -183,8 +157,9 @@
/**
* Fetch the HTML Template for the Action
*/
function getTemplate(action) {
function getTemplate(permittedActionResponse) {
var defered = $q.defer();
var action = permittedActionResponse.context;
$http.get(getTemplateUrl(action), {cache: $templateCache}).then(onTemplateGet);
return defered.promise;

View File

@ -22,7 +22,8 @@
deleteModalService.$inject = [
'$q',
'horizon.framework.widgets.modal.simple-modal.service',
'horizon.framework.widgets.toast.service'
'horizon.framework.widgets.toast.service',
'horizon.framework.util.q.extensions'
];
/**
@ -43,7 +44,7 @@
* and then raise the event.
* On cancel, do nothing.
*/
function deleteModalService($q, simpleModalService, toast) {
function deleteModalService($q, simpleModalService, toast, $qExtensions) {
var service = {
open: open
};
@ -91,26 +92,32 @@
simpleModalService.modal(options).result.then(onModalSubmit);
function onModalSubmit() {
resolveAll(entities.map(deleteEntityPromise)).then(notify);
$qExtensions.allSettled(entities.map(deleteEntityPromise)).then(notify);
}
function deleteEntityPromise(entity) {
return {promise: context.deleteEntity(entity.id), entity: entity};
return {promise: context.deleteEntity(entity.id), context: entity};
}
function notify(result) {
if (result.pass.length > 0) {
scope.$emit(context.successEvent, result.pass.map(getId));
toast.add('success', getMessage(context.labels.success, result.pass));
var passEntities = result.pass.map(getEntities);
scope.$emit(context.successEvent, passEntities.map(getId));
toast.add('success', getMessage(context.labels.success, passEntities));
}
if (result.fail.length > 0) {
scope.$emit(context.failedEvent, result.fail.map(getId));
toast.add('error', getMessage(context.labels.error, result.fail));
var failEntities = result.fail.map(getEntities);
scope.$emit(context.failedEvent, failEntities.map(getId));
toast.add('error', getMessage(context.labels.error, failEntities));
}
}
}
function getEntities(passResponse) {
return passResponse.context;
}
/**
* Helper method to get the displayed message
*/
@ -132,40 +139,5 @@
return entity.id;
}
/**
* Resolve all promises.
* It asks the backing API Service to suppress errors
* and collect all entities to display one
* success and one error message.
*/
function resolveAll(promiseList) {
var deferred = $q.defer();
var passList = [];
var failList = [];
var promises = promiseList.map(resolveSingle);
$q.all(promises).then(onComplete);
return deferred.promise;
function resolveSingle(singlePromise) {
var deferredInner = $q.defer();
singlePromise.promise.then(success, error);
return deferredInner.promise;
function success() {
passList.push(singlePromise.entity);
deferredInner.resolve();
}
function error() {
failList.push(singlePromise.entity);
deferredInner.resolve();
}
}
function onComplete() {
deferred.resolve({pass: passList, fail: failList});
}
}
} // end of batchDeleteService
})(); // end of IIFE