-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
1,694 additions
and
1,445 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,15 @@ | ||
<div class="fetch-select {{@wrapperClass}}" {{did-insert this.fetchOptions}}> | ||
<Select ...attributes @fetched={{true}} @options={{this.options}} @placeholder={{this.placeholder}} @optionLabel={{@optionLabel}} @optionValue={{@optionValue}} @onSelect={{@onSelect}} @humanize={{@humanize}} as |option key optionLabel|> | ||
{{yield option key optionLabel}} | ||
</Select> | ||
<div class="fleetbase-model-select fleetbase-power-select ember-model-select {{@wrapperClass}}"> | ||
<PowerSelect @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @animationEnabled={{@animationEnabled}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @beforeOptionsComponent={{@beforeOptionsComponent}} @buildSelection={{@buildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{or @dropdownClass "ember-model-select__dropdown"}} @extra={{@extra}} @groupComponent={{@groupComponent}} @highlightOnHover={{@highlightOnHover}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @eventType={{@eventType}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} @onChange={{this.onChange}} @onClose={{this.onClose}} @onFocus={{@onFocus}} @onInput={{this.onInput}} @onKeydown={{@onKeydown}} @onOpen={{this.onOpen}} @options={{this.options}} @optionsComponent={{component this.optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @renderInPlace={{@renderInPlace}} @scrollTo={{@scrollTo}} @search={{perform this.searchOptions}} @searchEnabled={{get-default-value @searchEnabled true}} @searchField={{@searchField}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} @selected={{this.selected}} @selectedItemComponent={{@selectedItemComponent}} @tabindex={{@tabindex}} @triggerClass="form-select form-input {{@triggerClass}}" @triggerComponent={{@triggerComponent}} @triggerId={{@triggerId}} @triggerRole={{@triggerRole}} @typeAheadMatcher={{@typeAheadMatcher}} @verticalPosition={{@verticalPosition}} @withCreate={{@withCreate}} ...attributes as |option|> | ||
{{#if (has-block)}} | ||
{{yield option}} | ||
{{else}} | ||
{{get option @optionLabel}} | ||
{{/if}} | ||
</PowerSelect> | ||
|
||
{{#if this.fetchOptions.isRunning}} | ||
<div class="ember-model-select__loading"> | ||
<ModelSelect::Spinner /> | ||
</div> | ||
{{/if}} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,233 @@ | ||
import Component from '@glimmer/component'; | ||
import { tracked } from '@glimmer/tracking'; | ||
import { inject as service } from '@ember/service'; | ||
import { isBlank } from '@ember/utils'; | ||
import { action, computed } from '@ember/object'; | ||
import { isEmpty } from '@ember/utils'; | ||
import { action } from '@ember/object'; | ||
import { isArray } from '@ember/array'; | ||
import { assign } from '@ember/polyfills'; | ||
import { assert } from '@ember/debug'; | ||
import { timeout } from 'ember-concurrency'; | ||
import { restartableTask, dropTask } from 'ember-concurrency-decorators'; | ||
|
||
/** | ||
* FetchSelectComponent is a Glimmer component responsible for rendering a | ||
* select input and fetching options asynchronously based on user input. | ||
* | ||
* @class FetchSelectComponent | ||
* @extends Component | ||
* @memberof FleetbaseComponents | ||
* | ||
* @property {Service} fetch - The fetch service injected into the component. | ||
* @property {Array} options - The list of selectable options. | ||
* @property {Object} selected - The currently selected option. | ||
* @property {number} debounceDuration - The duration to debounce the search input, in milliseconds. | ||
*/ | ||
export default class FetchSelectComponent extends Component { | ||
/** | ||
* The fetch service is used to make network requests to fetch the options for the select input. | ||
* @type {Service} | ||
*/ | ||
@service fetch; | ||
|
||
/** | ||
* The list of selectable options. | ||
* @type {Array} | ||
*/ | ||
@tracked options = []; | ||
@tracked isLoading = true; | ||
|
||
@computed('args.placeholder', 'isLoading') get palceholder() { | ||
const { placeholder } = this.args; | ||
/** | ||
* The currently selected option. | ||
* @type {Object} | ||
*/ | ||
@tracked selected; | ||
|
||
/** | ||
* The duration to debounce the search input, in milliseconds. | ||
* @type {number} | ||
*/ | ||
@tracked debounceDuration = 250; | ||
|
||
/** | ||
* The constructor ensures that the endpoint argument is specified, and | ||
* initializes the component's properties based on the arguments passed to it. | ||
*/ | ||
constructor() { | ||
super(...arguments); | ||
|
||
assert('<FetchSelect /> requires a valid `endpoint`.', !isEmpty(this.args.endpoint)); | ||
|
||
this.endpoint = this.args.endpoint; | ||
this.selected = this.setSelectedOption(this.args.selected); | ||
// this.debounceDuration = this.args.debounceDuration || this.debounceDuration; | ||
} | ||
|
||
/** | ||
* Searches for options based on the term provided. Debounces the search | ||
* if it's not the initial load. | ||
* | ||
* @param {string} term - The search term. | ||
* @param {Object} [options={}] - Additional options for the search. | ||
* @param {boolean} [initialLoad=false] - Whether this is the initial load. | ||
* @task | ||
*/ | ||
@restartableTask({ withTestWaiter: true }) searchOptions = function* (term, options = {}, initialLoad = false) { | ||
if (!initialLoad) { | ||
yield timeout(this.debounceDuration); | ||
} | ||
|
||
yield this.fetchOptions.perform(term, createOption); | ||
}; | ||
|
||
if (placeholder) { | ||
return placeholder; | ||
/** | ||
* Fetches options based on the term provided. | ||
* | ||
* @param {string} term - The search term. | ||
* @param {Object} [options={}] - Additional options for the fetch. | ||
* @task | ||
*/ | ||
@restartableTask({ withTestWaiter: true }) fetchOptions = function* (term, options = {}) { | ||
// query might be an EmptyObject/{{hash}}, make it a normal Object | ||
const query = assign({}, this.args.query); | ||
const endpoint = this.endpoint; | ||
|
||
if (term) { | ||
set(query, 'query', term); | ||
} | ||
|
||
if (this.isLoading) { | ||
return 'Loading options...'; | ||
let _options = yield this.fetch.get(this.endpoint, query, options); | ||
|
||
// if options returns is an object and not array | ||
if (this.isFetchResponseObject(_options)) { | ||
_options = this.convertOptionsObjectToArray(_options); | ||
} | ||
|
||
return null; | ||
// set options | ||
this.options = _options; | ||
return _options; | ||
}; | ||
|
||
convertOptionsObjectToArray(_options) { | ||
const objectKeys = Object.keys(_options); | ||
const _optionsFromObject = []; | ||
|
||
objectKeys.forEach((key) => { | ||
_optionsFromObject.pushObject({ | ||
key, | ||
value: _options[key], | ||
}); | ||
}); | ||
|
||
return _optionsFromObject; | ||
} | ||
|
||
@action fetchOptions() { | ||
const { path } = this.args; | ||
isFetchResponseObject(_options) { | ||
return !isArray(_options) && typeof _options === 'object' && Object.keys(_options).length; | ||
} | ||
|
||
if (isBlank(path)) { | ||
return; | ||
} | ||
/** | ||
* Set the selected option. | ||
* | ||
* @param {*} selected | ||
* @memberof FetchSelectComponent | ||
*/ | ||
setSelectedOption(selected) { | ||
const { optionValue } = this.args; | ||
|
||
if (optionValue) { | ||
this.fetchOptions.perform().then((options) => { | ||
let foundSelected = null; | ||
|
||
this.fetch | ||
.get(path) | ||
.then((options) => { | ||
this.options = options; | ||
}) | ||
.finally(() => { | ||
this.isLoading = false; | ||
if (isArray(options)) { | ||
foundSelected = options.find((option) => option[optionValue] === selected); | ||
} | ||
|
||
if (foundSelected) { | ||
this.selected = foundSelected; | ||
} else { | ||
this.selected = selected; | ||
} | ||
}); | ||
} else { | ||
this.selected = selected; | ||
} | ||
} | ||
|
||
/** | ||
* Loads the default set of options. | ||
*/ | ||
loadDefaultOptions() { | ||
const { loadDefaultOptions } = this.args; | ||
|
||
if (loadDefaultOptions === undefined || loadDefaultOptions) { | ||
this.fetchOptions.perform(null, {}, true); | ||
} | ||
} | ||
|
||
/** | ||
* Called when the select input is opened. | ||
* @action | ||
*/ | ||
@action onOpen() { | ||
const { onOpen } = this.args; | ||
|
||
this.loadDefaultOptions(); | ||
|
||
if (typeof onOpen === 'function') { | ||
onOpen(...arguments); | ||
} | ||
} | ||
|
||
/** | ||
* Called when the user inputs a search term. | ||
* | ||
* @param {string} term - The search term. | ||
* @action | ||
*/ | ||
@action onInput(term) { | ||
const { onInput } = this.args; | ||
|
||
if (isEmpty(term)) { | ||
this.loadDefaultOptions(); | ||
} | ||
|
||
if (typeof onInput === 'function') { | ||
onInput(...arguments); | ||
} | ||
} | ||
|
||
/** | ||
* Called when an option is selected. | ||
* | ||
* @param {Object} option - The selected option. | ||
* @action | ||
*/ | ||
@action onChange(option, ...rest) { | ||
const { onChange, optionValue } = this.args; | ||
|
||
// set selected | ||
this.selected = option; | ||
|
||
// if option value supplied | ||
if (optionValue && typeof option === 'object') { | ||
option = option[optionValue]; | ||
} | ||
|
||
if (typeof onChange === 'function') { | ||
onChange(option, ...rest); | ||
} | ||
} | ||
|
||
/** | ||
* Called when the select input is closed. | ||
* @action | ||
*/ | ||
@action onClose() { | ||
const { onClose } = this.args; | ||
|
||
this.fetchOptions.cancelAll(); | ||
|
||
if (typeof onClose === 'function') { | ||
onClose(...arguments); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
<ModelSelect @modelName={{@filter.model}} @query={{@filter.query}} @labelProperty={{or @filter.modelNamePath "name"}} @selectedModel={{this.selectedModel}} @placeholder={{@placeholder}} @triggerClass="form-select form-input form-input-sm flex-1" @infiniteScroll={{false}} @renderInPlace={{true}} @onChange={{this.onChange}} @allowClear={{true}} @onClear={{this.clear}} /> | ||
<ModelSelect @modelName={{@filter.model}} @query={{@filter.query}} @optionLabel={{or @filter.modelNamePath "name"}} @selectedModel={{this.selectedModel}} @placeholder={{@placeholder}} @triggerClass="form-select form-input form-input-sm flex-1" @infiniteScroll={{false}} @renderInPlace={{true}} @onChange={{this.onChange}} @allowClear={{true}} @onClear={{this.clear}} /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
<ModelSelect @modelName={{@modelName}} @selectedModel={{@selectedModel}} @labelProperty={{@labelProperty}} @searchProperty={{@searchProperty}} @searchKey={{@searchKey}} @loadDefaultOptions={{@loadDefaultOptions}} @infiniteScroll={{@infiniteScroll}} @pageSize={{@pageSize}} @query={{@query}} @debounceDuration={{this.debounceDuration}} @withCreate={{@withCreate}} @buildSuggestion={{@buildSuggestion}} @perPageParam={{@perPageParam}} @pageParam={{@pageParam}} @totalPagesParam={{@totalPagesParam}} @onCreate={{@onCreate}} {{!-- overwritten arguments --}} @onChange={{this.change}} @searchField={{@labelProperty}} @triggerClass="ember-model-select-multiple-trigger {{@triggerClass}}" {{!-- power-select-multiple defaults --}} @triggerRole={{@triggerRole}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @beforeOptionsComponent={{or @beforeOptionsComponent null}} @buildSelection={{or @buildSelection this.defaultBuildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{@dropdownClass}} @extra={{@extra}} @groupComponent={{@groupComponent}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} {{!-- @onChange={{@onChange}} --}} @onClose={{@onClose}} @onFocus={{this.handleFocus}} @onInput={{@onInput}} @onKeydown={{this.handleKeydown}} @onOpen={{this.handleOpen}} @options={{@options}} @optionsComponent={{@optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @registerAPI={{@registerAPI}} @renderInPlace={{@renderInPlace}} @required={{@required}} @scrollTo={{@scrollTo}} @search={{@search}} @searchEnabled={{@searchEnabled}} {{!-- @searchField={{@searchField}} --}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} {{!-- @selected={{@selected}} --}} @selectedItemComponent={{@selectedItemComponent}} @eventType={{@eventType}} @title={{@title}} {{!-- @triggerClass="ember-power-select-multiple-trigger {{@triggerClass}}" --}} @triggerComponent={{component (or @triggerComponent "power-select-multiple/trigger") tabindex=@tabindex}} @triggerId={{@triggerId}} @verticalPosition={{@verticalPosition}} @tabindex={{this.computedTabIndex}} ...attributes as |model|> | ||
<ModelSelect @modelName={{@modelName}} @selectedModel={{@selectedModel}} @optionLabel={{@optionLabel}} @searchProperty={{@searchProperty}} @searchKey={{@searchKey}} @loadDefaultOptions={{@loadDefaultOptions}} @infiniteScroll={{@infiniteScroll}} @pageSize={{@pageSize}} @query={{@query}} @debounceDuration={{this.debounceDuration}} @withCreate={{@withCreate}} @buildSuggestion={{@buildSuggestion}} @perPageParam={{@perPageParam}} @pageParam={{@pageParam}} @totalPagesParam={{@totalPagesParam}} @onCreate={{@onCreate}} {{!-- overwritten arguments --}} @onChange={{this.change}} @searchField={{@optionLabel}} @triggerClass="ember-model-select-multiple-trigger {{@triggerClass}}" {{!-- power-select-multiple defaults --}} @triggerRole={{@triggerRole}} @ariaDescribedBy={{@ariaDescribedBy}} @ariaInvalid={{@ariaInvalid}} @ariaLabel={{@ariaLabel}} @ariaLabelledBy={{@ariaLabelledBy}} @afterOptionsComponent={{@afterOptionsComponent}} @allowClear={{@allowClear}} @beforeOptionsComponent={{or @beforeOptionsComponent null}} @buildSelection={{or @buildSelection this.defaultBuildSelection}} @calculatePosition={{@calculatePosition}} @closeOnSelect={{@closeOnSelect}} @defaultHighlighted={{@defaultHighlighted}} @destination={{@destination}} @disabled={{@disabled}} @dropdownClass={{@dropdownClass}} @extra={{@extra}} @groupComponent={{@groupComponent}} @horizontalPosition={{@horizontalPosition}} @initiallyOpened={{@initiallyOpened}} @loadingMessage={{@loadingMessage}} @matcher={{@matcher}} @matchTriggerWidth={{@matchTriggerWidth}} @noMatchesMessage={{@noMatchesMessage}} @onBlur={{@onBlur}} {{!-- @onChange={{@onChange}} --}} @onClose={{@onClose}} @onFocus={{this.handleFocus}} @onInput={{@onInput}} @onKeydown={{this.handleKeydown}} @onOpen={{this.handleOpen}} @options={{@options}} @optionsComponent={{@optionsComponent}} @placeholder={{@placeholder}} @placeholderComponent={{@placeholderComponent}} @preventScroll={{@preventScroll}} @registerAPI={{@registerAPI}} @renderInPlace={{@renderInPlace}} @required={{@required}} @scrollTo={{@scrollTo}} @search={{@search}} @searchEnabled={{@searchEnabled}} {{!-- @searchField={{@searchField}} --}} @searchMessage={{@searchMessage}} @searchPlaceholder={{@searchPlaceholder}} {{!-- @selected={{@selected}} --}} @selectedItemComponent={{@selectedItemComponent}} @eventType={{@eventType}} @title={{@title}} {{!-- @triggerClass="ember-power-select-multiple-trigger {{@triggerClass}}" --}} @triggerComponent={{component (or @triggerComponent "power-select-multiple/trigger") tabindex=@tabindex}} @triggerId={{@triggerId}} @verticalPosition={{@verticalPosition}} @tabindex={{this.computedTabIndex}} ...attributes as |model|> | ||
{{#if (has-block)}} | ||
{{yield model}} | ||
{{else}} | ||
{{get model @labelProperty}} | ||
{{get model @optionLabel}} | ||
{{/if}} | ||
</ModelSelect> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.