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

Make Grid column filter UI lazy #3873

Merged
merged 3 commits into from
Dec 27, 2024
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
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
Loading