diff --git a/app/assets/javascripts/miq_tree.js b/app/assets/javascripts/miq_tree.js index 5cd98122e5d..ec4653c3fed 100644 --- a/app/assets/javascripts/miq_tree.js +++ b/app/assets/javascripts/miq_tree.js @@ -120,25 +120,6 @@ function miqExpandParentNodes(treename, selected_node) { } } -// OnCheck handler for the tags trees -function miqOnCheckProvTags(node, treename) { - var tree = miqTreeObject(treename); - // Allow only one node among siblings to be checked - if (node.state.checked) { - var siblings = $.grep(tree.getParents(node)[0].nodes, function(item) { - return item.key !== node.key; - }); - tree.uncheckNode(siblings, {silent: true }); - } - - var all_checked = tree.getChecked().map(function(item) { - return encodeURIComponent(item.key); - }); - - miqJqueryRequest(ManageIQ.tree.checkUrl + '?ids_checked=' + all_checked); - return true; -} - function miqOnClickSelectRbacTreeNode(id) { var tree = 'rbac_tree'; miqTreeExpandNode(tree, 'xx-' + id.split('-')[0]); @@ -433,7 +414,6 @@ function miqTreeEventSafeEval(func) { 'miqOnCheckGeneric', 'miqOnClickMenuRoles', 'miqOnCheckProtect', - 'miqOnCheckProvTags', 'miqOnCheckSections', 'miqOnCheckUserFilters', 'miqOnClickAutomate', diff --git a/app/controllers/application_controller/miq_request_methods.rb b/app/controllers/application_controller/miq_request_methods.rb index 21cc3fef383..5ef5f292ab9 100644 --- a/app/controllers/application_controller/miq_request_methods.rb +++ b/app/controllers/application_controller/miq_request_methods.rb @@ -651,7 +651,7 @@ def prov_req_submit # Get variables from provisioning form def prov_get_form_vars if params[:ids_checked] # User checked/unchecked a tree node - ids = params[:ids_checked].split(",") + ids = params[:ids_checked] # for some reason if tree is not expanded clicking on radiobuttons this.getAllChecked() sends up extra blanks @edit.store_path(:new, tag_symbol_for_workflow, ids.select(&:present?).collect(&:to_i)) end @@ -984,69 +984,36 @@ def workflow_instance_from_vars(req) end def build_tags_tree(wf, vm_tags, edit_mode) - tags = wf.send("allowed_tags") - @curr_tag = nil - # Build the default filters tree for the search views - all_tags = [] # Array to hold all CIs - kids_checked = false - tags.each_with_index do |t, i| # Go thru all of the Searches - if @curr_tag.blank? || @curr_tag != t[:name] - if @curr_tag != t[:name] && @ci_node - @ci_node[:expand] = true if kids_checked - kids_checked = false - @ci_node[:children] = @ci_kids if @ci_kids.present? - all_tags.push(@ci_node) if @ci_kids.present? - end - @curr_tag = t[:name] - @ci_node = {} # Build the ci node - @ci_node[:key] = t[:id].to_s - @ci_node[:title] = t[:description] - @ci_node[:title] += " *" if t[:single_value] - @ci_node[:tooltip] = t[:description] - @ci_node[:addClass] = "cfme-no-cursor-node" # No cursor pointer - @ci_node[:icon] = 'pficon pficon-folder-close' - @ci_node[:hideCheckbox] = @ci_node[:cfmeNoClick] = true - @ci_node[:addClass] = "cfme-bold-node" # Show node as different - @ci_kids = [] - end - if @curr_tag.present? && @curr_tag == t[:name] - t[:children].each do |c| - temp = {} - temp[:key] = c[0].to_s - # only add cfme_parent_key for single value tags, need to use in JS onclick handler - temp[:selectable] = false - temp[:title] = temp[:tooltip] = c[1][:description] - temp[:addClass] = "cfme-no-cursor-node" - temp[:icon] = 'fa fa-tag' - if edit_mode # Don't show checkboxes/radio buttons in non-edit mode - if vm_tags && vm_tags.include?(c[0].to_i) - temp[:select] = true - kids_checked = true - else - temp[:select] = false - end - if @edit && @edit[:current][tag_symbol_for_workflow] != @edit[:new][tag_symbol_for_workflow] - # checking to see if id is in current but not in new, change them to blue OR if id is in current but deleted from new - if (!@edit[:current][tag_symbol_for_workflow].include?(c[0].to_i) && @edit[:new][tag_symbol_for_workflow].include?(c[0].to_i)) || - (!@edit[:new][tag_symbol_for_workflow].include?(c[0].to_i) && @edit[:current][tag_symbol_for_workflow].include?(c[0].to_i)) - temp[:addClass] = "cfme-blue-bold-node" - end - end - @ci_kids.push(temp) unless @ci_kids.include?(temp) - else - temp[:hideCheckbox] = true - @ci_kids.push(temp) unless @ci_kids.include?(temp) || !vm_tags.include?(c[0].to_i) - end - end - end - if i == tags.length - 1 # Adding last node - @ci_node[:expand] = true if kids_checked - kids_checked = false - @ci_node[:children] = @ci_kids if @ci_kids.present? - all_tags.push(@ci_node) if @ci_kids.present? - end + # for some reason @tags is set in wf, and it is changed by map bellow which causes bugs + wf.instance_variable_set(:@tags, nil) + tags = wf.allowed_tags.map do |cat| + { + :values => cat[:children].map do |tag| + {:id => tag.first, :description => tag.second[:description]} + end, + :id => cat[:name], + :description => cat[:description], + :singleValue => cat[:single_value], + } end - @all_tags_tree = TreeBuilder.convert_bs_tree(all_tags).to_json # Add ci node array to root of tree + + assignments = Classification.find(vm_tags) + assigned_tags = assignments.map do |tag| + { + :description => tag.parent.description, + :id => tag.parent.name, + :singleValue => tag.parent.single_value, + :values => ->(arr, single_value) { single_value ? [arr.last] : arr }.call( + assignments.select do |assignment| + assignment.parent.name == tag.parent.name + end, + tag.parent.single_value + ).map do |assignment| + { :description => assignment.description, :id => assignment.id } + end + } + end.uniq + @tags = {:tags => tags, :assignedTags => assigned_tags, :affectedItems => []} end def build_template_filter diff --git a/app/controllers/vm_common.rb b/app/controllers/vm_common.rb index 3a43ed8bcc5..436c77c889d 100644 --- a/app/controllers/vm_common.rb +++ b/app/controllers/vm_common.rb @@ -1193,7 +1193,6 @@ def replace_right_cell(options = {}) locals[:submit_button] = @sb[:action] != 'miq_request_new' # need submit button on the screen locals[:continue_button] = @sb[:action] == 'miq_request_new' # need continue button on the screen update_buttons(locals) if @edit && @edit[:buttons].present? - presenter[:clear_tree_cookies] = "all_tags_tree" end if ['snapshot_add'].include?(@sb[:action]) diff --git a/app/javascript/components/taggingWrapper.jsx b/app/javascript/components/taggingWrapper.jsx index 1c818a0f104..568521845f7 100644 --- a/app/javascript/components/taggingWrapper.jsx +++ b/app/javascript/components/taggingWrapper.jsx @@ -5,6 +5,26 @@ import { Spinner } from 'patternfly-react'; import { TaggingWithButtonsConnected, TaggingConnected, taggingApp } from '@manageiq/react-ui-components/dist/tagging'; import { http } from '../http_api'; +const params = (type = 'default', state, tag = {}) => ({ + provision: { + id: "new", + ids_checked: [state.tagging.appState.assignedTags.map(t => t.values.map(val => val.id)).flat(), tag.tagValue.id].flat(), + tree_typ: 'tags' + }, + default: { + id: state.tagging.appState.affectedItems[0] || "new", + cat: tag.tagCategory.id, + val: tag.tagValue.id, + check: 1, + tree_typ: 'tags' + } +})[type] + +const onDelete = (type = 'default', params = [], deleted_element) => ({ + provision: {...params, check: 0, ids_checked: params.ids_checked.filter(element => element !== deleted_element) }, + default: params, +})[type] + class TaggingWrapper extends React.Component { constructor(props) { super(props); @@ -22,7 +42,7 @@ class TaggingWrapper extends React.Component { render() { if (!this.props.isLoaded) return ; const { urls, options, tagging } = this.props; - return (options && options.hideButtons && || || ); } } @@ -65,21 +85,21 @@ TaggingWrapper.propTypes = { urls: PropTypes.shape({ cancel_url: PropTypes.string.isRequired, save_url: PropTypes.string.isRequired, - }).isRequired, + }), tags: PropTypes.shape({ tags: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, description: PropTypes.string.isRequired, values: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, description: PropTypes.string.isRequired, }).isRequired).isRequired, })).isRequired, assignedTags: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, description: PropTypes.string.isRequired, values: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.number.isRequired, + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, description: PropTypes.string.isRequired, }).isRequired).isRequired, })).isRequired, diff --git a/app/javascript/miq-redux/middleware.js b/app/javascript/miq-redux/middleware.js index ad15b1d1906..e7c0f802162 100644 --- a/app/javascript/miq-redux/middleware.js +++ b/app/javascript/miq-redux/middleware.js @@ -5,11 +5,12 @@ import promiseMiddleware from 'redux-promise-middleware'; export const taggingMiddleware = store => next => action => { const { type, meta, tagCategory, tag } = action; if (meta && meta.url) { - const params = {id: store.getState().tagging.appState.affectedItems[0], cat: tag.tagCategory.id, val: tag.tagValue.id, check: 1, tree_typ: 'tags' }; + let params = meta.params(meta.type, store.getState(), tag); if (type === 'UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE') { - $.post(meta.url, params) + $.post({url: meta.url, data: JSON.stringify(params), contentType: "application/json"}) } else if (type === 'UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG') { - $.post(meta.url, {...params, check: 0}) + params = meta.onDelete(meta.type, params, tag.tagValue.id); + $.post({url: meta.url, data: JSON.stringify(params), contentType: "application/json"}) } } let result = next(action) diff --git a/app/javascript/spec/miq-redux/middleware.spec.js b/app/javascript/spec/miq-redux/middleware.spec.js index 19e1c164daa..93dd5dbadd3 100644 --- a/app/javascript/spec/miq-redux/middleware.spec.js +++ b/app/javascript/spec/miq-redux/middleware.spec.js @@ -2,6 +2,22 @@ import { taggingMiddleware } from '../../miq-redux/middleware'; //this is your m // const next = jest.fn(); // middleware needs those as parameters, usually calling next(action) at the end to proceed // const store = jest.fn(); +const params = (type = 'default', state, tag = {}) => ({ + provision: { + id: "new", + ids_checked: [], + tree_typ: 'tags' + }, + default: { + id: [], + cat: tag.tagCategory.id, + val: tag.tagValue.id, + check: 1, + tree_typ: 'tags' + } +})[type] + +const onDelete = (x, y, z) => ({...y, check: 0}); it('passes the intercepted action to next', () => { @@ -23,15 +39,16 @@ it('calls post for UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE action', () => getState: () => ({ tagging: { appState: { - affectedItems: [{}] + affectedItems: [{}], + assignedTags: [] } } }) }; - const action = { type: 'UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE', meta: { url: 'url/bla' }, tag: {tagCategory: {id: 1}, tagValue: { id:2 }}} + const action = { type: 'UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE', meta: { url: 'url/bla', params: params }, tag: {tagCategory: {id: 1}, tagValue: { id:2 }}} taggingMiddleware(store)(next)(action); - expect(next.mock.calls).toEqual( [[{"meta": {"url": "url/bla"}, "tag": {"tagCategory": {"id": 1}, "tagValue": {"id": 2}}, "type": "UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE"}]]); - expect(spy).toHaveBeenCalledWith("url/bla", {"cat": 1, "check": 1, "id": {}, "tree_typ": "tags", "val": 2}); + expect(next.mock.calls).toEqual( [[{"meta": {"url": "url/bla", "params": params}, "tag": {"tagCategory": {"id": 1}, "tagValue": {"id": 2}}, "type": "UI-COMPONENTS_TAGGING_TOGGLE_TAG_VALUE_CHANGE"}]]); + expect(spy).toHaveBeenCalledWith({"contentType": "application/json", "data": "{\"id\":[],\"cat\":1,\"val\":2,\"check\":1,\"tree_typ\":\"tags\"}", "url": "url/bla"}); }); @@ -48,8 +65,8 @@ it('calls post for UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG action', () => { } }) }; - const action = { type: 'UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG', meta: { url: 'url/bla' }, tag: {tagCategory: {id: 1}, tagValue: { id:2 }}}; + const action = { type: 'UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG', meta: { url: 'url/bla', params: params, onDelete: onDelete, type: 'default' }, tag: {tagCategory: {id: 1}, tagValue: { id:2 }}}; taggingMiddleware(store)(next)(action); - expect(next.mock.calls).toEqual( [[{"meta": {"url": "url/bla"}, "tag": {"tagCategory": {"id": 1}, "tagValue": {"id": 2}}, "type": 'UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG'}]]); - expect(spy).toHaveBeenCalledWith("url/bla", {"cat": 1, "check": 0, "id": {}, "tree_typ": "tags", "val": 2}); + expect(next.mock.calls).toEqual( [[{"meta": {"url": "url/bla", params: params, onDelete: onDelete, type: 'default'}, "tag": {"tagCategory": {"id": 1}, "tagValue": {"id": 2}}, "type": 'UI-COMPONENTS_TAGGING_DELETE_ASSIGNED_TAG'}]]); + expect(spy).toHaveBeenCalledWith({"contentType": "application/json", "data": "{\"id\":[],\"cat\":1,\"val\":2,\"check\":0,\"tree_typ\":\"tags\"}", "url": "url/bla"}); }); diff --git a/app/views/miq_request/_prov_edit.html.haml b/app/views/miq_request/_prov_edit.html.haml index d16c81014e3..d4d53c63b86 100644 --- a/app/views/miq_request/_prov_edit.html.haml +++ b/app/views/miq_request/_prov_edit.html.haml @@ -4,5 +4,3 @@ - unless @edit[:explorer] = render :partial => 'miq_request/prov_form_buttons' = _("Note: Fields marked with * are required.") -:javascript - miqDeleteTreeCookies('all_tags_tree') diff --git a/app/views/miq_request/_prov_field.html.haml b/app/views/miq_request/_prov_field.html.haml index 41c995f1b06..55c687f9e08 100644 --- a/app/views/miq_request/_prov_field.html.haml +++ b/app/views/miq_request/_prov_field.html.haml @@ -7,7 +7,6 @@ - return if field_hash.blank? - field_id = dialog.to_s + "__" + field.to_s - disabled ||= false - - unless [:hide, :ignore].include?(field_hash[:display]) :javascript // Create from/to date JS vars to limit calendar starting from @@ -290,26 +289,13 @@ = field_hash[:notes] - elsif [:tag_ids, :vm_tags].include?(field) -# tree control for tags fields - .col-md-8 - #all_tags_treebox.treeview-pf-hover.treeview-pf-select{:style => "color:#000; overflow: hidden;"} - - if @edit && @edit[:req_id] - - check = @edit[:req_id] - - elsif @miq_request - - check = @miq_request.id - - else - - check = 'new' - = render(:partial => "layouts/tree", - :locals => {:tree_id => "all_tags_treebox", - :tree_name => "all_tags_tree", - :bs_tree => @all_tags_tree, - :oncheck => "miqOnCheckProvTags", - :check_url => "/miq_request/prov_field_changed/#{check}", - :checkboxes => true}) + .col-md-10 + = react('TaggingWrapperConnected', + :tags => @tags, + :options => { :type => 'provision', :hideHeaders => true, :hideButtons => true, :url => url_for_only_path(:action => 'prov_field_changed')}) - unless field_hash[:notes_display] == :hide || field_hash[:notes].blank? -# Display notes if available = field_hash[:notes] - .note - = _("* Only a single value can be assigned from these Tag Categories") - elsif [:attached_ds, :iso_image_id, :placement_availability_zone, :placement_cluster_name, :placement_dc_name, :placement_ds_name, :placement_ems_name, :placement_host_name, :placement_rp_name, diff --git a/package.json b/package.json index 6a95ea09632..9f7c966f564 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dependencies": { "@data-driven-forms/pf3-component-mapper": "^1.13.3", "@data-driven-forms/react-form-renderer": "^1.13.3", - "@manageiq/react-ui-components": "~0.11.27", + "@manageiq/react-ui-components": "~0.11.35", "@manageiq/ui-components": "~1.2.10", "@pf3/select": "~1.12.6", "@pf3/timeline": "~1.0.8",