@xh/hoist 80.0.0-SNAPSHOT.1767974090726 → 80.0.0-SNAPSHOT.1767982629403

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/build/types/cmp/filter/FilterChooserFieldSpec.d.ts +1 -1
  3. package/build/types/cmp/filter/FilterChooserModel.d.ts +19 -10
  4. package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +4 -3
  5. package/build/types/cmp/grid/filter/GridFilterModel.d.ts +1 -1
  6. package/build/types/data/Store.d.ts +5 -6
  7. package/build/types/data/cube/View.d.ts +5 -3
  8. package/build/types/data/filter/BaseFilterFieldSpec.d.ts +3 -3
  9. package/build/types/data/filter/CompoundFilter.d.ts +1 -1
  10. package/build/types/data/filter/FieldFilter.d.ts +1 -1
  11. package/build/types/data/filter/Filter.d.ts +3 -3
  12. package/build/types/data/filter/FunctionFilter.d.ts +1 -1
  13. package/build/types/data/filter/Types.d.ts +39 -13
  14. package/build/types/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.d.ts +2 -2
  15. package/build/types/svc/InspectorService.d.ts +2 -2
  16. package/cmp/filter/FilterChooserFieldSpec.ts +5 -22
  17. package/cmp/filter/FilterChooserModel.ts +27 -12
  18. package/cmp/grid/filter/GridFilterFieldSpec.ts +52 -51
  19. package/cmp/grid/filter/GridFilterModel.ts +6 -7
  20. package/data/Store.ts +47 -13
  21. package/data/cube/View.ts +14 -3
  22. package/data/filter/BaseFilterFieldSpec.ts +3 -3
  23. package/data/filter/CompoundFilter.ts +3 -3
  24. package/data/filter/FieldFilter.ts +2 -2
  25. package/data/filter/Filter.ts +4 -4
  26. package/data/filter/FunctionFilter.ts +2 -2
  27. package/data/filter/Types.ts +53 -20
  28. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +9 -9
  29. package/package.json +1 -1
  30. package/svc/InspectorService.ts +6 -5
  31. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@
10
10
  * Renamed `AppContainerModel.loadModel` to `loadObserver`. This is primarily an internal model,
11
11
  so there is no deprecated alias. Any app usages should swap to `XH.appLoadObserver`.
12
12
  * Removed additional references to deprecated `loadModel` within Hoist itself.
13
+ * Removed the following instance getters - use new static typeguards instead:
14
+ * `Store.isStore`
15
+ * `View.isView`
16
+ * `Filter.isFilter`
17
+
18
+ ### ⚙️ Typescript API Adjustments
19
+
20
+ * Introduced new `FilterBindTarget` and `FilterValueSource` interfaces to generalize the data
21
+ sources that could be used with `FilterChooserModel` and `GridFilterModel`. Both `Store` and
22
+ `View` implement these interfaces, meaning no changes are required for apps, but it is now
23
+ possible to use these models with other, alternate implementations if needed.
24
+ * Added new static typeguard methods on `Store`, `View`, and `Filter` + subclasses.
13
25
 
14
26
  ## 79.0.0 - 2026-01-05
15
27
 
@@ -31,7 +31,7 @@ export declare class FilterChooserFieldSpec extends BaseFilterFieldSpec {
31
31
  renderValue(value: any, op: FieldFilterOperator): string;
32
32
  parseValue(value: any, op: FieldFilterOperator): any;
33
33
  parseExample(example: any): any;
34
- parseValueParser(valueParser: any): any;
34
+ parseValueParser(valueParser: FilterChooserValueParser): FilterChooserValueParser;
35
35
  loadValuesFromSource(): void;
36
36
  }
37
37
  type FilterChooserValueRenderer = (value: any, op: FieldFilterOperator) => ReactNode;
@@ -1,5 +1,5 @@
1
1
  import { HoistModel, PersistOptions, TaskObserver, Thunkable } from '@xh/hoist/core';
2
- import { CompoundFilter, FieldFilter, Filter, Store, View } from '@xh/hoist/data';
2
+ import { CompoundFilter, FieldFilter, Filter, FilterBindTarget, FilterValueSource } from '@xh/hoist/data';
3
3
  import { CompoundFilterSpec, FieldFilterSpec, FilterLike } from '@xh/hoist/data/filter/Types';
4
4
  import { ReactNode } from 'react';
