Skip to content

Commit

Permalink
Merge pull request #23 from fleetbase/dev-v0.2.1
Browse files Browse the repository at this point in the history
Dev v0.2.1
  • Loading branch information
roncodes authored Oct 26, 2023
2 parents 4f83cc5 + 6ab2d1d commit 76b784e
Show file tree
Hide file tree
Showing 15 changed files with 1,713 additions and 1,457 deletions.
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

0 comments on commit 76b784e

Please sign in to comment.