Skip to content

Commit

Permalink
Search Select Input Fix (hashicorp#13590)
Browse files Browse the repository at this point in the history
* removes ember-power-select-with-create dep and updates search-select component

* adds changelog

* fixes matching when filtering options
  • Loading branch information
zofskeez authored and heppu committed Jan 13, 2022
1 parent aad0b4f commit b54e99f
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 83 deletions.
3 changes: 3 additions & 0 deletions changelog/13590.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes issue with SearchSelect component not holding focus
```
110 changes: 74 additions & 36 deletions ui/lib/core/addon/components/search-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <SearchSelect @id="group-policies" @models={{["policies/acl"]}} @onChange={{onChange}} @selectLimit={{2}} @inputValue={{get model valuePath}} @helpText="Policies associated with this group" @label="Policies" @fallbackComponent="string-list" />
*
* @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.
*
*/
Expand All @@ -48,6 +50,7 @@ export default Component.extend({
shouldUseFallback: false,
shouldRenderName: false,
disallowNewItems: false,

init() {
this._super(...arguments);
this.set('selectedOptions', this.inputValue || []);
Expand Down Expand Up @@ -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
Expand All @@ -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();
},
// -----
},
});
15 changes: 6 additions & 9 deletions ui/lib/core/addon/templates/components/search-select.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,14 @@
{{/if}}
{{! template-lint-configure simple-unless "warn" }}
{{#unless (gte this.selectedOptions.length this.selectLimit)}}
<PowerSelectWithCreate
<PowerSelect
@eventType="click"
@searchEnabled={{true}}
@search={{action "searchAndSuggest"}}
@options={{this.options}}
@search={{this.search}}
@onChange={{action "selectOption"}}
@onCreate={{action "createOption"}}
@onChange={{action "selectOrCreate"}}
@placeholderComponent={{component "search-select-placeholder"}}
@renderInPlace={{true}}
@searchField="searchText"
@verticalPosition="below"
@showCreateWhen={{action "hideCreateOptionOnSameID"}}
@buildSuggestion={{action "constructSuggestion"}}
as |option|
>
{{#if this.shouldRenderName}}
Expand All @@ -43,7 +40,7 @@
{{else}}
{{option.id}}
{{/if}}
</PowerSelectWithCreate>
</PowerSelect>
{{/unless}}
<ul class="search-select-list">
{{#each this.selectedOptions as |selected|}}
Expand Down
2 changes: 1 addition & 1 deletion ui/lib/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"ember-composable-helpers": "*",
"ember-concurrency": "*",
"ember-maybe-in-element": "*",
"ember-power-select-with-create": "*",
"ember-power-select": "*",
"ember-radio-button": "*",
"ember-router-helpers": "*",
"ember-svg-jar": "*",
Expand Down
32 changes: 19 additions & 13 deletions ui/lib/core/stories/search-select.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
<!--THIS FILE IS AUTO GENERATED. This file is generated from JSDoc comments in lib/core/addon/components/search-select.js. To make changes, first edit that file and run "yarn gen-story-md search-select" to re-generate the content.-->

## 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.

**Params**

| Param | Type | Description |
| --- | --- | --- |
| id | <code>String</code> | The name of the form field |
| models | <code>String</code> | An array of model types to fetch from the API. |
| onChange | <code>Func</code> | The onchange action for this form field. |
| inputValue | <code>String</code> \| <code>Array</code> | A comma-separated string or an array of strings. |
| [helpText] | <code>String</code> | Text to be displayed in the info tooltip for this form field |
| label | <code>String</code> | Label for this form field |
| fallbackComponent | <code>String</code> | name of component to be rendered if the API call 403s |
| options | <code>Array</code> | *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. |
| search | <code>Func</code> | *Advanced usage* - Customizes how the power-select component searches for matches - see the power-select docs for more information. |
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| id | <code>string</code> | | The name of the form field |
| models | <code>Array</code> | | An array of model types to fetch from the API. |
| onChange | <code>function</code> | | The onchange action for this form field. |
| inputValue | <code>string</code> \| <code>Array</code> | | A comma-separated string or an array of strings. |
| label | <code>string</code> | | Label for this form field |
| fallbackComponent | <code>string</code> | | name of component to be rendered if the API call 403s |
| [backend] | <code>string</code> | | name of the backend if the query for options needs additional information (eg. secret backend) |
| [disallowNewItems] | <code>boolean</code> | <code>false</code> | Controls whether or not the user can add a new item if none found |
| [helpText] | <code>string</code> | | Text to be displayed in the info tooltip for this form field |
| [selectLimit] | <code>number</code> | | A number that sets the limit to how many select options they can choose |
| [subText] | <code>string</code> | | Text to be displayed below the label |
| [subLabel] | <code>string</code> | | a smaller label below the main Label |
| [wildcardLabel] | <code>string</code> | | 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. |
| options | <code>Array</code> | | *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. |
| search | <code>function</code> | | *Advanced usage* - Customizes how the power-select component searches for matches - see the power-select docs for more information. |

**Example**

```js
<SearchSelect @id="group-policies" @models={{["policies/acl"]}} @onChange={{onChange}} @inputValue={{get model valuePath}} @helpText="Policies associated with this group" @label="Policies" @fallbackComponent="string-list" />
<SearchSelect @id="group-policies" @models={{["policies/acl"]}} @onChange={{onChange}} @selectLimit={{2}} @inputValue={{get model valuePath}} @helpText="Policies associated with this group" @label="Policies" @fallbackComponent="string-list" />
```

**See**
Expand Down
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
"ember-maybe-in-element": "^2.0.3",
"ember-modal-dialog": "^3.0.3",
"ember-page-title": "^6.0.3",
"ember-power-select-with-create": "0.9.0",
"ember-power-select": "^5.0.3",
"ember-promise-helpers": "^1.0.9",
"ember-qunit": "^5.1.4",
"ember-radio-button": "^2.0.1",
Expand Down
73 changes: 50 additions & 23 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2632,6 +2632,19 @@
resolve "^1.8.1"
semver "^7.3.2"

"@embroider/[email protected]":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.47.2.tgz#23cbe92cac3c24747f054e1eea2a22538bf7ebd0"
integrity sha512-ViNWluJCeM5OPlM3rs8kdOz3RV5rpfXX5D2rDnc/q86xRS0xf4NFEjYRV7W6fBcD0b3v5jSHDTwrjq9Kee4rHg==
dependencies:
"@embroider/shared-internals" "0.47.2"
assert-never "^1.2.1"
ember-cli-babel "^7.26.6"
find-up "^5.0.0"
lodash "^4.17.21"
resolve "^1.20.0"
semver "^7.3.2"

"@embroider/macros@^0.43.5":
version "0.43.5"
resolved "https://registry.yarnpkg.com/@embroider/macros/-/macros-0.43.5.tgz#f846bb883482436611a58a3512c687d4f9fddfad"
Expand Down Expand Up @@ -2670,6 +2683,19 @@
semver "^7.3.5"
typescript-memoize "^1.0.1"

"@embroider/[email protected]":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/shared-internals/-/shared-internals-0.47.2.tgz#24e9fa0dd9c529d5c996ee1325729ea08d1fa19f"
integrity sha512-SxdZYjAE0fiM5zGDz+12euWIsQZ1tsfR1k+NKmiWMyLhA5T3pNgbR2/Djvx/cVIxOtEavGGSllYbzRKBtV4xMg==
dependencies:
babel-import-util "^0.2.0"
ember-rfc176-data "^0.3.17"
fs-extra "^9.1.0"
lodash "^4.17.21"
resolve-package-path "^4.0.1"
semver "^7.3.5"
typescript-memoize "^1.0.1"

"@embroider/util@^0.39.0 || ^0.40.0 || ^0.41.0", "@embroider/util@^0.39.1 || ^0.40.0 || ^0.41.0":
version "0.41.0"
resolved "https://registry.yarnpkg.com/@embroider/util/-/util-0.41.0.tgz#5324cb4742aa4ed8d613c4f88a466f73e4e6acc1"
Expand All @@ -2679,6 +2705,15 @@
broccoli-funnel "^3.0.5"
ember-cli-babel "^7.23.1"

"@embroider/util@^0.47.2":
version "0.47.2"
resolved "https://registry.yarnpkg.com/@embroider/util/-/util-0.47.2.tgz#d06497b4b84c07ed9c7b628293bb019c533f4556"
integrity sha512-g9OqnFJPktGu9NS0Ug3Pxz1JE3jeDceeVE4IrlxDrVmBXMA/GrBvpwjolWgl6jh97cMJyExdz62jIvPHV4256Q==
dependencies:
"@embroider/macros" "0.47.2"
broccoli-funnel "^3.0.5"
ember-cli-babel "^7.23.1"

"@emotion/cache@^10.0.27", "@emotion/cache@^10.0.9":
version "10.0.29"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0"
Expand Down Expand Up @@ -9577,13 +9612,13 @@ ember-assign-helper@^0.2.0:
dependencies:
ember-cli-babel "^6.6.0"

ember-assign-helper@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/ember-assign-helper/-/ember-assign-helper-0.3.0.tgz#7a023dd165ef56b28f77f70fd20e88261380aca7"
integrity sha512-kDY0IRP6PUSJjghM2gIq24OD7d6XcZ1666zmZrywxEVjCenhaR0Oi/BXUU8JEATrIcXIExMIu34GKrHHlCLw0Q==
ember-assign-helper@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/ember-assign-helper/-/ember-assign-helper-0.4.0.tgz#f0a313033656c0d2cbbcb29d55b9cd13f04bc7c1"
integrity sha512-GKHhT4HD2fhtDnuBk6eCdCA8XGew9hY7TVs8zjrykegiI7weC0CGtpJscmIG3O0gEEb0d07UTkF2pjfNGLx4Nw==
dependencies:
ember-cli-babel "^7.19.0"
ember-cli-htmlbars "^4.3.1"
ember-cli-babel "^7.26.0"
ember-cli-htmlbars "^6.0.0"

ember-auto-import@^1.10.1, ember-auto-import@^1.11.3:
version "1.12.0"
Expand Down Expand Up @@ -9665,7 +9700,7 @@ [email protected]:
ember-cli-babel "^7.1.2"
ember-cli-htmlbars "^3.0.0"

[email protected], ember-basic-dropdown@^1.1.2, ember-basic-dropdown@^3.0.21:
[email protected], ember-basic-dropdown@^1.1.2, "ember-basic-dropdown@^3.1.0 || ^4.0.2":
version "3.0.19"
resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-3.0.19.tgz#e15e71097cbcbc585e85c2c5cf677a6434edb1d5"
integrity sha512-5mZ4hbfGLd+TrFAp0JsfcpIb10zqF60SorKc1Bsm29kJF2wy8p0JUXMb21VVF7+phkrRFYbcXy5enFc8qdm4xw==
Expand Down Expand Up @@ -9919,7 +9954,7 @@ ember-cli-htmlbars@^4.3.1:
strip-bom "^4.0.0"
walk-sync "^2.0.2"

ember-cli-htmlbars@^5.0.0, ember-cli-htmlbars@^5.1.0, ember-cli-htmlbars@^5.2.0, ember-cli-htmlbars@^5.3.2, ember-cli-htmlbars@^5.7.0:
ember-cli-htmlbars@^5.1.0, ember-cli-htmlbars@^5.2.0, ember-cli-htmlbars@^5.3.2, ember-cli-htmlbars@^5.7.0:
version "5.7.2"
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-5.7.2.tgz#e0cd2fb3c20d85fe4c3e228e6f0590ee1c645ba8"
integrity sha512-Uj6R+3TtBV5RZoJY14oZn/sNPnc+UgmC8nb5rI4P3fR/gYoyTFIZSXiIM7zl++IpMoIrocxOrgt+mhonKphgGg==
Expand Down Expand Up @@ -10619,24 +10654,16 @@ ember-page-title@^6.0.3:
dependencies:
ember-cli-babel "^7.23.1"

[email protected]:
version "0.9.0"
resolved "https://registry.yarnpkg.com/ember-power-select-with-create/-/ember-power-select-with-create-0.9.0.tgz#83ed037987dc5824f2bf702d278d055932ec1c87"
integrity sha512-AQW7N+vZHTojfKSqp/L3HlB81SW3AJtvFJYpdwHqoIcmTsW0BGfXH2z/GVWBUky6fPQLCngKs2Uv5PmpbboVYQ==
dependencies:
ember-cli-babel "^7.20.0"
ember-cli-htmlbars "^5.0.0"
ember-power-select "^4.0.0"

ember-power-select@^4.0.0:
version "4.1.7"
resolved "https://registry.yarnpkg.com/ember-power-select/-/ember-power-select-4.1.7.tgz#eb547dd37448357d8f3fa789db18ddbba43fb8ca"
integrity sha512-Q4cjUudWb7JA6q7qe0jhcpLsipuFUHMwkYC05HxST5qm3MRMEzs6KfZ3Xd/TcrjBLSoWniw3Q61Quwcb41w5Jw==
ember-power-select@^5.0.3:
version "5.0.3"
resolved "https://registry.yarnpkg.com/ember-power-select/-/ember-power-select-5.0.3.tgz#615da0742a222a8c090590dd2cbb2db3a6147ac5"
integrity sha512-V72DxohhuPdsF6Rcxu1cVL2faHSk83RdTCkxeDT71NeQQNpHKbslhc0c+MMU52wm0mzch+oWmti/2McpL/vIvQ==
dependencies:
"@embroider/util" "^0.47.2"
"@glimmer/component" "^1.0.4"
"@glimmer/tracking" "^1.0.4"
ember-assign-helper "^0.3.0"
ember-basic-dropdown "^3.0.21"
ember-assign-helper "^0.4.0"
ember-basic-dropdown "^3.1.0 || ^4.0.2"
ember-cli-babel "^7.26.0"
ember-cli-htmlbars "^6.0.0"
ember-cli-typescript "^4.2.0"
Expand Down

0 comments on commit b54e99f

Please sign in to comment.