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

Excel like behavior for grid column filtering #3894

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

### 🎁 New Features

* Improvements to Grid columns `HeaderFilter` component:
* `GridFilterModel` `commitOnChage` now set to `false` by default
* Addition of ability to append terms to active filter **only** when `commitOnChage:false`
* Column header filtering functionality now similar to Excel on Windows
* Introduced a new "JSON Search" feature to the Hoist Admin Console, accessible from the Config,
User Preference, and JSON Blob tabs. Supports searching JSON values stored within these objects
to filter and match data using JSON Path expressions.
Expand Down
5 changes: 4 additions & 1 deletion cmp/grid/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export interface GridFilterModelConfig {
*/
bind?: Store | View;

/** True (default) to update filters immediately after each change made in the column-based filter UI.*/
/**
* True to update filters immediately after each change made in the column-based filter UI.
* Defaults to False.
*/
commitOnChange?: boolean;

/**
Expand Down
2 changes: 1 addition & 1 deletion cmp/grid/filter/GridFilterModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class GridFilterModel extends HoistModel {
static BLANK_PLACEHOLDER = '[blank]';

constructor(
{bind, commitOnChange = true, fieldSpecs, fieldSpecDefaults}: GridFilterModelConfig,
{bind, commitOnChange = false, fieldSpecs, fieldSpecDefaults}: GridFilterModelConfig,
gridModel: GridModel
) {
super();
Expand Down
13 changes: 13 additions & 0 deletions desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
.xh-values-filter-tab {
.store-filter-header {
padding: 5px 7px;
border-bottom: 1px solid var(--xh-grid-header-border-color);
row-gap: 5px;
.bp5-control-indicator {
font-size: 1em;
}
span {
font-size: var(--xh-grid-compact-header-font-size-px);
color: var(--xh-grid-header-text-color);
}
}

&__hidden-values-message {
display: flex;
padding: var(--xh-pad-half-px);
Expand Down
31 changes: 29 additions & 2 deletions desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
* Copyright © 2025 Extremely Heavy Industries Inc.
*/
import {grid} from '@xh/hoist/cmp/grid';
import {div, placeholder, vframe} from '@xh/hoist/cmp/layout';
import {div, hframe, placeholder, span, vbox, vframe} from '@xh/hoist/cmp/layout';
import {storeFilterField} from '@xh/hoist/cmp/store';
import {hoistCmp, uses} from '@xh/hoist/core';
import {button} from '@xh/hoist/desktop/cmp/button';
import {checkbox} from '@xh/hoist/desktop/cmp/input';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
import {Icon} from '@xh/hoist/icon';
Expand Down Expand Up @@ -47,7 +48,33 @@ const tbar = hoistCmp.factory(() => {
const body = hoistCmp.factory<ValuesTabModel>(({model}) => {
const {isCustomFilter} = model.headerFilterModel;
if (isCustomFilter) return customFilterPlaceholder();
return vframe(grid(), hiddenValuesMessage());
return vframe(storeFilterSelect(), grid(), hiddenValuesMessage());
});

const storeFilterSelect = hoistCmp.factory<ValuesTabModel>(({model}) => {
const {gridModel, allVisibleRecsChecked, filterText, headerFilterModel} = model,
{store} = gridModel;
return vbox({
className: 'store-filter-header',
items: [
hframe(
checkbox({
disabled: store.empty,
displayUnsetState: true,
value: allVisibleRecsChecked,
onChange: () => model.toggleAllRecsChecked()
}),
span(`(Select All${filterText ? ' Search Results' : ''})`)
),
hframe({
omit: !filterText || store.empty || headerFilterModel.commitOnChange,
items: [
checkbox({bind: 'combineCurrentFilters'}),
span(`Add current selection to filter`)
]
})
]
});
});

const customFilterPlaceholder = hoistCmp.factory<ValuesTabModel>(({model}) => {
Expand Down
52 changes: 37 additions & 15 deletions desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {FieldFilterSpec} from '@xh/hoist/data';
import {HeaderFilterModel} from '../HeaderFilterModel';
import {checkbox} from '@xh/hoist/desktop/cmp/input';
import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx';
import {castArray, difference, isEmpty, partition, uniq, without} from 'lodash';
import {castArray, difference, flatten, isEmpty, map, partition, uniq, without} from 'lodash';

export class ValuesTabModel extends HoistModel {
override xhImpl = true;
Expand All @@ -26,6 +26,12 @@ export class ValuesTabModel extends HoistModel {
/** Bound search term for `StoreFilterField` */
@bindable filterText: string = null;

/*
* Available only when commit on change is false merge
* current filter with pendingValues on commit
*/
@bindable combineCurrentFilters: boolean = false;

/** FieldFilter output by this model. */
@computed.struct
get filter(): FieldFilterSpec {
Expand Down Expand Up @@ -81,11 +87,18 @@ export class ValuesTabModel extends HoistModel {
this.headerFilterModel = headerFilterModel;
this.gridModel = this.createGridModel();

this.addReaction({
track: () => this.pendingValues,
run: () => this.syncGrid(),
fireImmediately: true
});
this.addReaction(
{
track: () => this.pendingValues,
run: () => this.syncGrid(),
fireImmediately: true
},
{
track: () => [this.filterText, this.combineCurrentFilters],
run: () => this.setPendingValues(),
debounce: 300
}
);
}

syncWithFilter() {
Expand Down Expand Up @@ -115,6 +128,23 @@ export class ValuesTabModel extends HoistModel {
//-------------------
// Implementation
//-------------------
@action
setPendingValues() {
if (!this.filterText) {
this.doSyncWithFilter();
this.syncGrid();
return;
}

const {records} = this.gridModel.store,
currentFilterValues = flatten(map(this.columnFilters, 'value')),
values = map(records, it => it.get('value'));

this.pendingValues = uniq(
this.combineCurrentFilters ? [...currentFilterValues, ...values] : values
);
}

private getFilter() {
const {gridFilterModel, pendingValues, values, valueCount, field} = this,
included = pendingValues.map(it => gridFilterModel.fromDisplayValue(it)),
Expand Down Expand Up @@ -217,17 +247,10 @@ export class ValuesTabModel extends HoistModel {
onRowClicked: ({data: record}) => {
this.setRecsChecked(!record.get('isChecked'), record.get('value'));
},
hideHeaders: true,
columns: [
{
field: 'isChecked',
headerName: ({gridModel}) => {
return checkbox({
disabled: gridModel.store.empty,
displayUnsetState: true,
value: this.allVisibleRecsChecked,
onChange: () => this.toggleAllRecsChecked()
});
},
width: 28,
autosizable: false,
pinned: true,
Expand All @@ -245,7 +268,6 @@ export class ValuesTabModel extends HoistModel {
},
{
field: 'value',
displayName: '(Select All)',
align: 'left',
comparator: (v1, v2, sortDir, abs, {defaultComparator}) => {
const mul = sortDir === 'desc' ? -1 : 1;
Expand Down
Loading