diff --git a/openstack_dashboard/static/app/core/images/actions/actions.module.js b/openstack_dashboard/static/app/core/images/actions/actions.module.js
index 6fb6bcb64e..a49af8c263 100644
--- a/openstack_dashboard/static/app/core/images/actions/actions.module.js
+++ b/openstack_dashboard/static/app/core/images/actions/actions.module.js
@@ -38,7 +38,8 @@
'horizon.app.core.images.actions.delete-image.service',
'horizon.app.core.images.actions.launch-instance.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(
@@ -49,7 +50,8 @@
deleteImageService,
launchInstanceService,
updateMetadataService,
- imageResourceTypeCode
+ imageResourceTypeCode,
+ basePath
) {
var imageResourceType = registry.getResourceType(imageResourceTypeCode);
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
.append({
id: 'batchDeleteImageAction',
service: deleteImageService,
template: {
- type: 'delete-selected',
- text: gettext('Delete Images')
+ url: basePath + "/actions/delete-image-selected-button.template.html"
}
});
}
-
})();
diff --git a/openstack_dashboard/static/app/core/images/actions/delete-image-selected-button.template.html b/openstack_dashboard/static/app/core/images/actions/delete-image-selected-button.template.html
new file mode 100644
index 0000000000..517db0cab3
--- /dev/null
+++ b/openstack_dashboard/static/app/core/images/actions/delete-image-selected-button.template.html
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.js b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.js
new file mode 100644
index 0000000000..e6745abbd5
--- /dev/null
+++ b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.js
@@ -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';
+ }
+
+})();
diff --git a/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.spec.js b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.spec.js
new file mode 100644
index 0000000000..dd6639a060
--- /dev/null
+++ b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.component.spec.js
@@ -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 = '
Mock
';
+ 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(
+ '' +
+ ''
+ );
+
+ $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);
+ });
+ });
+})();
diff --git a/openstack_dashboard/static/app/core/images/actions/delete-image-selected.template.html b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.template.html
new file mode 100644
index 0000000000..3ec8c8bfb6
--- /dev/null
+++ b/openstack_dashboard/static/app/core/images/actions/delete-image-selected.template.html
@@ -0,0 +1,7 @@
+
+
+ {{ $ctrl.text }}
+