Skip to content

Commit

Permalink
Make Column Filter UI lazy. (#3873)
Browse files Browse the repository at this point in the history
  • Loading branch information
lbwexler authored Dec 27, 2024
1 parent 2368d09 commit 24c7fbd
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 250 deletions.
5 changes: 1 addition & 4 deletions cmp/grid/impl/ColumnHeader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,7 @@ class ColumnHeaderModel extends HoistModel {
this.availableSorts = this.parseAvailableSorts();

if (!XH.isMobileApp && xhColumn?.filterable && filterModel?.getFieldSpec(xhColumn.field)) {
this.columnHeaderFilterModel = new ColumnHeaderFilterModel({
filterModel,
column: xhColumn
});
this.columnHeaderFilterModel = new ColumnHeaderFilterModel(filterModel, xhColumn);
this.enableFilter = true;
} else {
this.isAgFiltered = agColumn.isFilterActive();
Expand Down
81 changes: 6 additions & 75 deletions desktop/cmp/grid/impl/filter/ColumnHeaderFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@
* Copyright © 2024 Extremely Heavy Industries Inc.
*/

import {div, filler} from '@xh/hoist/cmp/layout';
import {tabContainer} from '@xh/hoist/cmp/tab';
import {div, span} from '@xh/hoist/cmp/layout';
import {hoistCmp, uses} from '@xh/hoist/core';
import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button';
import {panel} from '@xh/hoist/desktop/cmp/panel';
import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
import {Icon} from '@xh/hoist/icon';
import {popover} from '@xh/hoist/kit/blueprint';
import {stopPropagation} from '@xh/hoist/utils/js';
import './ColumnHeaderFilter.scss';
import classNames from 'classnames';
import {ColumnHeaderFilterModel} from './ColumnHeaderFilterModel';
import {headerFilter} from './headerfilter/HeaderFilter';

/**
* Component to manage column filters from header. Will appear as a "filter" icon if filters are
Expand All @@ -41,83 +37,18 @@ export const columnHeaderFilter = hoistCmp.factory({
hasBackdrop: true,
interactionKind: 'click',
onInteraction: open => {
if (!open) model.closeMenu();
if (!open) model.close();
},
item: div({
item: hasFilter ? Icon.filter() : Icon.columnMenu(),
onClick: e => {
e.stopPropagation();
model.openMenu();
model.open();
}
}),
targetTagName: 'div',
content: content()
// Force unmount on close
content: isOpen ? headerFilter() : span()
});
}
});

const content = hoistCmp.factory({
render() {
return panel({
title: `Filter`,
className: 'xh-column-header-filter',
compactHeader: true,
onClick: stopPropagation,
onDoubleClick: stopPropagation,
headerItems: [switcher()],
item: tabContainer(),
bbar: bbar()
});
}
});

const bbar = hoistCmp.factory<ColumnHeaderFilterModel>({
render({model}) {
const {commitOnChange} = model;
return toolbar({
compact: true,
items: [
filler(),
button({
icon: Icon.delete(),
text: 'Clear Filter',
intent: 'danger',
disabled: !model.hasFilter,
onClick: () => model.clear()
}),
button({
omit: commitOnChange,
icon: Icon.check(),
text: 'Apply Filter',
intent: 'success',
disabled: !model.hasFilter && !model.hasPendingFilter,
onClick: () => model.commit()
})
]
});
}
});

const switcher = hoistCmp.factory<ColumnHeaderFilterModel>(({model}) => {
const {fieldType, enableValues} = model.fieldSpec,
{tabs} = model.tabContainerModel;

return buttonGroup({
omit: !enableValues || fieldType === 'bool',
className: 'xh-column-header-filter__tab-switcher',
items: tabs.map(it => switcherButton({...it}))
});
});

const switcherButton = hoistCmp.factory<ColumnHeaderFilterModel>(({model, id, title}) => {
const {tabContainerModel} = model,
{activeTabId} = tabContainerModel;

return button({
className: 'xh-column-header-filter__tab-switcher__button',
text: title,
active: activeTabId === id,
outlined: true,
onClick: () => tabContainerModel.activateTab(id)
});
});
172 changes: 12 additions & 160 deletions desktop/cmp/grid/impl/filter/ColumnHeaderFilterModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,186 +6,38 @@
*/

import {Column} from '@xh/hoist/cmp/grid';
import {TabContainerModel} from '@xh/hoist/cmp/tab';
import {HoistModel, managed} from '@xh/hoist/core';
import {action, computed, makeObservable, observable} from '@xh/hoist/mobx';
import {wait} from '@xh/hoist/promise';
import {HoistModel} from '@xh/hoist/core';
import {action, makeObservable, observable} from '@xh/hoist/mobx';
import {isEmpty} from 'lodash';
import {GridFilterModel, GridFilterFieldSpec} from '@xh/hoist/cmp/grid';
import {customTab} from './custom/CustomTab';
import {CustomTabModel} from './custom/CustomTabModel';
import {valuesTab} from './values/ValuesTab';
import {ValuesTabModel} from './values/ValuesTabModel';
import {GridFilterModel} from '@xh/hoist/cmp/grid';

export class ColumnHeaderFilterModel extends HoistModel {
override xhImpl = true;

column: Column;
gridFilterModel: GridFilterModel;
fieldSpec: GridFilterFieldSpec;
readonly column: Column;
readonly filterModel: GridFilterModel;

@observable isOpen = false;

@managed tabContainerModel: TabContainerModel;
@managed valuesTabModel: ValuesTabModel;
@managed customTabModel: CustomTabModel;

get field() {
return this.fieldSpec.field;
}

get store() {
return this.gridFilterModel.gridModel.store;
}

get fieldType() {
return this.store.getField(this.field).type;
}

get currentGridFilter() {
return this.gridFilterModel.filter;
}

get columnFilters() {
return this.gridFilterModel.getColumnFilters(this.field);
}

get columnCompoundFilter() {
return this.gridFilterModel.getColumnCompoundFilter(this.field);
}
@observable isOpen: boolean = false;

get hasFilter() {
return !isEmpty(this.columnFilters);
}

get hasPendingFilter() {
const {activeTabId} = this.tabContainerModel;
return activeTabId === 'valuesFilter'
? !!this.valuesTabModel.filter
: !!this.customTabModel.filter;
}

@computed
get isCustomFilter() {
const {columnCompoundFilter, columnFilters} = this;
if (columnCompoundFilter) return true;
if (isEmpty(columnFilters)) return false;
return columnFilters.some(it => !['=', '!=', 'includes'].includes(it.op));
}

get commitOnChange() {
return this.gridFilterModel.commitOnChange;
const filters = this.filterModel.getColumnFilters(this.column.field);
return !isEmpty(filters);
}

constructor({filterModel, column}) {
constructor(filterModel: GridFilterModel, column: Column) {
super();
makeObservable(this);

this.gridFilterModel = filterModel;
this.filterModel = filterModel;
this.column = column;
this.fieldSpec = filterModel.getFieldSpec(column.field);

const {enableValues} = this.fieldSpec;
this.valuesTabModel = enableValues ? new ValuesTabModel(this) : null;
this.customTabModel = new CustomTabModel(this);
this.tabContainerModel = new TabContainerModel({
switcher: false,
tabs: [
{
id: 'valuesFilter',
title: 'Values',
content: valuesTab,
omit: !enableValues
},
{
id: 'customFilter',
title: 'Custom',
content: customTab
}
],
xhImpl: true
});

this.addReaction({
track: () => this.valuesTabModel?.filter,
run: () => this.doCommitOnChange('valuesFilter'),
debounce: 100
});

this.addReaction({
track: () => this.customTabModel.filter,
run: () => this.doCommitOnChange('customFilter'),
debounce: 100
});
}

@action
commit(close = true) {
const {tabContainerModel, customTabModel, valuesTabModel} = this,
{activeTabId} = tabContainerModel,
valuesIsActive = activeTabId === 'valuesFilter',
activeTabModel = valuesIsActive ? valuesTabModel : customTabModel,
otherTabModel = valuesIsActive ? customTabModel : valuesTabModel;

this.setColumnFilters(activeTabModel.filter);
if (close) {
this.closeMenu();
} else {
// We must wait before resetting as GridFilterModel.setFilter() is async
wait().then(() => otherTabModel?.reset());
}
}

@action
clear(close = true) {
this.setColumnFilters(null);
if (close) {
this.closeMenu();
} else {
// We must wait before resetting as GridFilterModel.setFilter() is async
wait().then(() => this.resetTabModels());
}
}

@action
openMenu() {
open() {
this.isOpen = true;
this.syncWithFilter();
}

@action
closeMenu() {
close() {
this.isOpen = false;
}

//-------------------
// Implementation
//-------------------
@action
private syncWithFilter() {
const {isCustomFilter, valuesTabModel, customTabModel, tabContainerModel} = this,
useCustomTab = isCustomFilter || !valuesTabModel,
toTab = useCustomTab ? customTabModel : valuesTabModel,
toTabId = useCustomTab ? 'customFilter' : 'valuesFilter';

this.resetTabModels();
toTab.syncWithFilter();

tabContainerModel.activateTab(toTabId);
}

private setColumnFilters(filters) {
this.gridFilterModel.setColumnFilters(this.field, filters);
}

private doCommitOnChange(tab) {
if (!this.commitOnChange) return;
if (this.tabContainerModel.activeTabId !== tab) return;
this.commit(false);
}

private resetTabModels() {
this.customTabModel.reset();
this.valuesTabModel?.reset();
}
}
Loading

0 comments on commit 24c7fbd

Please sign in to comment.