5
5
  import { FilterChooserFieldSpec, FilterChooserFieldSpecConfig } from './FilterChooserFieldSpec';
@@ -15,16 +15,25 @@ export interface FilterChooserConfig {
15
15
  /** Default properties to be assigned to all FilterChooserFieldSpecs created by this model. */
16
16
  fieldSpecDefaults?: Partial<FilterChooserFieldSpecConfig>;
17
17
  /**
18
- * Store or cube View that should actually be filtered as this model's value changes.
19
- * This may be the same as `valueSource`. Leave undefined if you wish to combine this model's values
20
- * with other filters, send it to the server, or otherwise observe and handle value changes manually.
18
+ * Target (typically a {@link Store} or Cube {@link View}) to which this model's filter should
19
+ * be automatically applied as it changes.
20
+ *
21
+ * Note this binding is bi-directional - the target's filter will also be *set onto* this model
22
+ * if it changes on the target, to support e.g. sync'd filtering between a FilterChooser and
23
+ * Grid filtering bound to the same target Store.
24
+ *
25
+ * Leave undefined if you wish to combine this model's values with other filters, send it to
26
+ * the server, or otherwise observe and handle value changes manually.
21
27
  */
22
- bind?: Store | View;
28
+ bind?: FilterBindTarget;
23
29
  /**
24
- * Store or cube View to be used to lookup matching Field-level defaults for `fieldSpecs` and to
25
- * provide suggested data values (if so configured) from user input. Defaults to `bind` if provided.
30
+ * Source (typically a {@link Store} or Cube {@link View}) from which this model can lookup
31
+ * matching Field-level defaults for `fieldSpecs` and provide suggested data values (if so
32
+ * configured) from user input.
33
+ *
34
+ * Defaults to {@link bind} if the a bind target is provided and is a valid source.
26
35
  */
27
- valueSource?: Store | View;
36
+ valueSource?: FilterValueSource;
28
37
  /**
29
38
  * Configuration for a filter appropriate to be rendered and managed by FilterChooser, or a function
30
39
  * to produce the same. Note that FilterChooser currently can only edit and create a flat collection
@@ -62,8 +71,8 @@ export interface FilterChooserConfig {
62
71
  export declare class FilterChooserModel extends HoistModel {
63
72
  value: FilterChooserFilter;
64
73
  favorites: FilterChooserFilter[];
65
- bind: Store | View;
66
- valueSource: Store | View;
74
+ bind: FilterBindTarget;
75
+ valueSource: FilterValueSource;
67
76
  fieldSpecs: FilterChooserFieldSpec[];
68
77
  suggestFieldsWhenEmpty: boolean;
69
78
  sortFieldSuggestions: boolean;
@@ -22,17 +22,18 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
22
22
  }
23
23
  /**
24
24
  * Apps should NOT instantiate this class directly.
25
- * Instead, provide a config for this object to the GridModel's `filterModel` config.
25
+ * Instead, provide a config for this object via {@link GridConfig.filterModel} config.
26
26
  */
27
27
  export declare class GridFilterFieldSpec extends BaseFilterFieldSpec {
28
28
  filterModel: GridFilterModel;
29
29
  renderer: ColumnRenderer;
30
30
  inputProps: PlainObject;
31
31
  defaultOp: FieldFilterOperator;
32
- valueCount: number;
32
+ /** Total number of unique values for this field in the source, regardless of other filters. */
33
+ allValuesCount: number;
33
34
  constructor({ filterModel, renderer, inputProps, defaultOp, ...rest }: GridFilterFieldSpecConfig);
34
35
  getUniqueValue(value: unknown): unknown;
35
36
  loadValuesFromSource(): void;
36
37
  private cleanFilter;
37
- private valueFromRecord;
38
+ private toDisplayValue;
38
39
  }
@@ -38,7 +38,7 @@ export declare class GridFilterModel extends HoistModel {
38
38
  fromDisplayValue(value: any): any;
39
39
  openDialog(): void;
40
40
  closeDialog(): void;
41
- setFilter(filter: any): void;
41
+ setFilter(filter: Filter): void;
42
42
  private parseFieldSpecs;
43
43
  private getOuterCompoundFilter;
44
44
  }
@@ -1,10 +1,7 @@
1
1
  import { HoistBase, PlainObject, Some } from '@xh/hoist/core';
2
- import { Field, FieldSpec } from './Field';
2
+ import { Field, FieldSpec, Filter, FilterBindTarget, FilterLike, FilterValueSource, StoreRecord, StoreRecordId, StoreRecordOrId } from '@xh/hoist/data';
3
3
  import { RecordSet } from './impl/RecordSet';
4
4
  import { StoreErrorMap, StoreValidator } from './impl/StoreValidator';
5
- import { StoreRecord, StoreRecordId, StoreRecordOrId } from './StoreRecord';
6
- import { Filter } from './filter/Filter';
7
- import { FilterLike } from './filter/Types';
8
5
  export interface StoreConfig {
9
6
  /** Field names, configs, or instances. */
10
7
  fields?: Array<string | FieldSpec | Field>;
@@ -127,8 +124,9 @@ export type StoreRecordIdSpec = string | ((data: PlainObject) => StoreRecordId);
127
124
  /**
128
125
  * A managed and observable set of local, in-memory Records.
129
126
  */
130
- export declare class Store extends HoistBase {
131
- get isStore(): boolean;
127
+ export declare class Store extends HoistBase implements FilterBindTarget, FilterValueSource {
128
+ static isStore(obj: unknown): obj is Store;
129
+ readonly isFilterValueSource = true;
132
130
  fields: Field[];
133
131
  idSpec: (data: PlainObject) => StoreRecordId;
134
132
  processRawData: (raw: any) => any;
@@ -323,6 +321,7 @@ export declare class Store extends HoistBase {
323
321
  * false if the record is either not in the Store at all or not filtered out.
324
322
  */
325
323
  recordIsFiltered(recOrId: StoreRecordOrId): boolean;
324
+ getValuesForFieldFilter(fieldName: string, filter?: Filter): any[];
326
325
  /**
327
326
  * Set whether the root should be loaded as summary data in loadData().
328
327
  */
@@ -1,5 +1,5 @@
1
1
  import { HoistBase, PlainObject, Some } from '@xh/hoist/core';
2
- import { Cube, CubeField, Filter, FilterLike, Query, QueryConfig, Store, StoreChangeLog, StoreRecordId } from '@xh/hoist/data';
2
+ import { Cube, CubeField, Filter, FilterBindTarget, FilterLike, FilterValueSource, Query, QueryConfig, Store, StoreChangeLog, StoreRecordId } from '@xh/hoist/data';
3
3
  import { ViewRowData } from '@xh/hoist/data/cube/ViewRowData';
4
4
  import { AggregationContext } from './aggregate/AggregationContext';
5
5
  import { BaseRow } from './row/BaseRow';
@@ -33,8 +33,9 @@ export interface DimensionValue {
33
33
  * Primary interface for consuming grouped and aggregated data from the cube.
34
34
  * Applications should create via the {@link Cube.createView} factory.
35
35
  */
36
- export declare class View extends HoistBase {
37
- get isView(): boolean;
36
+ export declare class View extends HoistBase implements FilterBindTarget, FilterValueSource {
37
+ static isView(obj: unknown): obj is View;
38
+ readonly isFilterValueSource = true;
38
39
  /** Query defining this View. Update via {@link updateQuery}. */
39
40
  query: Query;
40
41
  /**
@@ -81,6 +82,7 @@ export declare class View extends HoistBase {
81
82
  setFilter(filter: FilterLike): void;
82
83
  noteCubeLoaded(): void;
83
84
  noteCubeUpdated(changeLog: StoreChangeLog): void;
85
+ getValuesForFieldFilter(fieldName: string, filter?: Filter): any[];
84
86
  private fullUpdate;
85
87
  private dataOnlyUpdate;
86
88
  private loadStores;
@@ -1,5 +1,5 @@
1
1
  import { HoistBase } from '@xh/hoist/core';
2
- import { Field, Store, FieldType, View } from '@xh/hoist/data';
2
+ import { Field, FieldType, FilterValueSource } from '@xh/hoist/data';
3
3
  import { FieldFilterOperator } from './Types';
4
4
  export interface BaseFilterFieldSpecConfig {
5
5
  /** Identifying field name to filter on. */
@@ -11,7 +11,7 @@ export interface BaseFilterFieldSpecConfig {
11
11
  /** Operators available for filtering, will default to a supported set based on type.*/
12
12
  ops?: FieldFilterOperator[];
13
13
  /** Used to source matching data `Field` and extract values if configured. */
14
- source?: Store | View;
14
+ source?: FilterValueSource;
15
15
  /**
16
16
  * True to provide interfaces and auto-complete options
17
17
  * with enumerated matches for creating '=' or '!=' filters. Defaults to true for
@@ -38,7 +38,7 @@ export declare abstract class BaseFilterFieldSpec extends HoistBase {
38
38
  fieldType: FieldType;
39
39
  displayName: string;
40
40
  ops: FieldFilterOperator[];
41
- source: Store | View;
41
+ source: FilterValueSource;
42
42
  enableValues: boolean;
43
43
  forceSelection: boolean;
44
44
  values: any[];
@@ -6,7 +6,7 @@ import { CompoundFilterSpec, CompoundFilterOperator, FilterTestFn } from './Type
6
6
  * Immutable.
7
7
  */
8
8
  export declare class CompoundFilter extends Filter {
9
- get isCompoundFilter(): boolean;
9
+ static isCompoundFilter(obj: unknown): obj is CompoundFilter;
10
10
  readonly filters: Filter[];
11
11
  readonly op: CompoundFilterOperator;
12
12
  /** @returns the singular field this filter operates on, if consistent across all clauses. */
@@ -11,7 +11,7 @@ import { FieldFilterOperator, FieldFilterSpec, FilterTestFn } from './Types';
11
11
  * Immutable.
12
12
  */
13
13
  export declare class FieldFilter extends Filter {
14
- get isFieldFilter(): boolean;
14
+ static isFieldFilter(obj: unknown): obj is FieldFilter;
15
15
  readonly field: string;
16
16
  readonly op: FieldFilterOperator;
17
17
  readonly value: any;
@@ -1,5 +1,5 @@
1
- import { Store } from '../Store';
2
- import { FilterSpec, FilterTestFn } from './Types';
1
+ import { Store } from '@xh/hoist/data';
2
+ import type { FilterSpec, FilterTestFn } from './Types';
3
3
  /**
4
4
  * Base class for Hoist data package Filters.
5
5
  *
@@ -12,7 +12,7 @@ import { FilterSpec, FilterTestFn } from './Types';
12
12
  * via an `AND` or `OR` operator.
13
13
  */
14
14
  export declare abstract class Filter {
15
- get isFilter(): boolean;
15
+ static isFilter(obj: unknown): obj is Filter;
16
16
  /**
17
17
  * @returns a function that can be used to test a record or object.
18
18
  * @param store - if provided, return will be appropriate for testing records of this store.
@@ -8,7 +8,7 @@ import { FunctionFilterSpec, FilterTestFn } from './Types';
8
8
  * Immutable.
9
9
  */
10
10
  export declare class FunctionFilter extends Filter {
11
- get isFunctionFilter(): boolean;
11
+ static isFunctionFilter(obj: unknown): obj is FunctionFilter;
12
12
  readonly key: string;
13
13
  readonly testFn: FilterTestFn;
14
14
  /**
@@ -1,14 +1,8 @@
1
- import { PlainObject } from '@xh/hoist/core';
2
- import { Filter } from './Filter';
3
- import { StoreRecord, Field, FieldType } from '../';
4
- export type CompoundFilterOperator = 'AND' | 'OR' | 'and' | 'or';
5
- export interface CompoundFilterSpec {
6
- /** Collection of Filters or configs to create. */
7
- filters: FilterLike[];
8
- /** logical operator 'AND' (default) or 'OR'. */
9
- op?: CompoundFilterOperator;
10
- }
11
- export type FieldFilterOperator = '=' | '!=' | '>' | '>=' | '<' | '<=' | 'like' | 'not like' | 'begins' | 'not begins' | 'ends' | 'not ends' | 'includes' | 'excludes';
1
+ import type { PlainObject } from '@xh/hoist/core';
2
+ import type { Filter } from './Filter';
3
+ import type { StoreRecord, Field, FieldType } from '../';
4
+ export type FilterLike = Filter | FilterSpec | FilterTestFn | FilterLike[];
5
+ export type FilterSpec = FieldFilterSpec | FunctionFilterSpec | CompoundFilterSpec;
12
6
  export interface FieldFilterSpec {
13
7
  /** Name of Field to filter or Field instance. */
14
8
  field: string | Field;
@@ -18,6 +12,14 @@ export interface FieldFilterSpec {
18
12
  /** For internal serialization only. */
19
13
  valueType?: FieldType;
20
14
  }
15
+ export type FieldFilterOperator = '=' | '!=' | '>' | '>=' | '<' | '<=' | 'like' | 'not like' | 'begins' | 'not begins' | 'ends' | 'not ends' | 'includes' | 'excludes';
16
+ export interface CompoundFilterSpec {
17
+ /** Collection of Filters or configs to create. */
18
+ filters: FilterLike[];
19
+ /** logical operator 'AND' (default) or 'OR'. */
20
+ op?: CompoundFilterOperator;
21
+ }
22
+ export type CompoundFilterOperator = 'AND' | 'OR' | 'and' | 'or';
21
23
  export interface FunctionFilterSpec {
22
24
  /** Key used to identify this FunctionFilter.*/
23
25
  key: string;
@@ -30,5 +32,29 @@ export interface FunctionFilterSpec {
30
32
  * @returns true if the candidate passes and should be included in filtered results.
31
33
  */
32
34
  export type FilterTestFn = (candidate: PlainObject | StoreRecord) => boolean;
33
- export type FilterSpec = FieldFilterSpec | FunctionFilterSpec | CompoundFilterSpec;
34
- export type FilterLike = Filter | FilterSpec | FilterTestFn | FilterLike[];
35
+ /**
36
+ * Target (typically a {@link Store} or Cube {@link View}) that can be used by filtering models
37
+ * such as {@link FilterChooserModel} and {@link GridFilterModel} to get and set filters.
38
+ */
39
+ export interface FilterBindTarget {
40
+ filter: Filter;
41
+ setFilter(filter: FilterLike): unknown;
42
+ }
43
+ /**
44
+ * Data provider (typically a {@link Store} or Cube {@link View}) that can be used by filtering
45
+ * models such as {@link FilterChooserModel} and {@link GridFilterModel} to source available
46
+ * values from within a dataset for display / suggestion to users.
47
+ */
48
+ export interface FilterValueSource {
49
+ /** Names of all fields available in the source. */
50
+ fieldNames: string[];
51
+ /** @returns the Field instance for the given field name. */
52
+ getField(fieldName: string): Field;
53
+ /** @returns unique values for the given field, applying the given filter if provided. */
54
+ getValuesForFieldFilter(fieldName: string, filter?: Filter): any[];
55
+ /** Observable timestamp for the source's data, to trigger consumers to re-query values. */
56
+ lastUpdated: number;
57
+ /** For the {@link isFilterValueSource} typeguard. */
58
+ isFilterValueSource: true;
59
+ }
60
+ export declare function isFilterValueSource(v: unknown): v is FilterValueSource;
@@ -1,6 +1,6 @@
1
1
  import { GridFilterModel, GridModel } from '@xh/hoist/cmp/grid';
2
2
  import { HoistModel } from '@xh/hoist/core';
3
- import { FieldFilterSpec } from '@xh/hoist/data';
3
+ import type { FieldFilterSpec } from '@xh/hoist/data';
4
4
  import { HeaderFilterModel } from '../HeaderFilterModel';
5
5
  export declare class ValuesTabModel extends HoistModel {
6
6
  xhImpl: boolean;
@@ -24,7 +24,7 @@ export declare class ValuesTabModel extends HoistModel {
24
24
  get columnFilters(): import("@xh/hoist/data").FieldFilter[];
25
25
  get gridFilterModel(): GridFilterModel;
26
26
  get values(): any[];
27
- get valueCount(): number;
27
+ get allValuesCount(): number;
28
28
  get hasHiddenValues(): boolean;
29
29
  get sortIcon(): any;
30
30
  constructor(headerFilterModel: HeaderFilterModel);
@@ -45,13 +45,13 @@ export declare class InspectorService extends HoistService {
45
45
  clearStats(): void;
46
46
  restoreDefaultsAsync(): Promise<void>;
47
47
  private sync;
48
- setActiveInstances(ai: any): void;
48
+ setActiveInstances(ai: InspectorInstanceData[]): void;
49
49
  _prevModelCount: number;
50
50
  get conf(): any;
51
51
  }
52
52
  interface InspectorInstanceData {
53
53
  className: string;
54
- created: Date;
54
+ created: number;
55
55
  isHoistModel: boolean;
56
56
  isHoistService: boolean;
57
57
  isStore: boolean;
@@ -4,17 +4,17 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {parseFieldValue} from '@xh/hoist/data';
7
8
  import {
8
9
  BaseFilterFieldSpec,
9
10
  BaseFilterFieldSpecConfig
10
11
  } from '@xh/hoist/data/filter/BaseFilterFieldSpec';
11
12
  import {FieldFilterOperator} from '@xh/hoist/data/filter/Types';
12
- import {parseFieldValue, View} from '@xh/hoist/data';
13
13
  import {fmtDate, parseNumber} from '@xh/hoist/format';
14
14
  import {stripTags, throwIf} from '@xh/hoist/utils/js';
15
- import {isFunction, isNil} from 'lodash';
16
- import {isValidElement, ReactNode} from 'react';
17
15
  import {renderToStaticMarkup} from '@xh/hoist/utils/react';
16
+ import {isFunction} from 'lodash';
17
+ import {isValidElement, ReactNode} from 'react';
18
18
 
19
19
  export interface FilterChooserFieldSpecConfig extends BaseFilterFieldSpecConfig {
20
20
  /**
@@ -120,7 +120,7 @@ export class FilterChooserFieldSpec extends BaseFilterFieldSpec {
120
120
  return 'value';
121
121
  }
122
122
 
123
- parseValueParser(valueParser) {
123
+ parseValueParser(valueParser: FilterChooserValueParser): FilterChooserValueParser {
124
124
  // Default numeric parser
125
125
  if (!valueParser && (this.fieldType === 'int' || this.fieldType === 'number')) {
126
126
  return input => parseNumber(input);
@@ -129,24 +129,7 @@ export class FilterChooserFieldSpec extends BaseFilterFieldSpec {
129
129
  }
130
130
 
131
131
  loadValuesFromSource() {
132
- const {field, source} = this,
133
- values = new Set();
134
-
135
- // Note use of unfiltered recordset here to source suggest values. This allows chooser to
136
- // suggest values from already-filtered fields that will expand the results when selected.
137
- const sourceStore = source instanceof View ? source.cube.store : source;
138
- sourceStore.allRecords.forEach(rec => {
139
- const val = rec.get(field);
140
- if (!isNil(val)) {
141
- if (sourceStore.getField(field).type === 'tags') {
142
- val.forEach(it => values.add(it));
143
- } else {
144
- values.add(val);
145
- }
146
- }
147
- });
148
-
149
- this.values = Array.from(values);
132
+ this.values = this.source.getValuesForFieldFilter(this.field);
150
133
  }
151
134
  }
152
135
 
@@ -18,9 +18,10 @@ import {
18
18
  CompoundFilter,
19
19
  FieldFilter,
20
20
  Filter,
21
+ FilterBindTarget,
22
+ FilterValueSource,
23
+ isFilterValueSource,
21
24
  parseFilter,
22
- Store,
23
- View,
24
25
  withFilterByTypes
25
26
  } from '@xh/hoist/data';
26
27
  import {CompoundFilterSpec, FieldFilterSpec, FilterLike} from '@xh/hoist/data/filter/Types';
@@ -66,17 +67,26 @@ export interface FilterChooserConfig {
66
67
  fieldSpecDefaults?: Partial<FilterChooserFieldSpecConfig>;
67
68
 
68
69
  /**
69
- * Store or cube View that should actually be filtered as this model's value changes.
70
- * This may be the same as `valueSource`. Leave undefined if you wish to combine this model's values
71
- * with other filters, send it to the server, or otherwise observe and handle value changes manually.
70
+ * Target (typically a {@link Store} or Cube {@link View}) to which this model's filter should
71
+ * be automatically applied as it changes.
72
+ *
73
+ * Note this binding is bi-directional - the target's filter will also be *set onto* this model
74
+ * if it changes on the target, to support e.g. sync'd filtering between a FilterChooser and
75
+ * Grid filtering bound to the same target Store.
76
+ *
77
+ * Leave undefined if you wish to combine this model's values with other filters, send it to
78
+ * the server, or otherwise observe and handle value changes manually.
72
79
  */
73
- bind?: Store | View;
80
+ bind?: FilterBindTarget;
74
81
 
75
82
  /**
76
- * Store or cube View to be used to lookup matching Field-level defaults for `fieldSpecs` and to
77
- * provide suggested data values (if so configured) from user input. Defaults to `bind` if provided.
83
+ * Source (typically a {@link Store} or Cube {@link View}) from which this model can lookup
84
+ * matching Field-level defaults for `fieldSpecs` and provide suggested data values (if so
85
+ * configured) from user input.
86
+ *
87
+ * Defaults to {@link bind} if the a bind target is provided and is a valid source.
78
88
  */
79
- valueSource?: Store | View;
89
+ valueSource?: FilterValueSource;
80
90
 
81
91
  /**
82
92
  * Configuration for a filter appropriate to be rendered and managed by FilterChooser, or a function
@@ -124,8 +134,8 @@ export interface FilterChooserConfig {
124
134
  export class FilterChooserModel extends HoistModel {
125
135
  @observable.ref value: FilterChooserFilter = null;
126
136
  @observable.ref favorites: FilterChooserFilter[] = [];
127
- bind: Store | View;
128
- valueSource: Store | View;
137
+ bind: FilterBindTarget;
138
+ valueSource: FilterValueSource;
129
139
 
130
140
  @managed fieldSpecs: FilterChooserFieldSpec[] = [];
131
141
 
@@ -155,7 +165,7 @@ export class FilterChooserModel extends HoistModel {
155
165
  fieldSpecs,
156
166
  fieldSpecDefaults,
157
167
  bind = null,
158
- valueSource = bind,
168
+ valueSource,
159
169
  initialValue = null,
160
170
  initialFavorites = [],
161
171
  suggestFieldsWhenEmpty = true,
@@ -169,7 +179,12 @@ export class FilterChooserModel extends HoistModel {
169
179
  makeObservable(this);
170
180
 
171
181
  this.bind = bind;
182
+
172
183
  this.valueSource = valueSource;
184
+ if (!this.valueSource && isFilterValueSource(bind)) {
185
+ this.valueSource = bind;
186
+ }
187
+
173
188
  this.fieldSpecs = this.parseFieldSpecs(fieldSpecs, fieldSpecDefaults);
174
189
  this.suggestFieldsWhenEmpty = !!suggestFieldsWhenEmpty;
175
190
  this.sortFieldSuggestions = sortFieldSuggestions;
@@ -7,12 +7,18 @@
7
7
  import {ColumnRenderer} from '@xh/hoist/cmp/grid';
8
8
  import {HoistInputProps} from '@xh/hoist/cmp/input';
9
9
  import {PlainObject} from '@xh/hoist/core';
10
- import {FieldFilterOperator, parseFilter, StoreRecord, View} from '@xh/hoist/data';
10
+ import {
11
+ CompoundFilter,
12
+ FieldFilter,
13
+ FieldFilterOperator,
14
+ Filter,
15
+ parseFilter
16
+ } from '@xh/hoist/data';
11
17
  import {
12
18
  BaseFilterFieldSpec,
13
19
  BaseFilterFieldSpecConfig
14
20
  } from '@xh/hoist/data/filter/BaseFilterFieldSpec';
15
- import {castArray, compact, flatten, isDate, isEmpty, uniqBy} from 'lodash';
21
+ import {castArray, compact, flatMap, isDate, isEmpty, uniqBy} from 'lodash';
16
22
  import {GridFilterModel} from './GridFilterModel';
17
23
 
18
24
  export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
@@ -37,14 +43,16 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
37
43
 
38
44
  /**
39
45
  * Apps should NOT instantiate this class directly.
40
- * Instead, provide a config for this object to the GridModel's `filterModel` config.
46
+ * Instead, provide a config for this object via {@link GridConfig.filterModel} config.
41
47
  */
42
48
  export class GridFilterFieldSpec extends BaseFilterFieldSpec {
43
49
  filterModel: GridFilterModel;
44
50
  renderer: ColumnRenderer;
45
51
  inputProps: PlainObject;
46
52
  defaultOp: FieldFilterOperator;
47
- valueCount: number;
53
+
54
+ /** Total number of unique values for this field in the source, regardless of other filters. */
55
+ allValuesCount: number;
48
56
 
49
57
  constructor({
50
58
  filterModel,
@@ -71,68 +79,61 @@ export class GridFilterFieldSpec extends BaseFilterFieldSpec {
71
79
  //------------------------
72
80
  loadValuesFromSource() {
73
81
  const {filterModel, field, source, sourceField} = this,
74
- columnFilters = filterModel.getColumnFilters(field),
75
- sourceStore = source instanceof View ? source.cube.store : source,
76
- allRecords = sourceStore.allRecords;
77
-
78
- // Apply external filters *not* pertaining to this field to the sourceStore
79
- // to get the filtered set of available values to offer as options.
80
- const cleanedFilter = this.cleanFilter(filterModel.filter);
81
- let filteredRecords = allRecords;
82
- if (cleanedFilter) {
83
- const testFn = parseFilter(cleanedFilter).getTestFn(sourceStore);
84
- filteredRecords = allRecords.filter(testFn);
85
- }
82
+ columnFilters = filterModel.getColumnFilters(field);
83
+
84
+ // All possible values for this field from our source.
85
+ const allSrcVals = source.getValuesForFieldFilter(field).map(it => this.toDisplayValue(it));
86
+
87
+ // Values from current column filter.
88
+ const colFilterVals = flatMap(columnFilters, filter => {
89
+ return castArray(filter.value).map(val => sourceField.parseVal(val));
90
+ }).map(it => this.toDisplayValue(it));
91
+
92
+ // Combine + unique - these are all values that *could* be shown in the filter UI.
93
+ const allValues = uniqBy([...allSrcVals, ...colFilterVals], this.getUniqueValue);
86
94
 
87
- // Get values from current column filter
88
- const filterValues = [];
89
- columnFilters.forEach(filter => {
90
- const newValues = castArray(filter.value).map(value => {
91
- value = sourceField.parseVal(value);
92
- return filterModel.toDisplayValue(value);
93
- });
94
- filterValues.push(...newValues);
95
- });
96
-
97
- // Combine unique values from record sets and column filters.
98
- const allValues = uniqBy(
99
- [...flatten(allRecords.map(rec => this.valueFromRecord(rec))), ...filterValues],
100
- this.getUniqueValue
101
- );
102
- let values;
103
- if (cleanedFilter) {
104
- values = uniqBy(
105
- [
106
- ...flatten(filteredRecords.map(rec => this.valueFromRecord(rec))),
107
- ...filterValues
108
- ],
109
- this.getUniqueValue
110
- );
95
+ // Create a filter with other filters *not* pertaining to this field, if any.
96
+ // This filter will be used to get the subset of all possible values for this field that
97
+ // exist in records passing those other filters.
98
+ const otherFieldsFilter = this.cleanFilter(filterModel.filter);
99
+
100
+ let values: any[];
101
+
102
+ if (otherFieldsFilter) {
103
+ // If we have filters on other fields, get values from source that pass that filter.
104
+ // These will be the set of values shown in the filter UI.
105
+ const filteredSrcVals = source
106
+ .getValuesForFieldFilter(field, otherFieldsFilter)
107
+ .map(it => this.toDisplayValue(it));
108
+ values = uniqBy([...filteredSrcVals, ...colFilterVals], this.getUniqueValue);
111
109
  } else {
110
+ // Otherwise, all possible values are shown.
112
111
  values = allValues;
113
112
  }
114
113
 
115
114
  this.values = values.sort();
116
- this.valueCount = allValues.length;
115
+
116
+ // Note count of all possible values for this field - allows ValuesTabModel
117
+ // to indicate to the user if some values are hidden due to other active filters.
118
+ this.allValuesCount = allValues.length;
117
119
  }
118
120
 
119
121
  // Recursively modify a Filter|CompoundFilter to remove all FieldFilters referencing this column
120
- private cleanFilter(filter) {
121
- if (!filter) return filter;
122
-
123
- const {field, filters, op} = filter;
124
- if (filters) {
122
+ private cleanFilter(filter: Filter): Filter {
123
+ if (CompoundFilter.isCompoundFilter(filter)) {
124
+ const {filters, op} = filter;
125
125
  const ret = compact(filters.map(it => this.cleanFilter(it)));
126
- return !isEmpty(ret) ? {op, filters: ret} : null;
127
- } else if (field === this.field) {
126
+ return !isEmpty(ret) ? parseFilter({op, filters: ret}) : null;
127
+ }
128
+
129
+ if (FieldFilter.isFieldFilter(filter) && filter.field === this.field) {
128
130
  return null;
129
131
  }
130
132
 
131
133
  return filter;
132
134
  }
133
135
 
134
- private valueFromRecord(record: StoreRecord) {
135
- const {filterModel, field} = this;
136
- return filterModel.toDisplayValue(record.get(field));
136
+ private toDisplayValue(val: any): any {
137
+ return this.filterModel.toDisplayValue(val);
137
138
  }
138
139
  }