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 10 commits into
base: develop
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## v72.0.0-SNAPSHOT - unreleased

### 🎁 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

### 💥 Breaking Changes

* Mobile `Navigator` no longer supports `animation` prop, and `NavigatorModel` no longer supports
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' : ''})`)
cnrudd marked this conversation as resolved.
Show resolved Hide resolved
),
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