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

Dev v0.2.1 #23

Merged
merged 4 commits into from
Oct 26, 2023
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
2 changes: 1 addition & 1 deletion addon/components/badge.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="status-badge {{safe-dasherize @status}}-status-badge" ...attributes>
<div class="status-badge {{safe-dasherize (or @status @type)}}-status-badge" ...attributes>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium leading-4 whitespace-no-wrap {{@spanClass}}">
<svg class="mr-1.5 h-2 w-2 {{if @hideStatusDot "hidden"}}" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3"></circle>
Expand Down
18 changes: 14 additions & 4 deletions addon/components/fetch-select.hbs
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>
234 changes: 212 additions & 22 deletions addon/components/fetch-select.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,232 @@
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, set } 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 } 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, options);
};

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);

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);
}
}
}
2 changes: 1 addition & 1 deletion addon/components/filter/model.hbs
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}} />
2 changes: 1 addition & 1 deletion addon/components/modal/layouts/confirm.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<Modal::Default @modalIsOpened={{@modalIsOpened}} @options={{@options}} @confirm={{@onConfirm}} @decline={{@onDecline}}>
<div class="modal-body-container flex {{if @options.body 'items-start' 'items-center'}}">
<div class="px-6 py-4 flex {{if @options.body 'items-start' 'items-center'}}">
<div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-red-100 rounded-full sm:mx-0 sm:h-10 sm:w-10">
{{#if @options.icon}}
<FaIcon @icon={{@options.icon}} @size={{@options.iconSize}} @spin={{@options.iconSpin}} @flip={{@options.iconFlip}} class={{@options.iconClass}} />
Expand Down
7 changes: 5 additions & 2 deletions addon/components/modals/changelog.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
</div>
{{else}}
{{#each this.releases as |release|}}
<div class="mb-3">
<h2 class="font-mono text-black dark:text-gray-100 font-bold text-base mb-1">{{release.name}}</h2>
<div class="mb-4">
<div class="flex flex-row">
<h2 class="font-mono text-black dark:text-gray-100 font-bold text-base mb-1">{{release.name}}</h2>
<span class="text-xs font-mono text-black dark:text-gray-100">{{release.created_at}}</span>
</div>
<div class="pl-6">
<ul class="list-disc">
{{#each release.changes as |change|}}
Expand Down
4 changes: 2 additions & 2 deletions addon/components/model-select-multiple.hbs
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>
Loading
Loading