Replaces the delete button with a disabling version

Replaces the default delete button template for the images batch delete
action with a custom one. This template contains a new component, which
changes the disabled/enabled state of the underlying action based on the
selected images allowed value as specified by the delete-image service
used to evaluate the allowed value for individual rows/images and their
respective delete actions.

Closes-Bug: 1549544

Change-Id: Idd5f59e32268cf4ceefe3639c607fb6c3520e538
This commit is contained in:
mareklycka 2018-07-23 09:45:55 +02:00 committed by Marek
parent f5728eed7a
commit 71cac4a145
5 changed files with 189 additions and 5 deletions

View File

@ -38,7 +38,8 @@
'horizon.app.core.images.actions.delete-image.service', 'horizon.app.core.images.actions.delete-image.service',
'horizon.app.core.images.actions.launch-instance.service', 'horizon.app.core.images.actions.launch-instance.service',
'horizon.app.core.images.actions.update-metadata.service', 'horizon.app.core.images.actions.update-metadata.service',
'horizon.app.core.images.resourceType' 'horizon.app.core.images.resourceType',
'horizon.app.core.images.basePath'
]; ];
function registerImageActions( function registerImageActions(
@ -49,7 +50,8 @@
deleteImageService, deleteImageService,
launchInstanceService, launchInstanceService,
updateMetadataService, updateMetadataService,
imageResourceTypeCode imageResourceTypeCode,
basePath
) { ) {
var imageResourceType = registry.getResourceType(imageResourceTypeCode); var imageResourceType = registry.getResourceType(imageResourceTypeCode);
imageResourceType.itemActions imageResourceType.itemActions
@ -100,15 +102,18 @@
} }
}); });
// A custom template is provided instead of the 'standard' definition
// to customize when the rendered button is disabled
//
// The template contains a new angular component which controls the
// disabled/enabled state of the rendered button.
imageResourceType.batchActions imageResourceType.batchActions
.append({ .append({
id: 'batchDeleteImageAction', id: 'batchDeleteImageAction',
service: deleteImageService, service: deleteImageService,
template: { template: {
type: 'delete-selected', url: basePath + "/actions/delete-image-selected-button.template.html"
text: gettext('Delete Images')
} }
}); });
} }
})(); })();

View File

@ -0,0 +1 @@
<delete-image-selected selected="tCtrl.selected"></delete-image-selected>

View File

@ -0,0 +1,74 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use self 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.
*/
// The component generally renders the same content as the default batch action
// button with the added complexity of changing the buttons enabled/disabled
// stated based on the 'allowed' state of the passed selected images.
(function() {
'use strict';
angular
.module('horizon.app.core.images.actions')
.component('deleteImageSelected', {
controller: controller,
templateUrl: templateUrl,
bindings: {
callback: '=?',
selected: '<'
}
});
controller.$inject = [
'horizon.app.core.images.actions.delete-image.service',
'$q'
];
function controller(deleteImageService, $q) {
var ctrl = this;
ctrl.$onInit = function() {
ctrl.text = gettext('Delete Images');
ctrl._disable();
};
ctrl.$onChanges = function() {
ctrl._disable();
};
ctrl._disable = function() {
if (ctrl.selected.length === 0) {
ctrl.disabled = true;
} else {
var promises = $.map(ctrl.selected, function(image) {
return deleteImageService.allowed(image);
});
$q.all(promises).then(
function() {
ctrl.disabled = false;
},
function() {
ctrl.disabled = true;
}
);
}
};
}
templateUrl.$inject = ['horizon.app.core.images.basePath'];
function templateUrl(basePath) {
return basePath + 'actions/delete-image-selected.template.html';
}
})();

View File

@ -0,0 +1,97 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use self 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('delete-image-selected component', function() {
var $scope, $element, $controller, $q;
// Mock image data
var mockAllowed = { allowed: true };
var mockDisallowed = { allowed: false };
beforeEach(module('templates'));
beforeEach(module('horizon.app.core.images.actions', function($provide) {
// Injects a mock 'action' directive for unit testing
$provide.decorator('actionDirective', function($delegate) {
var component = $delegate[0];
component.template = '<div>Mock</div>';
component.templateUrl = null;
return $delegate;
});
// Mock delete-image.service. The disabling mechanism uses the allowed
// function from that service using the promises API, which is mocked
// here.
$provide.service(
'horizon.app.core.images.actions.delete-image.service', function() {
return {
allowed: function(mockImage) {
var deferred = $q.defer();
if (mockImage.allowed) {
deferred.resolve();
} else {
deferred.reject();
}
return deferred.promise;
}
};
}
);
}));
beforeEach(inject(function(_$rootScope_, _$compile_, _$q_) {
$q = _$q_;
$scope = _$rootScope_.$new();
var tag = angular.element(
'<delete-image-selected selected="selected" callback="callback">' +
'</delete-image-selected>'
);
$scope.selected = [];
$element = _$compile_(tag)($scope);
$scope.$apply();
$controller = $element.controller('deleteImageSelected');
}));
it('disables for empty list', function() {
expect($controller.disabled).toBe(true);
});
it('enables for all allowed images', function() {
// Selections change the object; just pushing in new values wouldn't
// trigger disable recalculations
$scope.selected = [$.extend({}, mockAllowed)];
$scope.$apply();
expect($controller.disabled).toBe(false);
});
it('disables for all disallowed images', function() {
$scope.selected = [$.extend({}, mockDisallowed)];
$scope.$apply();
expect($controller.disabled).toBe(true);
});
it('disables for mixed images', function() {
$scope.selected = [
$.extend({}, mockDisallowed),
$.extend({}, mockDisallowed)
];
$scope.$apply();
expect($controller.disabled).toBe(true);
});
});
})();

View File

@ -0,0 +1,7 @@
<action action-classes="'btn btn-danger'"
disabled="$ctrl.disabled"
item="$ctrl.selected"
callback="$ctrl.callback">
<span class="fa fa-trash"></span>
{{ $ctrl.text }}
</action>