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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
* `Store.isStore`
* `View.isView`
* `Filter.isFilter`
* Replaced `LeftRightChooserFilter.anyMatch` with `matchMode`. Changes are not expected to be
required as apps typically do not create this component directly.

### 🐞 Bug Fixes

Expand All @@ -32,6 +34,8 @@
hamburger menu. Set to `true` to render the current user's initials instead or provide a function
to render a custom element for the user.
* Added `AggregationContext` as an additional argument to `CubeField.canAggregateFn`.
* Added `filterMatchMode` option to `ColChooserModel`, allowing customizing match to `start`,
`startWord`, or `any`.

### ⚙️ Typescript API Adjustments

Expand Down
2 changes: 1 addition & 1 deletion cmp/grid/GridModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export interface GridConfig {
filterModel?: GridFilterModelConfig | boolean;

/** Config with which to create a ColChooserModel, or boolean `true` to enable default.*/
colChooserModel?: ColChooserConfig | boolean;
colChooserModel?: Omit<ColChooserConfig, 'gridModel'> | boolean;

/**
* Function to be called when the user triggers GridModel.restoreDefaultsAsync(). This
Expand Down
19 changes: 15 additions & 4 deletions cmp/grid/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
* Copyright © 2026 Extremely Heavy Industries Inc.
*/

import {GridFilterFieldSpecConfig} from '@xh/hoist/cmp/grid/filter/GridFilterFieldSpec';
import {HSide, PersistOptions, Some} from '@xh/hoist/core';
import {FilterBindTarget, FilterValueSource, Store, StoreRecord} from '@xh/hoist/data';

import type {HSide, PersistOptions, Some} from '@xh/hoist/core';
import type {
FilterBindTarget,
FilterMatchMode,
FilterValueSource,
Store,
StoreRecord
} from '@xh/hoist/data';
import type {
CellClassParams,
CustomCellEditorProps,
Expand All @@ -23,6 +27,7 @@ import type {
import type {ReactElement, ReactNode} from 'react';
import type {Column, ColumnSpec} from './columns/Column';
import type {ColumnGroup, ColumnGroupSpec} from './columns/ColumnGroup';
import type {GridFilterFieldSpecConfig} from './filter/GridFilterFieldSpec';
import type {GridModel} from './GridModel';

export interface ColumnState {
Expand Down Expand Up @@ -124,6 +129,9 @@ export interface GridFilterBindTarget extends FilterBindTarget, FilterValueSourc
export type GroupRowRenderer = (context: ICellRendererParams) => ReactNode;

export interface ColChooserConfig {
/** GridModel to bind to. Not required if creating via `GridModel.colChooserModel` */
gridModel?: GridModel;

/**
* Immediately render changed columns on grid (default true).
* Set to false to enable Save button for committing changes on save. Desktop only.
Expand All @@ -147,6 +155,9 @@ export interface ColChooserConfig {

/** Chooser height for popover and dialog. Desktop only. */
height?: string | number;

/** Mode to use when filtering (default 'startWord'). Desktop only. */
filterMatchMode?: FilterMatchMode;
}

export type ColumnOrGroup = Column | ColumnGroup;
Expand Down
6 changes: 3 additions & 3 deletions cmp/store/StoreFilterField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/
import {GridModel} from '@xh/hoist/cmp/grid';
import type {GridModel} from '@xh/hoist/cmp/grid';
import {DefaultHoistProps, hoistCmp, HoistModel, useLocalModel, XH} from '@xh/hoist/core';
import {FilterTestFn, Store} from '@xh/hoist/data';
import type {FilterMatchMode, FilterTestFn, Store} from '@xh/hoist/data';
import {storeFilterFieldImpl as desktopStoreFilterFieldImpl} from '@xh/hoist/dynamics/desktop';
import {storeFilterFieldImpl as mobileStoreFilterFieldImpl} from '@xh/hoist/dynamics/mobile';
import {StoreFilterFieldImplModel} from './impl/StoreFilterFieldImplModel';
Expand Down Expand Up @@ -53,7 +53,7 @@ export interface StoreFilterFieldProps extends DefaultHoistProps {
includeFields?: string[];

/** Mode to use when filtering (default 'startWord'). */
matchMode?: 'start' | 'startWord' | 'any';
matchMode?: FilterMatchMode;

/** Optional model for raw value binding - see comments on the `bind` prop for details. */
model?: HoistModel;
Expand Down
58 changes: 32 additions & 26 deletions cmp/store/impl/StoreFilterFieldImplModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
*
* Copyright © 2026 Extremely Heavy Industries Inc.
*/
import {HoistModel, XH, lookup} from '@xh/hoist/core';
import {GridModel} from '@xh/hoist/cmp/grid';
import {HoistModel, lookup, XH} from '@xh/hoist/core';
import type {FilterMatchMode, StoreRecord} from '@xh/hoist/data';
import {CompoundFilter, FilterLike, Store, withFilterByKey} from '@xh/hoist/data';
import {action, makeObservable, comparer} from '@xh/hoist/mobx';
import {action, comparer, makeObservable} from '@xh/hoist/mobx';
import {stripTags, throwIf, warnIf, withDefault} from '@xh/hoist/utils/js';
import {
debounce,
Expand All @@ -33,8 +34,12 @@ export class StoreFilterFieldImplModel extends HoistModel {
gridModel: GridModel;
store: Store;

filter;
bufferedApplyFilter;
private filter: (rec: StoreRecord) => boolean;
private bufferedApplyFilter;

get matchMode(): FilterMatchMode {
return this.componentProps.matchMode ?? 'startWord';
}

constructor() {
super();
Expand All @@ -59,29 +64,30 @@ export class StoreFilterFieldImplModel extends HoistModel {

this.bufferedApplyFilter = debounce(() => this.applyFilter(), filterBuffer);

this.addReaction({
track: () => [this.filterText, gridModel?.columns, gridModel?.groupBy],
run: () => this.regenerateFilter(),
fireImmediately: true
});

this.addReaction({
track: () => [this.componentProps.includeFields, this.componentProps.excludeFields],
run: () => this.regenerateFilter(),
equals: comparer.structural
});
this.addReaction(
{
track: () => [this.filterText, gridModel?.columns, gridModel?.groupBy],
run: () => this.regenerateFilter(),
fireImmediately: true
},
{
track: () => [this.componentProps.includeFields, this.componentProps.excludeFields],
run: () => this.regenerateFilter(),
equals: comparer.structural
}
);
}

//------------------------------------------------------------------
// Trampoline value to bindable -- from bound model, or store
//------------------------------------------------------------------
get filterText() {
get filterText(): string {
const {bind, model} = this.componentProps;
return bind ? model[bind] : this.store.xhFilterText;
}

@action
setFilterText(v) {
setFilterText(v: string) {
const {bind, model} = this.componentProps;
if (bind) {
model.setBindable(bind, v);
Expand Down Expand Up @@ -122,7 +128,7 @@ export class StoreFilterFieldImplModel extends HoistModel {
if (filterText && !isEmpty(activeFields)) {
const regex = this.getRegex(filterText),
valGetters = flatMap(activeFields, fieldPath => this.getValGetters(fieldPath));
newFilter = rec => valGetters.some(fn => regex.test(fn(rec)));
newFilter = (rec: StoreRecord) => valGetters.some(fn => regex.test(fn(rec)));
}

if (filter === newFilter) return;
Expand All @@ -140,9 +146,9 @@ export class StoreFilterFieldImplModel extends HoistModel {
}
}

getRegex(searchTerm) {
getRegex(searchTerm: string): RegExp {
searchTerm = escapeRegExp(searchTerm);
switch (this.componentProps.matchMode ?? 'startWord') {
switch (this.matchMode) {
case 'any':
return new RegExp(searchTerm, 'i');
case 'start':
Expand All @@ -153,11 +159,11 @@ export class StoreFilterFieldImplModel extends HoistModel {
throw XH.exception('Unknown matchMode in StoreFilterField');
}

getActiveFields() {
getActiveFields(): string[] {
const {gridModel, store, componentProps} = this,
{includeFields, excludeFields} = componentProps;

let ret = store ? ['id', ...store.fields.map(f => f.name)] : [];
let ret = store ? ['id', ...store.fieldNames] : [];
if (includeFields) ret = store ? intersection(ret, includeFields) : includeFields;
if (excludeFields) ret = without(ret, ...excludeFields);

Expand Down Expand Up @@ -196,7 +202,7 @@ export class StoreFilterFieldImplModel extends HoistModel {
return ret;
}

getValGetters(fieldName) {
getValGetters(fieldName: string) {
const {gridModel} = this;

// If a GridModel has been configured, the user is looking at rendered values in a grid and
Expand All @@ -219,7 +225,7 @@ export class StoreFilterFieldImplModel extends HoistModel {

return cols.map(column => {
const {renderer, getValueFn} = column;
return record => {
return (record: StoreRecord) => {
const ctx = {
record,
field: field.name,
Expand All @@ -239,7 +245,7 @@ export class StoreFilterFieldImplModel extends HoistModel {
// Otherwise just match raw.
// Use expensive get() only when needed to support dot-separated paths.
return fieldName.includes('.')
? rec => get(rec.data, fieldName)
: rec => rec.data[fieldName];
? (rec: StoreRecord) => get(rec.data, fieldName)
: (rec: StoreRecord) => rec.data[fieldName];
}
}
8 changes: 8 additions & 0 deletions data/filter/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@ export interface FilterValueSource {
export function isFilterValueSource(v: unknown): v is FilterValueSource {
return (v as any)?.isFilterValueSource === true;
}

/**
* Option to customize matching behavior for {@link StoreFilterField} and related components.
* - `start`: match beginning of candidate strings only.
* - `startWord`: match beginning of words within candidate strings.
* - `any`: match anywhere within candidate strings.
*/
export type FilterMatchMode = 'start' | 'startWord' | 'any';
5 changes: 3 additions & 2 deletions desktop/cmp/grid/find/GridFindField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
* Copyright © 2026 Extremely Heavy Industries Inc.
*/
import composeRefs from '@seznam/compose-react-refs/composeRefs';
import {GridModel} from '@xh/hoist/cmp/grid';
import type {GridModel} from '@xh/hoist/cmp/grid';
import {hbox, span, vbox} from '@xh/hoist/cmp/layout';
import {hoistCmp, LayoutProps, useLocalModel} from '@xh/hoist/core';
import type {FilterMatchMode} from '@xh/hoist/data';
import {button} from '@xh/hoist/desktop/cmp/button';
import {textInput, TextInputProps} from '@xh/hoist/desktop/cmp/input';
import '@xh/hoist/desktop/register';
Expand All @@ -25,7 +26,7 @@ export interface GridFindFieldProps extends TextInputProps, LayoutProps {
gridModel?: GridModel;

/** Mode to use when searching (default 'startWord'). */
matchMode?: 'start' | 'startWord' | 'any';
matchMode?: FilterMatchMode;

/**
* Delay (in ms) to buffer searching the grid after the value changes from user input.
Expand Down
Loading
Loading