Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Search Select Input Fix #13590

Merged
merged 3 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 -
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for cleaning up the documentation!

* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

much cleaner, I like it!

@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 @@ -125,7 +125,7 @@
"ember-maybe-import-regenerator": "^0.1.6",
"ember-maybe-in-element": "^2.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 @@ -10601,24 +10636,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