Skip to content
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/dropdown-button.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
{{did-insert this.onInsert}}
as |dd|
>
<dd.Trigger class={{@triggerClass}} {{did-insert this.onTriggerInsert}} {{did-update this.onArgsChanged @disabled @visible @permission}}>
<dd.Trigger class={{@triggerClass}} {{did-insert this.onTriggerInsert}} {{did-update this.onArgsChanged @disabled @visible @permission @buttonComponentArgs}}>
{{#if @buttonComponent}}
{{component
@buttonComponent
Expand Down
3 changes: 2 additions & 1 deletion addon/components/dropdown-button.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ export default class DropdownButtonComponent extends Component {
this._onInsertFired = true;
}

@action onArgsChanged(el, [disabled = false, visible = true, permission = null]) {
@action onArgsChanged(el, [disabled = false, visible = true, permission = null, buttonComponentArgs = {}]) {
this.buttonComponentArgs = buttonComponentArgs;
this.visible = visible;
this.disabled = disabled;
if (!disabled && permission) {
Expand Down
3 changes: 3 additions & 0 deletions addon/components/filter/checkbox.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="filter-checkbox">
<Checkbox @value={{this.value}} @label={{or @filter.filterLabel @filter.label}} @onToggle={{this.onChange}} @alignItems="center" @labelClass="mb-0i" />
</div>
23 changes: 23 additions & 0 deletions addon/components/filter/checkbox.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import toBoolean from '@fleetbase/ember-core/utils/to-boolean';

export default class FilterCheckboxComponent extends Component {
@tracked value = false;

constructor(owner, { value = false }) {
super(...arguments);
this.value = toBoolean(value);
}

@action onChange(checked) {
const { onChange, filter } = this.args;

this.value = checked;

if (typeof onChange === 'function') {
onChange(filter, checked);
}
}
}
4 changes: 3 additions & 1 deletion addon/components/filters-picker.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
<div class="grid grid-cols-3 lg:grid-cols-3 sm:grid-cols-1 gap-2 w-full mb-4">
{{#each this.filters as |filter|}}
<div class="filter-component">
<label class="filter-component-label">{{or filter.filterLabel filter.label}}</label>
{{#unless filter.noFilterLabel}}
<label class="filter-component-label">{{or filter.filterLabel filter.label}}</label>
{{/unless}}
{{component
filter.filterComponent
value=filter.filterValue
Expand Down
225 changes: 79 additions & 146 deletions addon/components/filters-picker.js
Original file line number Diff line number Diff line change
@@ -1,183 +1,116 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { set, action } from '@ember/object';
import { filter, gt } from '@ember/object/computed';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { isArray } from '@ember/array';
import { later } from '@ember/runloop';
import getUrlParam from '../utils/get-url-param';

export default class FiltersPickerComponent extends Component {
/**
* Inject router to handle param changes via `transitionTo`
*
* @memberof FiltersPickerComponent
*/
@service hostRouter;

/**
* Array of filters created from columns argument.
*
* @memberof FiltersPickerComponent
*/
@tracked filters = [];

/**
* Filters which are active and should be applied.
*
* @memberof FiltersPickerComponent
*/
@filter('filters.@each.isFilterActive', (filter) => filter.isFilterActive === true) activeFilters;

/**
* Computed property that determines if any filters are set.
*
* @memberof FiltersPickerComponent
*/
@gt('activeFilters.length', 0) hasFilters;

/**
* Creates an instance of FiltersPickerComponent.
* @memberof FiltersPickerComponent
*/
get activeFilters() {
return this.filters.filter((f) => f.isFilterActive);
}

get hasFilters() {
return this.activeFilters.length > 0;
}

constructor() {
super(...arguments);
this.updateFilters();

this.#rebuildFilters(); // initial state

// Refresh whenever the route (→ query-params) changes
this._routeHandler = () => this.#rebuildFilters();
this.hostRouter.on('routeDidChange', this._routeHandler);
}

/**
* Creates and updates filters via map
*
* @param {null|Function} onColumn
* @memberof FiltersPickerComponent
*/
@action updateFilters(onColumn) {
this.filters = this.args.columns
.filter((column) => column.filterable)
.map((column, trueIndex) => {
// add true index to column
column = { ...column, trueIndex };

// set the column param
column.param = column.filterParam ?? column.valuePath;

// get the active param if any and update filter
const activeParam = getUrlParam(column.param);

// update if an activeParam exists
if (activeParam) {
column.isFilterActive = true;

if (isArray(activeParam) && activeParam.length === 0) {
column.isFilterActive = false;
}

column.filterValue = activeParam;
}
willDestroy() {
super.willDestroy(...arguments);
this.hostRouter.off('routeDidChange', this._routeHandler);
}

#readUrlValue(param) {
const raw = getUrlParam(param); // string | string[] | undefined
if (isArray(raw)) {
return raw.length ? raw : undefined;
}
return raw === '' ? undefined : raw;
}

#rebuildFilters(onColumn) {
const cols = this.args.columns ?? [];

this.filters = cols
.filter((c) => c.filterable)
.map((column, index) => {
const param = column.filterParam ?? column.valuePath;
const value = this.#readUrlValue(param);
const active = value != null; // null & undefined only

const filterCol = {
...column,
trueIndex: index,
param,
filterValue: value,
isFilterActive: active,
};

// callback to modify column from hook
if (typeof onColumn === 'function') {
onColumn(column, trueIndex, activeParam);
onColumn(filterCol, index, value);
}

return column;
return filterCol;
});

return this;
}

/**
* Triggers the apply callback for the filters picker.
*
* @memberof FiltersPickerComponent
*/
@action applyFilters() {
const { onApply } = this.args;

// run `onApply()` callback
if (typeof onApply === 'function') {
onApply();
if (typeof this.args.onApply === 'function') {
this.args.onApply();
}

// manually run update filters after apply with slight 300ms delay to update activeFilters
later(
this,
() => {
this.updateFilters();
},
150
);
}

/**
* Updates an individual filter/column value.
*
* @param {String} key
* @param {*} value
* @memberof FiltersPickerComponent
*/
@action updateFilterValue({ param }, value) {
const { onChange } = this.args;

// run `onChange()` callback
if (typeof onChange === 'function') {
onChange(param, value);
if (typeof this.args.onChange === 'function') {
this.args.onChange(param, value);
}
}

/**
* Callback to clear a single filter/column value.
*
* @param {String} key
* @memberof FiltersPickerComponent
*/
@action clearFilterValue({ param }) {
const { onFilterClear } = this.args;

// update filters
this.updateFilters((column) => {
if (column.param !== param) {
return;
}

// clear column values
set(column, 'filterValue', undefined);
set(column, 'isFilterActive', false);
});

// run `onFilterClear()` callback
if (typeof onFilterClear === 'function') {
onFilterClear(param);
if (typeof this.args.onFilterClear === 'function') {
this.args.onFilterClear(param);
}
}

/**
* Used to clear all filter/column values and URL params.
*
* @memberof FiltersPickerComponent
*/
@action clearFilters() {
const { onClear } = this.args;
const currentRouteName = this.hostRouter.currentRouteName;
const currentQueryParams = { ...this.hostRouter.currentRoute.queryParams };

// update filters
this.updateFilters((column) => {
const paramKey = column.filterParam ?? column.valuePath;
delete currentQueryParams[paramKey];
delete currentQueryParams[`${paramKey}[]`];

// reset column values
set(column, 'filterValue', undefined);
set(column, 'isFilterActive', false);
});

// transition to cleared params with router service
this.hostRouter.transitionTo(currentRouteName, { queryParams: currentQueryParams });

// run `onClear()` callback
if (typeof onClear === 'function') {
onClear(...arguments);
@action async clearFilters(...args) {
if (typeof this.args.onClear === 'function') {
this.args.onClear(...args);
}

// Build a clean query-param bag
const qp = { ...this.hostRouter.currentRoute.queryParams };
(this.args.columns ?? [])
.filter((c) => c.filterable)
.forEach((c) => {
const key = c.filterParam ?? c.valuePath;
delete qp[key];
delete qp[`${key}[]`];
});

// Transition – routeDidChange listener will rebuild afterwards
try {
await this.hostRouter.transitionTo(this.hostRouter.currentRouteName, {
queryParams: qp,
});
} catch (error) {
// Ignore only the "transition aborted" case
if (error?.name !== 'TransitionAborted') {
throw error; // real error → rethrow
}
}
}
}
6 changes: 3 additions & 3 deletions addon/components/filters-picker/button.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Button @type={{@type}} @text={{or @text "Filters"}} @icon={{or @icon "filter"}} @size={{or @size "xs"}} @wrapperClass={{@wrapperClass}} ...attributes>
{{#if @buttonComponentArgs.activeFilters.length}}
<Button @type={{@type}} @text={{or @text "Filters"}} @icon={{or @icon "filter"}} @size={{or @size "xs"}} @wrapperClass={{@wrapperClass}} {{did-update this.handleComponentArgsUpdate @buttonComponentArgs}} ...attributes>
{{#if this.buttonComponentArgs.activeFilters.length}}
<div class="box-divider">
<span class="font-semibold text-sky-500">{{@buttonComponentArgs.activeFilters.length}}</span>
<span class="font-semibold text-sky-500">{{this.buttonComponentArgs.activeFilters.length}}</span>
</div>
{{/if}}
</Button>
16 changes: 16 additions & 0 deletions addon/components/filters-picker/button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

export default class FiltersPickerButtonComponent extends Component {
@tracked buttonComponentArgs = {};

constructor(owner, { buttonComponentArgs = {} }) {
super(...arguments);
this.buttonComponentArgs = buttonComponentArgs;
}

@action handleComponentArgsUpdate(el, [buttonComponentArgs = {}]) {
this.buttonComponentArgs = buttonComponentArgs;
}
}
27 changes: 20 additions & 7 deletions addon/components/modals/bulk-action-model.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
{{pluralize @options.modelName}}?
{{/if}}
</h3>
<div class="mt-2">
<div class="mt-0.5">
<p class="text-sm leading-5 text-gray-500 dark:text-gray-50">
{{#if @options.message}}
{{@options.message}}
Expand All @@ -36,14 +36,27 @@
</div>
</div>
</div>
<ul class="list-scroll-box rounded shadow border border-gray-300 bg-gray-100 dark:bg-gray-800">
<div>{{yield @options}}</div>
<ul class="list-scroll-box rounded shadow border border-gray-300 bg-gray-100 dark:bg-gray-800 {{@options.listScrollBoxClass}}">
{{#each @options.selected as |selected|}}
<li>
<span class="text-sm dark:text-gray-100">{{n-a (get selected @options.modelNamePath)}}</span>
<li id={{selected.id}} data-public-id={{selected.public_id}}>
<div>
{{#if @options.modelNameRenderComponent}}
{{component @options.modalNameRenderComponent options=@options}}
{{else}}
{{#if @options.resolveModelName}}
<span class="text-sm dark:text-gray-100">{{n-a selected.list_resolved_name}}</span>
{{else}}
<span class="text-sm dark:text-gray-100">{{n-a (get selected @options.modelNamePath)}}</span>
{{/if}}
{{/if}}
</div>

<a href="javascript:;" {{on "click" (fn @options.remove selected)}} class="my-1">
<FaIcon @icon="times-circle" />
</a>
<div>
<a href="javascript:;" {{on "click" (fn @options.remove selected)}} class="my-1">
<FaIcon @icon="times-circle" />
</a>
</div>
</li>
{{/each}}
</ul>
Expand Down
Loading
Loading