diff --git a/app/assets/javascripts/components/generic_object/assign-buttons.js b/app/assets/javascripts/components/generic_object/assign-buttons.js new file mode 100644 index 00000000000..0f4c205d95f --- /dev/null +++ b/app/assets/javascripts/components/generic_object/assign-buttons.js @@ -0,0 +1,59 @@ +ManageIQ.angular.app.component('assignButtons', { + bindings: { + assignedButtons: '<', + unassignedButtons: '<', + updateButtons: '<', + }, + require: { + parent: '^^mainCustomButtonGroupForm', + }, + controllerAs: 'vm', + controller: assignButtonsController, + templateUrl: '/static/generic_object/assign-buttons.html.haml', +}); + +function assignButtonsController() { + var vm = this; + + vm.model = { + selectedAssignedButtons: [], + selectedUnassignedButtons: [], + }; + + vm.leftButtonClicked = function() { + var ret = ManageIQ.move.between({ + from: [].concat(vm.assignedButtons), + to: [].concat(vm.unassignedButtons), + selected: vm.model.selectedAssignedButtons, + }); + + vm.updateButtons(ret.from, ret.to); + }; + + vm.rightButtonClicked = function() { + var ret = ManageIQ.move.between({ + from: [].concat(vm.unassignedButtons), + to: [].concat(vm.assignedButtons), + selected: vm.model.selectedUnassignedButtons, + }); + + vm.updateButtons(ret.to, ret.from); + }; + + function wrap(fn) { + return function() { + var assigned = fn({ + array: [].concat(vm.assignedButtons), + selected: vm.model.selectedAssignedButtons, + }); + + vm.updateButtons(assigned); + }; + } + + vm.topButtonClicked = wrap(ManageIQ.move.top); + vm.bottomButtonClicked = wrap(ManageIQ.move.bottom); + + vm.upButtonClicked = wrap(ManageIQ.move.up); + vm.downButtonClicked = wrap(ManageIQ.move.down); +} diff --git a/app/assets/javascripts/components/generic_object/main-custom-button-group-form.js b/app/assets/javascripts/components/generic_object/main-custom-button-group-form.js index 9cc6e013c28..fe34ead47c9 100644 --- a/app/assets/javascripts/components/generic_object/main-custom-button-group-form.js +++ b/app/assets/javascripts/components/generic_object/main-custom-button-group-form.js @@ -9,9 +9,9 @@ ManageIQ.angular.app.component('mainCustomButtonGroupForm', { templateUrl: '/static/generic_object/main_custom_button_group_form.html.haml', }); -mainCustomButtonGroupFormController.$inject = ['API', 'miqService']; +mainCustomButtonGroupFormController.$inject = ['API', 'miqService', '$http']; -function mainCustomButtonGroupFormController(API, miqService) { +function mainCustomButtonGroupFormController(API, miqService, $http) { var vm = this; vm.$onInit = function() { @@ -26,20 +26,35 @@ function mainCustomButtonGroupFormController(API, miqService) { button_icon: '', button_color: '#4d5258', set_data: {}, + assigned_buttons: [], + unassigned_buttons: [], }; - if (vm.customButtonGroupRecordId) { - vm.newRecord = false; - miqService.sparkleOn(); - API.get('/api/custom_button_sets/' + vm.customButtonGroupRecordId) - .then(getCustomButtonGroupFormData) - .catch(miqService.handleFailure); - } else { - vm.newRecord = true; + $http.get('/generic_object_definition/custom_buttons_in_set/?custom_button_set_id=' + vm.customButtonGroupRecordId + '&generic_object_definition_id=' + vm.genericObjectDefnRecordId) + .then(function(response) { + Object.assign(vm.customButtonGroupModel, response.data); + if (vm.customButtonGroupRecordId) { + vm.newRecord = false; + miqService.sparkleOn(); + API.get('/api/custom_button_sets/' + vm.customButtonGroupRecordId) + .then(getCustomButtonGroupFormData) + .catch(miqService.handleFailure); + } else { + vm.newRecord = true; + + API.get('/api/custom_button_sets?expand=resources&attributes=set_data') + .then(getCustomButtonSetGroupIndex) + .catch(miqService.handleFailure); + } + }) + .catch(miqService.handleFailure); + }; + + vm.updateButtons = function(assignedButtons, unassignedButtons) { + vm.customButtonGroupModel.assigned_buttons = assignedButtons; - API.get('/api/custom_button_sets?expand=resources&attributes=set_data') - .then(getCustomButtonSetGroupIndex) - .catch(miqService.handleFailure); + if (unassignedButtons) { + vm.customButtonGroupModel.unassigned_buttons = unassignedButtons; } }; @@ -53,7 +68,7 @@ function mainCustomButtonGroupFormController(API, miqService) { }; vm.resetClicked = function(angularForm) { - vm.customButtonGroupModel = Object.assign({}, vm.modelCopy); + vm.customButtonGroupModel = _.cloneDeep(vm.modelCopy); angularForm.$setUntouched(true); angularForm.$setPristine(true); @@ -71,11 +86,19 @@ function mainCustomButtonGroupFormController(API, miqService) { vm.saveWithAPI('post', '/api/custom_button_sets/', vm.prepSaveObject(), saveMsg); }; + vm.buttonOrder = function() { + var orderedButtons = []; + vm.customButtonGroupModel.assigned_buttons.forEach(function(button) { + orderedButtons.push(button.id); + }); + return orderedButtons; + }; + vm.prepSaveObject = function() { vm.customButtonGroupModel.set_data = { button_icon: vm.customButtonGroupModel.button_icon, button_color: vm.customButtonGroupModel.button_color, - button_order: vm.customButtonGroupModel.button_order, + button_order: vm.buttonOrder(), display: vm.customButtonGroupModel.display, applies_to_class: 'GenericObjectDefinition', applies_to_id: parseInt(vm.genericObjectDefnRecordId, 10), @@ -94,7 +117,9 @@ function mainCustomButtonGroupFormController(API, miqService) { vm.saveWithAPI = function(method, url, saveObject, saveMsg) { miqService.sparkleOn(); API[method](url, saveObject) - .then(miqService.redirectBack.bind(vm, saveMsg, 'success', vm.redirectUrl)) + .then(function() { + miqService.redirectBack(saveMsg, 'success', vm.redirectUrl); + }) .catch(miqService.handleFailure); }; @@ -112,7 +137,8 @@ function mainCustomButtonGroupFormController(API, miqService) { vm.customButtonGroupModel.set_data = {}; - vm.modelCopy = Object.assign({}, vm.customButtonGroupModel); + vm.modelCopy = _.cloneDeep(vm.customButtonGroupModel); + vm.afterGet = true; miqService.sparkleOff(); } @@ -123,6 +149,6 @@ function mainCustomButtonGroupFormController(API, miqService) { return setData.applies_to_class === 'GenericObject'; }).length; vm.customButtonGroupModel.group_index = currentGroupIndex + 1; - vm.modelCopy = angular.copy( vm.customButtonGroupModel ); + vm.modelCopy = _.cloneDeep(vm.customButtonGroupModel); } } diff --git a/app/assets/javascripts/directives/form_changed.js b/app/assets/javascripts/directives/form_changed.js index ee95b022fc9..4eb619c21cc 100644 --- a/app/assets/javascripts/directives/form_changed.js +++ b/app/assets/javascripts/directives/form_changed.js @@ -16,7 +16,7 @@ ManageIQ.angular.app.directive('formChanged', function() { var compare = function(original, copy, key) { // add missing keys in copy from original so recursion works - if (_.isObject(copy) && _.isObject(original)) { + if (_.isObject(copy) && _.isObject(original) && !_.isArray(copy) && !_.isArray(original)) { _.difference(Object.keys(original), Object.keys(copy)).forEach(function(k) { copy[k] = undefined; }); diff --git a/app/assets/javascripts/miq_global.js b/app/assets/javascripts/miq_global.js index 75d474e4fd9..66d73e859b0 100644 --- a/app/assets/javascripts/miq_global.js +++ b/app/assets/javascripts/miq_global.js @@ -52,6 +52,8 @@ if (!window.ManageIQ) { x: null, // mouse X coordinate for popup menu y: null, // mouse Y coordinate for popup menu }, + move: { //methods to move elements between Arrays or in an Array + }, noCollapseEvent: false, // enable/disable events fired after collapsing an accordion observe: { // keeping track of data-miq_observe requests processing: false, // is a request currently being processed? diff --git a/app/controllers/generic_object_definition_controller.rb b/app/controllers/generic_object_definition_controller.rb index 28012416e7c..20d79e41eb8 100644 --- a/app/controllers/generic_object_definition_controller.rb +++ b/app/controllers/generic_object_definition_controller.rb @@ -106,6 +106,7 @@ def custom_button_group_edit assert_privileges('ab_group_edit') @custom_button_group = CustomButtonSet.find(params[:id]) @right_cell_text = _("Edit Custom Button Group '%{name}'") % {:name => @custom_button_group.name} + @generic_object_definition = find_record_with_rbac(GenericObjectDefinition, @custom_button_group.set_data[:applies_to_id]) render_form(@right_cell_text, 'custom_button_group_form') end @@ -148,6 +149,20 @@ def add_button_in_group custom_button_set.save! end + def custom_buttons_in_set + assigned_buttons = if params[:custom_button_set_id].present? + button_set = find_record_with_rbac(CustomButtonSet, params[:custom_button_set_id]) + button_set.custom_buttons + else + [] + end + generic_object_definition = find_record_with_rbac(GenericObjectDefinition, params[:generic_object_definition_id]) + unassigned_buttons = generic_object_definition.custom_buttons + assigned_buttons.map! { |button| {:name => button.name, :id => button.id} } + unassigned_buttons.map! { |button| {:name => button.name, :id => button.id} } + render :json => {:assigned_buttons => assigned_buttons, :unassigned_buttons => unassigned_buttons} + end + private def node_type(node) diff --git a/app/javascript/helpers/move.js b/app/javascript/helpers/move.js new file mode 100644 index 00000000000..5a7d2b8c90f --- /dev/null +++ b/app/javascript/helpers/move.js @@ -0,0 +1,109 @@ +import { find, findIndex, some, reject } from 'lodash'; + +// [{id: 123}], [123] => [0] +function idsToElements(arr, ids) { + return ids.map((id) => find(arr, { id })); +} + +function idsToIndexes(arr, ids) { + return ids.map((id) => findIndex(arr, { id })); +} + +function removeElements(arr, elements) { + return reject(arr, (elem) => some(elements, elem)); +} + +function filterIndexes(indexes) { + if (indexes[0] !== 0) { + return indexes; + } + var previous = 0; + var filteredIndexes = []; + indexes.forEach(function(index) { + if (index !== 0 && index - 1 !== previous) { + filteredIndexes.push(index); + } else { + previous = index; + } + }); + return filteredIndexes; +} + +function filterReverseIndexes(indexes, endIndex) { + if (indexes[0] !== endIndex) { + return indexes; + } + var previous = endIndex; + var filteredIndexes = []; + indexes.forEach(function(index) { + if (index !== endIndex && index + 1 !== previous) { + filteredIndexes.push(index); + } else { + previous = index; + } + }); + return filteredIndexes; +} + +function stepUp(array, index) { + if (index < 1) { + return; + } + + [array[index], array[index - 1]] = [array[index - 1], array[index]]; +} + +function stepDown(array, index) { + if ((index < 0) || (index >= array.length - 1)) { + return; + } + + [array[index], array[index + 1]] = [array[index + 1], array[index]]; +} + + +// move selected elements between two arrays +export function between({from, to, selected}) { + var moved = idsToElements(from, selected); + + return { + from: removeElements(from, moved), + to: to.concat(moved), + }; +} + +// move selected elements to the top of the array +export function top({array, selected}) { + var moved = idsToElements(array, selected); + array = removeElements(array, moved); + + return moved.concat(array); +} + +// move selected elements to the bottom of the array +export function bottom({array, selected}) { + var moved = idsToElements(array, selected); + array = removeElements(array, moved); + + return array.concat(moved); +} + +// move selected elements one position up +export function up({array, selected}) { + var indexes = idsToIndexes(array, selected); + indexes = filterIndexes(indexes); + + indexes.forEach((index) => stepUp(array, index)); + + return array; +} + +// move selected elements one position up +export function down({array, selected}) { + var indexes = idsToIndexes(array, selected).reverse(); + indexes = filterReverseIndexes(indexes, array.length - 1); + + indexes.forEach((index) => stepDown(array, index)); + + return array; +} diff --git a/app/javascript/packs/application-common.js b/app/javascript/packs/application-common.js index 4704c7ada53..1f7594a0e5e 100644 --- a/app/javascript/packs/application-common.js +++ b/app/javascript/packs/application-common.js @@ -61,3 +61,6 @@ window.Spinner = Spinner; // Overview > Optimization miqOptimizationInit(); + +import * as move from '../helpers/move.js'; +ManageIQ.move = move; diff --git a/app/views/static/generic_object/assign-buttons.html.haml b/app/views/static/generic_object/assign-buttons.html.haml new file mode 100644 index 00000000000..63cb161d3b2 --- /dev/null +++ b/app/views/static/generic_object/assign-buttons.html.haml @@ -0,0 +1,55 @@ +.form-horizontal + %hr + %h3 + = _('Assign Buttons') + #column_lists + .col-md-5 + = _('Unassigned:') + %select.form-control{:name => 'unassigned_buttons', + "ng-model" => "vm.model.selectedUnassignedButtons", + "ng-options" => "item.id as item.name for item in vm.unassignedButtons", + :multiple => true, + :style => "overflow-x: scroll;", + :size => 8, + :id => "available_fields"} + .col-md-1{:style => "padding: 10px"} + .spacer + .spacer + %button.btn.btn-default.btn-block{:title => _("Move selected fields right"), + "ng-click" => "vm.rightButtonClicked()"} + %i.fa.fa-angle-right.fa-lg.hidden-xs.hidden-sm + %i.fa.fa-lg.fa-angle-right.fa-rotate-90.hidden-md.hidden-lg + %button.btn.btn-default.btn-block{:title => _("Move selected fields left"), + "ng-click" => "vm.leftButtonClicked()"} + %i.fa.fa-angle-left.fa-lg.hidden-xs.hidden-sm + %i.fa.fa-lg.fa-angle-left.fa-rotate-90.hidden-md.hidden-lg + .spacer + .col-md-5 + = _('Selected:') + %select.form-control{:name => 'assigned_buttons', + "ng-model" => "vm.model.selectedAssignedButtons", + "ng-options" => "item.id as item.name for item in vm.assignedButtons", + :multiple => true, + :style => "overflow-x: scroll;", + :size => 8, + :id => "selected_fields"} + .col-md-1{:style => "padding: 10px"} + .spacer + .spacer + %button.btn.btn-default.btn-block{:title => _("Move selected fields to top"), + "ng-enabled" => "vm.moveButtonsEnabled()", + "ng-click" => "vm.topButtonClicked()"} + %i.fa.fa-angle-double-up.fa-lg + %button.btn.btn-default.btn-block{:title => _("Move selected fields up"), + :enabled => "vm.moveButtonsEnabled()", + "ng-click" => "vm.upButtonClicked()"} + %i.fa.fa-angle-up.fa-lg + %button.btn.btn-default.btn-block{:title => _("Move selected fields down"), + "ng-enabled" => "vm.moveButtonsEnabled()", + "ng-click" => "vm.downButtonClicked()"} + %i.fa.fa-angle-down.fa-lg + %button.btn.btn-default.btn-block{:title => _("Move selected fields to bottom"), + "ng-enabled" => "vm.moveButtonsEnabled()", + "ng-click" => "vm.bottomButtonClicked()"} + %i.fa.fa-angle-double-down + .spacer diff --git a/app/views/static/generic_object/main_custom_button_group_form.html.haml b/app/views/static/generic_object/main_custom_button_group_form.html.haml index c564ece4c29..a1c88304370 100644 --- a/app/views/static/generic_object/main_custom_button_group_form.html.haml +++ b/app/views/static/generic_object/main_custom_button_group_form.html.haml @@ -8,4 +8,7 @@ "miq-form" => true} %custom-button-group-form{"model" => "vm.customButtonGroupModel", "angular-form" => "angularForm"} + %assign-buttons{"assigned-buttons" => 'vm.customButtonGroupModel.assigned_buttons', + "unassigned-buttons" => 'vm.customButtonGroupModel.unassigned_buttons', + "update-buttons" => 'vm.updateButtons'} = render :partial => "layouts/angular/generic_form_buttons" diff --git a/config/routes.rb b/config/routes.rb index 9470ae8c4c9..5ff0d943304 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1962,6 +1962,7 @@ :generic_object_definition => { :get => %w( + custom_buttons_in_set download_data download_summary_pdf edit diff --git a/spec/javascripts/components/generic_object_definition/assign-buttons_spec.js b/spec/javascripts/components/generic_object_definition/assign-buttons_spec.js new file mode 100644 index 00000000000..db5e8262a91 --- /dev/null +++ b/spec/javascripts/components/generic_object_definition/assign-buttons_spec.js @@ -0,0 +1,156 @@ +describe('assign-buttons', function() { + var vm; + var after = {}; + + // buttons + var b1 = { name: 'button1', id: 1 }; + var b2 = { name: 'button2', id: 2 }; + var b3 = { name: 'button3', id: 3 }; + var b4 = { name: 'button4', id: 4 }; + var b5 = { name: 'button5', id: 5 }; + var b6 = { name: 'button6', id: 6 }; + var b7 = { name: 'button7', id: 7 }; + var b8 = { name: 'button8', id: 8 }; + var b9 = { name: 'button9', id: 9 }; + var b10 = { name: 'button10', id: 10 }; + var b11 = { name: 'button11', id: 11 }; + var b12 = { name: 'button12', id: 12 }; + + beforeEach(module('ManageIQ')); + beforeEach(inject(function($componentController) { + var bindings = { + assignedButtons: [b1, b2, b3, b4, b5, b6], + unassignedButtons: [b7, b8, b9, b10, b11, b12], + updateButtons: function(assigned, unassigned) { + after.assigned = assigned; + after.unassigned = unassigned || bindings.unassignedButtons; + }, + }; + + bindings.updateButtons([], []); + + vm = $componentController('assignButtons', null, bindings); + })); + + describe('#leftButtonClicked', function() { + it('one button from assignedButtons is moved to the end of unassignedButtons', function() { + vm.model.selectedAssignedButtons = [1]; + vm.leftButtonClicked(); + expect(after.assigned).toEqual([b2, b3, b4, b5, b6]); + expect(after.unassigned[after.unassigned.length - 1]).toEqual(b1); + }); + + it('multiple buttons from assignedButtons are moved to the end of unassignedButtons', function() { + vm.model.selectedAssignedButtons = [2, 4, 6]; + var expectedResult = vm.unassignedButtons.concat([b2, b4, b6]); + vm.leftButtonClicked(); + expect(after.assigned).toEqual([b1, b3, b5]); + expect(after.unassigned).toEqual(expectedResult); + }); + }); + + describe('#rightButtonClicked', function() { + it('one button from unassignedButtons is moved to the end of assignedButtons', function() { + vm.model.selectedUnassignedButtons = [7]; + vm.rightButtonClicked(); + expect(after.unassigned).toEqual([b8, b9, b10, b11, b12]); + expect(after.assigned[after.assigned.length - 1]).toEqual(b7); + }); + + it('multiple buttons from unassignedButtons are moved to the end of assignedButtons', function() { + vm.model.selectedUnassignedButtons = [8, 10, 12]; + var expectedResult = vm.assignedButtons.concat([b8, b10, b12]); + vm.rightButtonClicked(); + expect(after.unassigned).toEqual([b7, b9, b11]); + expect(after.assigned).toEqual(expectedResult); + }); + }); + + describe('#upButtonClicked', function() { + it('one button is moved one place up', function() { + vm.model.selectedAssignedButtons = [2]; + vm.upButtonClicked(); + expect(after.assigned[0]).toEqual(b2); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [1, 2, 3]; + var original = [].concat(vm.assignedButtons); + vm.upButtonClicked(); + expect(after.assigned).toEqual(original); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [1, 3, 5]; + var original = [b1, b3, b2, b5, b4, b6]; + vm.upButtonClicked(); + expect(after.assigned).toEqual(original); + }); + }); + + describe('#downButtonClicked', function() { + it('one button is moved one place down', function() { + vm.model.selectedAssignedButtons = [5]; + vm.downButtonClicked(); + expect(after.assigned[5]).toEqual(b5); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [4, 5, 6]; + var original = [].concat(vm.assignedButtons); + vm.downButtonClicked(); + expect(after.assigned).toEqual(original); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [1, 3, 5]; + var original = [b2, b1, b4, b3, b6, b5]; + vm.downButtonClicked(); + expect(after.assigned).toEqual(original); + }); + }); + + describe('#bottomButtonClicked', function() { + it('one button is moved one place down', function() { + vm.model.selectedAssignedButtons = [1]; + vm.bottomButtonClicked(); + expect(after.assigned[5]).toEqual(b1); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [4, 5, 6]; + var original = [].concat(vm.assignedButtons); + vm.bottomButtonClicked(); + expect(after.assigned).toEqual(original); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [2, 4, 6]; + var original = [b1, b3, b5, b2, b4, b6]; + vm.bottomButtonClicked(); + expect(after.assigned).toEqual(original); + }); + }); + + describe('#topButtonClicked', function() { + it('one button is moved to the beginning', function() { + vm.model.selectedAssignedButtons = [5]; + vm.topButtonClicked(); + expect(after.assigned[0]).toEqual(b5); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [1, 2, 3]; + var original = [].concat(vm.assignedButtons); + vm.topButtonClicked(); + expect(after.assigned).toEqual(original); + }); + + it('buttons preserve their relative order', function() { + vm.model.selectedAssignedButtons = [1, 3, 5]; + var original = [b1, b3, b5, b2, b4, b6]; + vm.topButtonClicked(); + expect(after.assigned).toEqual(original); + }); + }); +});