diff --git a/changelog/13590.txt b/changelog/13590.txt new file mode 100644 index 00000000000000..873160707787a3 --- /dev/null +++ b/changelog/13590.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fixes issue with SearchSelect component not holding focus +``` \ No newline at end of file diff --git a/ui/lib/core/addon/components/search-select.js b/ui/lib/core/addon/components/search-select.js index 303420a4401b52..dd85aa7f521655 100644 --- a/ui/lib/core/addon/components/search-select.js +++ b/ui/lib/core/addon/components/search-select.js @@ -3,32 +3,34 @@ import { inject as service } from '@ember/service'; import { task } from 'ember-concurrency'; import { computed } from '@ember/object'; import { singularize } from 'ember-inflector'; +import { resolve } from 'rsvp'; +import { filterOptions, defaultMatcher } from 'ember-power-select/utils/group-utils'; import layout from '../templates/components/search-select'; /** * @module SearchSelect - * The `SearchSelect` is an implementation of the [ember-power-select-with-create](https://github.com/poteto/ember-cli-flash) used for form elements where options come dynamically from the API. + * The `SearchSelect` is an implementation of the [ember-power-select](https://github.com/cibernox/ember-power-select) used for form elements where options come dynamically from the API. * @example * * - * @param id {String} - The name of the form field - * @param models {Array} - An array of model types to fetch from the API. - * @param onChange {Func} - The onchange action for this form field. - * @param inputValue {String | Array} - A comma-separated string or an array of strings. - * @param label {String} - Label for this form field - * @param fallbackComponent {String} - name of component to be rendered if the API call 403s - * @param [backend] {String} - name of the backend if the query for options needs additional information (eg. secret backend) - * @param [disallowNewItems=false] {Boolean} - Controls whether or not the user can add a new item if none found - * @param [helpText] {String} - Text to be displayed in the info tooltip for this form field - * @param [selectLimit] {Number} - A number that sets the limit to how many select options they can choose - * @param [subText] {String} - Text to be displayed below the label - * @param [subLabel] {String} - a smaller label below the main Label - * @param [wildcardLabel] {String} - when you want the searchSelect component to return a count on the model for options returned when using a wildcard you must provide a label of the count e.g. role. Should be singular. + * @param {string} id - The name of the form field + * @param {Array} models - An array of model types to fetch from the API. + * @param {function} onChange - The onchange action for this form field. + * @param {string | Array} inputValue - A comma-separated string or an array of strings. + * @param {string} label - Label for this form field + * @param {string} fallbackComponent - name of component to be rendered if the API call 403s + * @param {string} [backend] - name of the backend if the query for options needs additional information (eg. secret backend) + * @param {boolean} [disallowNewItems=false] - Controls whether or not the user can add a new item if none found + * @param {string} [helpText] - Text to be displayed in the info tooltip for this form field + * @param {number} [selectLimit] - A number that sets the limit to how many select options they can choose + * @param {string} [subText] - Text to be displayed below the label + * @param {string} [subLabel] - a smaller label below the main Label + * @param {string} [wildcardLabel] - when you want the searchSelect component to return a count on the model for options returned when using a wildcard you must provide a label of the count e.g. role. Should be singular. * - * @param options {Array} - *Advanced usage* - `options` can be passed directly from the outside to the + * @param {Array} options - *Advanced usage* - `options` can be passed directly from the outside to the * power-select component. If doing this, `models` should not also be passed as that will overwrite the * passed value. - * @param search {Func} - *Advanced usage* - Customizes how the power-select component searches for matches - + * @param {function} search - *Advanced usage* - Customizes how the power-select component searches for matches - * see the power-select docs for more information. * */ @@ -48,6 +50,7 @@ export default Component.extend({ shouldUseFallback: false, shouldRenderName: false, disallowNewItems: false, + init() { this._super(...arguments); this.set('selectedOptions', this.inputValue || []); @@ -130,20 +133,39 @@ export default Component.extend({ this.onChange(this.selectedOptions); } }, + shouldShowCreate(id, options) { + if (options && options.length && options.firstObject.groupName) { + return !options.some((group) => group.options.findBy('id', id)); + } + let existingOption = this.options && (this.options.findBy('id', id) || this.options.findBy('name', id)); + if (this.disallowNewItems && !existingOption) { + return false; + } + return !existingOption; + }, + //----- adapted from ember-power-select-with-create + addCreateOption(term, results) { + if (this.shouldShowCreate(term, results)) { + const name = `Add new ${singularize(this.label)}: ${term}`; + const suggestion = { + __isSuggestion__: true, + __value__: term, + name, + id: name, + }; + results.unshift(suggestion); + } + }, + filter(options, searchText) { + const matcher = (option, text) => defaultMatcher(option.searchText, text); + return filterOptions(options || [], searchText, matcher); + }, + // ----- + actions: { onChange(val) { this.onChange(val); }, - createOption(optionId) { - let newOption = { name: optionId, id: optionId, new: true }; - this.selectedOptions.pushObject(newOption); - this.handleChange(); - }, - selectOption(option) { - this.selectedOptions.pushObject(option); - this.options.removeObject(option); - this.handleChange(); - }, discardSelection(selected) { this.selectedOptions.removeObject(selected); // fire off getSelectedValue action higher up in get-credentials-card component @@ -152,18 +174,34 @@ export default Component.extend({ } this.handleChange(); }, - constructSuggestion(id) { - return `Add new ${singularize(this.label)}: ${id}`; - }, - hideCreateOptionOnSameID(id, options) { - if (options && options.length && options.firstObject.groupName) { - return !options.some((group) => group.options.findBy('id', id)); + // ----- adapted from ember-power-select-with-create + searchAndSuggest(term, select) { + if (term.length === 0) { + return this.options; + } + if (this.search) { + return resolve(this.search(term, select)).then((results) => { + if (results.toArray) { + results = results.toArray(); + } + this.addCreateOption(term, results); + return results; + }); } - let existingOption = this.options && (this.options.findBy('id', id) || this.options.findBy('name', id)); - if (this.disallowNewItems && !existingOption) { - return false; + const newOptions = this.filter(this.options, term); + this.addCreateOption(term, newOptions); + return newOptions; + }, + selectOrCreate(selection) { + if (selection && selection.__isSuggestion__) { + const name = selection.__value__; + this.selectedOptions.pushObject({ name, id: name, new: true }); + } else { + this.selectedOptions.pushObject(selection); + this.options.removeObject(selection); } - return !existingOption; + this.handleChange(); }, + // ----- }, }); diff --git a/ui/lib/core/addon/templates/components/search-select.hbs b/ui/lib/core/addon/templates/components/search-select.hbs index ce87273cd45818..0e5fe04ad227af 100644 --- a/ui/lib/core/addon/templates/components/search-select.hbs +++ b/ui/lib/core/addon/templates/components/search-select.hbs @@ -22,17 +22,14 @@ {{/if}} {{! template-lint-configure simple-unless "warn" }} {{#unless (gte this.selectedOptions.length this.selectLimit)}} - {{#if this.shouldRenderName}} @@ -43,7 +40,7 @@ {{else}} {{option.id}} {{/if}} - + {{/unless}}