From adee7902ee12a9f3c391a5c659f5d08df0904e95 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 15 Jan 2026 10:46:09 -0800 Subject: [PATCH 1/7] WIP --- cmp/grid/GridModel.ts | 53 +++++++++++++++--------------- cmp/grid/Types.ts | 24 +++++++------- cmp/grid/filter/GridFilterModel.ts | 8 +++-- data/Store.ts | 2 +- data/cube/Cube.ts | 5 +++ 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/cmp/grid/GridModel.ts b/cmp/grid/GridModel.ts index eaa8d8e45..c7c005c12 100644 --- a/cmp/grid/GridModel.ts +++ b/cmp/grid/GridModel.ts @@ -1167,7 +1167,7 @@ export class GridModel extends HoistModel { } @action - setColumns(colConfigs: Array) { + setColumns(colConfigs: GridConfig['columns']) { this.validateColConfigs(colConfigs); colConfigs = this.enhanceColConfigsFromStore(colConfigs); @@ -1686,7 +1686,7 @@ export class GridModel extends HoistModel { // so it can be better re-used across Hoist APIs such as `Filter` and `FormModel`. However for // convenience, a `GridModel.store` config can also be very minimal (or non-existent), and // in this case GridModel should work out the required Store fields from column definitions. - private parseAndSetColumnsAndStore(colConfigs, store = {}) { + private parseAndSetColumnsAndStore(colConfigs: GridConfig['columns'], store: GridConfig['store'] = {}) { // 1) Validate configs. this.validateStoreConfig(store); this.validateColConfigs(colConfigs); @@ -1711,14 +1711,14 @@ export class GridModel extends HoistModel { this.store = newStore; } - private validateStoreConfig(store) { + private validateStoreConfig(store: GridConfig['store']) { throwIf( !(store instanceof Store || isPlainObject(store)), 'GridModel.store config must be either an instance of a Store or a config to create one.' ); } - private validateColConfigs(colConfigs) { + private validateColConfigs(colConfigs: GridConfig['columns']) { throwIf(!isArray(colConfigs), 'GridModel.columns config must be an array.'); throwIf( colConfigs.some(c => !isPlainObject(c)), @@ -1782,7 +1782,8 @@ export class GridModel extends HoistModel { // Selectively enhance raw column configs with field-level metadata from store.fields and/or // field config partials provided by the column configs themselves. - private enhanceColConfigsFromStore(colConfigs, storeOrConfig?) { + // TODO - add types here and resolve errors + private enhanceColConfigsFromStore(colConfigs, storeOrConfig?): GridConfig['columns'] { const store = storeOrConfig || this.store, storeFields = store?.fields, fieldsByName = {}; @@ -1886,31 +1887,33 @@ export class GridModel extends HoistModel { return sizingMode; } - private parseSelModel(selModel): StoreSelectionModel { - const {store} = this; - selModel = withDefault(selModel, XH.isMobileApp ? 'disabled' : 'single'); - + private parseSelModel(selModel: GridConfig['selModel']): StoreSelectionModel { + // Return actual instance directly. if (selModel instanceof StoreSelectionModel) { return selModel; } - if (isPlainObject(selModel)) { - return this.markManaged(new StoreSelectionModel({...selModel, store, xhImpl: true})); + // Default unspecified based on platform, treat explicit null as disabled. + if (selModel === undefined) { + selModel = XH.isMobileApp ? 'disabled' : 'single'; + } else if (selModel === null) { + selModel = 'disabled'; } - // Assume its just the mode... - let mode: any = 'single'; + // Strings specify the mode. if (isString(selModel)) { - mode = selModel; - } else if (selModel === null) { - mode = 'disabled'; + selModel = {mode: selModel}; } - return this.markManaged(new StoreSelectionModel({mode, store, xhImpl: true})); + + return this.markManaged( + new StoreSelectionModel({...selModel, store: this.store, xhImpl: true}) + ); } - private parseFilterModel(filterModel) { + private parseFilterModel(filterModel: GridConfig['filterModel']) { if (XH.isMobileApp || !filterModel) return null; - filterModel = isPlainObject(filterModel) ? filterModel : {}; + + filterModel = filterModel === true ? {} : filterModel; return new GridFilterModel({bind: this.store, ...filterModel}, this); } @@ -1921,14 +1924,12 @@ export class GridModel extends HoistModel { }; } - private parseChooserModel(chooserModel): HoistModel { - const modelClass = XH.isMobileApp ? MobileColChooserModel : DesktopColChooserModel; + private parseChooserModel(chooserModel: GridConfig['colChooserModel']): HoistModel { + if (!chooserModel) return null; - if (isPlainObject(chooserModel)) { - return this.markManaged(new modelClass({...chooserModel, gridModel: this})); - } - - return chooserModel ? this.markManaged(new modelClass({gridModel: this})) : null; + const modelClass = XH.isMobileApp ? MobileColChooserModel : DesktopColChooserModel; + chooserModel = chooserModel === true ? {} : chooserModel; + return this.markManaged(new modelClass({...chooserModel, gridModel: this})); } private isGroupSpec(col: ColumnGroupSpec | ColumnSpec): col is ColumnGroupSpec { diff --git a/cmp/grid/Types.ts b/cmp/grid/Types.ts index 8b7c21ac8..13ac8b253 100644 --- a/cmp/grid/Types.ts +++ b/cmp/grid/Types.ts @@ -7,23 +7,23 @@ import {GridFilterFieldSpecConfig} from '@xh/hoist/cmp/grid/filter/GridFilterFieldSpec'; import {HSide, PersistOptions, Some} from '@xh/hoist/core'; -import {Store, StoreRecord, View} from '@xh/hoist/data'; -import {ReactElement, ReactNode} from 'react'; -import {Column} from './columns/Column'; -import {ColumnGroup} from './columns/ColumnGroup'; -import {GridModel} from './GridModel'; +import {FilterBindTarget, FilterValueSource, Store, StoreRecord} from '@xh/hoist/data'; import type { CellClassParams, + CustomCellEditorProps, HeaderClassParams, HeaderValueGetterParams, ICellRendererParams, IRowNode, ITooltipParams, RowClassParams, - ValueSetterParams, - CustomCellEditorProps + ValueSetterParams } from '@xh/hoist/kit/ag-grid'; +import {ReactElement, ReactNode} from 'react'; +import {Column} from './columns/Column'; +import {ColumnGroup} from './columns/ColumnGroup'; +import {GridModel} from './GridModel'; export interface ColumnState { colId: string; @@ -87,10 +87,10 @@ export interface GridModelPersistOptions extends PersistOptions { export interface GridFilterModelConfig { /** - * Store / Cube View to be filtered as column filters are applied. Defaulted to the + * Target ( filtered as column filters are applied. Defaulted to the * gridModel's store. */ - bind?: Store | View; + bind?: GridFilterBindTarget; /** * True to update filters immediately after each change made in the column-based filter UI. @@ -100,8 +100,8 @@ export interface GridFilterModelConfig { /** * Specifies the fields this model supports for filtering. Should be configs for - * {@link GridFilterFieldSpec}, string names to match with Fields in bound Store/View, or omitted - * entirely to indicate that all fields should be filter-enabled. + * {@link GridFilterFieldSpec}, string names to match with Fields in bound Store/View, or + * omitted entirely to indicate that all fields should be filter-enabled. */ fieldSpecs?: Array; @@ -109,6 +109,8 @@ export interface GridFilterModelConfig { fieldSpecDefaults?: Omit; } +export type GridFilterBindTarget = FilterBindTarget & FilterValueSource; + /** * Renderer for a group row * @param context - The group renderer params from ag-Grid diff --git a/cmp/grid/filter/GridFilterModel.ts b/cmp/grid/filter/GridFilterModel.ts index 68c2c8096..dfde853af 100644 --- a/cmp/grid/filter/GridFilterModel.ts +++ b/cmp/grid/filter/GridFilterModel.ts @@ -5,7 +5,11 @@ * Copyright © 2026 Extremely Heavy Industries Inc. */ -import {GridFilterFieldSpec, GridFilterModelConfig} from '@xh/hoist/cmp/grid'; +import { + GridFilterFieldSpec, + GridFilterFieldSpecConfig, + GridFilterModelConfig +} from '@xh/hoist/cmp/grid'; import {HoistModel, managed} from '@xh/hoist/core'; import { CompoundFilter, @@ -144,7 +148,7 @@ export class GridFilterModel extends HoistModel { //-------------------------------- // Implementation //-------------------------------- - private parseFieldSpecs(specs, fieldSpecDefaults) { + private parseFieldSpecs(specs: Array, fieldSpecDefaults: Omit) { const {bind} = this; // If no specs provided, include all source fields. diff --git a/data/Store.ts b/data/Store.ts index 2f91fa9de..1413ab5f3 100644 --- a/data/Store.ts +++ b/data/Store.ts @@ -46,7 +46,7 @@ import { import {instanceManager} from '../core/impl/InstanceManager'; import {RecordSet} from './impl/RecordSet'; -export interface StoreConfig { +export interface sStoreConfig { /** Field names, configs, or instances. */ fields?: Array; diff --git a/data/cube/Cube.ts b/data/cube/Cube.ts index 38c20f09c..9fc238c92 100755 --- a/data/cube/Cube.ts +++ b/data/cube/Cube.ts @@ -6,6 +6,7 @@ */ import {HoistBase, managed, PlainObject, Some} from '@xh/hoist/core'; +import {Field} from '@xh/hoist/data'; import {action, makeObservable, observable} from '@xh/hoist/mobx'; import {forEachAsync} from '@xh/hoist/utils/async'; import {CubeField, CubeFieldSpec} from './CubeField'; @@ -160,6 +161,10 @@ export class Cube extends HoistBase { return this._connectedViews.size; } + getField(name: string): CubeField { + return this.store.getField(name) as CubeField; + } + //------------------ // Querying API //----------------- From 7704be6350cd5a746203edfbd60817f025f73e7e Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Thu, 15 Jan 2026 12:51:43 -0800 Subject: [PATCH 2/7] Complete planned changes --- cmp/grid/GridModel.ts | 83 +++++++++++++++++++----------- cmp/grid/Types.ts | 22 +++++--- cmp/grid/columns/ColumnGroup.ts | 10 ++-- cmp/grid/filter/GridFilterModel.ts | 12 +++-- cmp/grid/impl/Utils.ts | 8 ++- cmp/tab/TabContainerModel.ts | 4 +- data/Store.ts | 2 +- data/cube/Cube.ts | 15 +++--- 8 files changed, 91 insertions(+), 65 deletions(-) diff --git a/cmp/grid/GridModel.ts b/cmp/grid/GridModel.ts index c7c005c12..aad6d382c 100644 --- a/cmp/grid/GridModel.ts +++ b/cmp/grid/GridModel.ts @@ -10,10 +10,13 @@ import { ColumnCellClassRuleFn, ColumnGroup, ColumnGroupSpec, + ColumnOrGroup, + ColumnOrGroupSpec, ColumnSpec, GridAutosizeMode, GridFilterModelConfig, GridGroupSortFn, + isColumnSpec, TreeStyle } from '@xh/hoist/cmp/grid'; import {GridFilterModel} from '@xh/hoist/cmp/grid/filter/GridFilterModel'; @@ -32,6 +35,7 @@ import { XH } from '@xh/hoist/core'; import { + Field, FieldSpec, Store, StoreConfig, @@ -117,7 +121,7 @@ import { export interface GridConfig { /** Columns for this grid. */ - columns?: Array; + columns?: ColumnOrGroupSpec[]; /** Column configs to be set on all columns. Merges deeply. */ colDefaults?: Partial; @@ -454,7 +458,7 @@ export class GridModel extends HoistModel { //------------------------ // Observable API //------------------------ - @observable.ref columns: Array = []; + @observable.ref columns: ColumnOrGroup[] = []; @observable.ref columnState: ColumnState[] = []; @observable.ref expandState: any = {}; @observable.ref sortBy: GridSorter[] = []; @@ -1167,7 +1171,7 @@ export class GridModel extends HoistModel { } @action - setColumns(colConfigs: GridConfig['columns']) { + setColumns(colConfigs: ColumnOrGroupSpec[]) { this.validateColConfigs(colConfigs); colConfigs = this.enhanceColConfigsFromStore(colConfigs); @@ -1362,7 +1366,7 @@ export class GridModel extends HoistModel { } /** Return matching leaf-level Column object from the provided collection. */ - findColumn(cols: Array, colId: string): Column { + findColumn(cols: ColumnOrGroup[], colId: string): Column { for (let col of cols) { if (col instanceof ColumnGroup) { const ret = this.findColumn(col.children, colId); @@ -1375,7 +1379,7 @@ export class GridModel extends HoistModel { } /** Return matching ColumnGroup from the provided collection. */ - findColumnGroup(cols: Array, groupId: string): ColumnGroup { + findColumnGroup(cols: ColumnOrGroup[], groupId: string): ColumnGroup { for (let col of cols) { if (col instanceof ColumnGroup) { if (col.groupId === groupId) return col; @@ -1595,7 +1599,7 @@ export class GridModel extends HoistModel { //----------------------- // Implementation //----------------------- - private buildColumn(config: ColumnGroupSpec | ColumnSpec, borderedGroup?: ColumnGroupSpec) { + private buildColumn(config: ColumnOrGroupSpec, borderedGroup?: ColumnGroupSpec): ColumnOrGroup { // Merge leaf config with defaults. // Ensure *any* tooltip setting on column itself always wins. if (this.colDefaults && !this.isGroupSpec(config)) { @@ -1609,9 +1613,7 @@ export class GridModel extends HoistModel { if (this.isGroupSpec(config)) { if (config.borders !== false) borderedGroup = config; - const children = compact( - config.children.map(c => this.buildColumn(c, borderedGroup)) - ) as Array; + const children = compact(config.children.map(c => this.buildColumn(c, borderedGroup))); return !isEmpty(children) ? new ColumnGroup(config as ColumnGroupSpec, this, children) : null; @@ -1649,19 +1651,23 @@ export class GridModel extends HoistModel { } } - private gatherLeaves(columns, leaves = []) { + private gatherLeaves(columns: ColumnOrGroup[], leaves: Column[] = []): Column[] { columns.forEach(col => { - if (col.groupId) this.gatherLeaves(col.children, leaves); - if (col.colId) leaves.push(col); + if (col instanceof ColumnGroup) { + this.gatherLeaves(col.children, leaves); + } else { + leaves.push(col); + } }); return leaves; } - private collectIds(cols, ids = []) { + private collectIds(cols: ColumnOrGroup[], ids: string[] = []) { cols.forEach(col => { - if (col.colId) ids.push(col.colId); - if (col.groupId) { + if (col instanceof Column) { + ids.push(col.colId); + } else { ids.push(col.groupId); this.collectIds(col.children, ids); } @@ -1686,7 +1692,10 @@ export class GridModel extends HoistModel { // so it can be better re-used across Hoist APIs such as `Filter` and `FormModel`. However for // convenience, a `GridModel.store` config can also be very minimal (or non-existent), and // in this case GridModel should work out the required Store fields from column definitions. - private parseAndSetColumnsAndStore(colConfigs: GridConfig['columns'], store: GridConfig['store'] = {}) { + private parseAndSetColumnsAndStore( + colConfigs: ColumnOrGroupSpec[], + store: Store | StoreConfig = {} + ) { // 1) Validate configs. this.validateStoreConfig(store); this.validateColConfigs(colConfigs); @@ -1711,14 +1720,14 @@ export class GridModel extends HoistModel { this.store = newStore; } - private validateStoreConfig(store: GridConfig['store']) { + private validateStoreConfig(store: Store | StoreConfig) { throwIf( !(store instanceof Store || isPlainObject(store)), 'GridModel.store config must be either an instance of a Store or a config to create one.' ); } - private validateColConfigs(colConfigs: GridConfig['columns']) { + private validateColConfigs(colConfigs: ColumnOrGroupSpec[]) { throwIf(!isArray(colConfigs), 'GridModel.columns config must be an array.'); throwIf( colConfigs.some(c => !isPlainObject(c)), @@ -1726,7 +1735,7 @@ export class GridModel extends HoistModel { ); } - private validateColumns(cols) { + private validateColumns(cols: ColumnOrGroup[]) { if (isEmpty(cols)) return; const ids = this.collectIds(cols); @@ -1782,17 +1791,30 @@ export class GridModel extends HoistModel { // Selectively enhance raw column configs with field-level metadata from store.fields and/or // field config partials provided by the column configs themselves. - // TODO - add types here and resolve errors - private enhanceColConfigsFromStore(colConfigs, storeOrConfig?): GridConfig['columns'] { + private enhanceColConfigsFromStore( + colConfigs: ColumnOrGroupSpec[], + storeOrConfig?: Store | StoreConfig + ): ColumnOrGroupSpec[] { const store = storeOrConfig || this.store, storeFields = store?.fields, - fieldsByName = {}; + fieldsByName: Record = {}; // Extract field definitions in all supported forms: pull Field instances/configs from - // storeFields first, then fill in with any col-level `field` config objects. - storeFields?.forEach(sf => (fieldsByName[sf.name] = sf)); + // storeFields first... + storeFields?.forEach(sf => { + if (sf && !isString(sf)) { + fieldsByName[sf.name] = sf; + } + }); + + // Then fill in with any col-level `field` config objects. colConfigs.forEach(cc => { - if (isPlainObject(cc.field) && !fieldsByName[cc.field.name]) { + if ( + isColumnSpec(cc) && + cc.field && + !isString(cc.field) && + !fieldsByName[cc.field.name] + ) { fieldsByName[cc.field.name] = cc.field; } }); @@ -1801,16 +1823,17 @@ export class GridModel extends HoistModel { const numTypes = ['int', 'number'], dateTypes = ['date', 'localDate']; + return colConfigs.map(col => { // Recurse into children for column groups - if (col.children) { + if (!isColumnSpec(col)) { return { ...col, children: this.enhanceColConfigsFromStore(col.children, storeOrConfig) }; } - const colFieldName = isPlainObject(col.field) ? col.field.name : col.field, + const colFieldName = isString(col.field) ? col.field : col.field?.name, field = fieldsByName[colFieldName]; if (!field) return col; @@ -1838,9 +1861,9 @@ export class GridModel extends HoistModel { // Ensure store config has a complete set of fields for all configured columns. Note this // requires columns to have been constructed and set, and will only work with a raw store // config object, not an instance. - private enhanceStoreConfigFromColumns(storeConfig) { + private enhanceStoreConfigFromColumns(storeConfig: StoreConfig) { const fields = storeConfig.fields ?? [], - storeFieldNames = fields.map(it => it.name ?? it), + storeFieldNames = fields.map(it => (isString(it) ? it : it.name)), leafColsByFieldName = this.leafColsByFieldName(); const newFields: FieldSpec[] = []; @@ -1932,7 +1955,7 @@ export class GridModel extends HoistModel { return this.markManaged(new modelClass({...chooserModel, gridModel: this})); } - private isGroupSpec(col: ColumnGroupSpec | ColumnSpec): col is ColumnGroupSpec { + private isGroupSpec(col: ColumnOrGroupSpec): col is ColumnGroupSpec { return 'children' in col; } diff --git a/cmp/grid/Types.ts b/cmp/grid/Types.ts index 13ac8b253..98e1bdb32 100644 --- a/cmp/grid/Types.ts +++ b/cmp/grid/Types.ts @@ -20,10 +20,10 @@ import type { RowClassParams, ValueSetterParams } from '@xh/hoist/kit/ag-grid'; -import {ReactElement, ReactNode} from 'react'; -import {Column} from './columns/Column'; -import {ColumnGroup} from './columns/ColumnGroup'; -import {GridModel} from './GridModel'; +import type {ReactElement, ReactNode} from 'react'; +import type {Column, ColumnSpec} from './columns/Column'; +import type {ColumnGroup, ColumnGroupSpec} from './columns/ColumnGroup'; +import type {GridModel} from './GridModel'; export interface ColumnState { colId: string; @@ -87,8 +87,9 @@ export interface GridModelPersistOptions extends PersistOptions { export interface GridFilterModelConfig { /** - * Target ( filtered as column filters are applied. Defaulted to the - * gridModel's store. + * Target (typically a {@link Store} or Cube {@link View}) to be filtered as column filters + * are applied and used as a source for unique values displayed in the filtering UI when + * applicable. Defaulted to the gridModel's store. */ bind?: GridFilterBindTarget; @@ -144,6 +145,13 @@ export interface ColChooserConfig { height?: string | number; } +export type ColumnOrGroup = Column | ColumnGroup; +export type ColumnOrGroupSpec = ColumnSpec | ColumnGroupSpec; + +export function isColumnSpec(spec: ColumnOrGroupSpec): spec is ColumnSpec { + return !(spec as ColumnGroupSpec).children; +} + /** * Sort comparator function for a grid column. Note that this comparator will also be called if * agGrid-provided column filtering is enabled: it is used to sort values shown for set filter @@ -250,7 +258,7 @@ export type ColumnTooltipFn = ( * @returns CSS class(es) to use. */ export type ColumnHeaderClassFn = (context: { - column: Column | ColumnGroup; + column: ColumnOrGroup; gridModel: GridModel; agParams: HeaderClassParams; }) => Some; diff --git a/cmp/grid/columns/ColumnGroup.ts b/cmp/grid/columns/ColumnGroup.ts index d62311f62..2300a1d68 100644 --- a/cmp/grid/columns/ColumnGroup.ts +++ b/cmp/grid/columns/ColumnGroup.ts @@ -13,7 +13,7 @@ import {throwIf, withDefault} from '@xh/hoist/utils/js'; import {clone, isEmpty, isFunction, isString, keysIn} from 'lodash'; import {ReactNode} from 'react'; import {GridModel} from '../GridModel'; -import {ColumnHeaderClassFn, ColumnHeaderNameFn} from '../Types'; +import {ColumnHeaderClassFn, ColumnHeaderNameFn, ColumnOrGroup} from '../Types'; import {Column, ColumnSpec} from './Column'; export interface ColumnGroupSpec { @@ -51,7 +51,7 @@ export interface ColumnGroupSpec { * Provided to GridModels as plain configuration objects. */ export class ColumnGroup { - readonly children: Array; + readonly children: ColumnOrGroup[]; readonly gridModel: GridModel; readonly groupId: string; readonly headerName: ReactNode | ColumnHeaderNameFn; @@ -79,11 +79,7 @@ export class ColumnGroup { * * @internal */ - constructor( - config: ColumnGroupSpec, - gridModel: GridModel, - children: Array - ) { + constructor(config: ColumnGroupSpec, gridModel: GridModel, children: ColumnOrGroup[]) { const { children: childrenSpecs, groupId, diff --git a/cmp/grid/filter/GridFilterModel.ts b/cmp/grid/filter/GridFilterModel.ts index dfde853af..b80df4070 100644 --- a/cmp/grid/filter/GridFilterModel.ts +++ b/cmp/grid/filter/GridFilterModel.ts @@ -6,6 +6,7 @@ */ import { + GridFilterBindTarget, GridFilterFieldSpec, GridFilterFieldSpecConfig, GridFilterModelConfig @@ -17,8 +18,6 @@ import { Filter, FilterLike, flattenFilter, - Store, - View, withFilterByField, withFilterByTypes } from '@xh/hoist/data'; @@ -35,7 +34,7 @@ export class GridFilterModel extends HoistModel { override xhImpl = true; gridModel: GridModel; - bind: Store | View; + bind: GridFilterBindTarget; @bindable commitOnChange: boolean; @managed fieldSpecs: GridFilterFieldSpec[] = []; @@ -70,7 +69,7 @@ export class GridFilterModel extends HoistModel { setColumnFilters(field: string, filter: FilterLike) { // If current bound filter is a CompoundFilter for a single column, wrap it // in an 'AND' CompoundFilter so new columns get 'ANDed' alongside it. - let currFilter = this.filter as any; + let currFilter: FilterLike = this.filter; if (currFilter instanceof CompoundFilter && currFilter.field) { currFilter = {filters: [currFilter], op: 'AND'}; } @@ -148,7 +147,10 @@ export class GridFilterModel extends HoistModel { //-------------------------------- // Implementation //-------------------------------- - private parseFieldSpecs(specs: Array, fieldSpecDefaults: Omit) { + private parseFieldSpecs( + specs: Array, + fieldSpecDefaults: Omit + ) { const {bind} = this; // If no specs provided, include all source fields. diff --git a/cmp/grid/impl/Utils.ts b/cmp/grid/impl/Utils.ts index 0a4fbddd3..9585223e2 100644 --- a/cmp/grid/impl/Utils.ts +++ b/cmp/grid/impl/Utils.ts @@ -4,8 +4,8 @@ * * Copyright © 2026 Extremely Heavy Industries Inc. */ -import {Column, ColumnGroup, ColumnRenderer, GroupRowRenderer} from '@xh/hoist/cmp/grid'; -import {HeaderClassParams} from '@xh/hoist/kit/ag-grid'; +import {Column, ColumnOrGroup, ColumnRenderer, GroupRowRenderer} from '@xh/hoist/cmp/grid'; +import type {HeaderClassParams} from '@xh/hoist/kit/ag-grid'; import {logWarn} from '@xh/hoist/utils/js'; import {castArray, isFunction} from 'lodash'; @@ -31,9 +31,7 @@ export function managedRenderer( * * @internal */ -export function getAgHeaderClassFn( - column: Column | ColumnGroup -): (params: HeaderClassParams) => string[] { +export function getAgHeaderClassFn(column: ColumnOrGroup): (params: HeaderClassParams) => string[] { const {headerClass, headerAlign, gridModel} = column; return agParams => { diff --git a/cmp/tab/TabContainerModel.ts b/cmp/tab/TabContainerModel.ts index c6f56931a..d3706e0f0 100644 --- a/cmp/tab/TabContainerModel.ts +++ b/cmp/tab/TabContainerModel.ts @@ -123,7 +123,7 @@ export class TabContainerModel extends HoistModel { /** * @param config - TabContainer configuration. - * @param [depth] - Depth in hierarchy of nested TabContainerModels. Not for application use. + * @param depth - Depth in hierarchy of nested TabContainerModels. Not for application use. */ constructor( { @@ -138,7 +138,7 @@ export class TabContainerModel extends HoistModel { xhImpl = false, switcher = {mode: 'static'} }: TabContainerConfig, - depth = 0 + depth: number = 0 ) { super(); makeObservable(this); diff --git a/data/Store.ts b/data/Store.ts index 1413ab5f3..2f91fa9de 100644 --- a/data/Store.ts +++ b/data/Store.ts @@ -46,7 +46,7 @@ import { import {instanceManager} from '../core/impl/InstanceManager'; import {RecordSet} from './impl/RecordSet'; -export interface sStoreConfig { +export interface StoreConfig { /** Field names, configs, or instances. */ fields?: Array; diff --git a/data/cube/Cube.ts b/data/cube/Cube.ts index 9fc238c92..4a3aceeca 100755 --- a/data/cube/Cube.ts +++ b/data/cube/Cube.ts @@ -6,20 +6,19 @@ */ import {HoistBase, managed, PlainObject, Some} from '@xh/hoist/core'; -import {Field} from '@xh/hoist/data'; import {action, makeObservable, observable} from '@xh/hoist/mobx'; import {forEachAsync} from '@xh/hoist/utils/async'; -import {CubeField, CubeFieldSpec} from './CubeField'; -import {ViewRowData} from './ViewRowData'; -import {Query, QueryConfig} from './Query'; -import {View} from './View'; +import {defaultsDeep, isEmpty} from 'lodash'; import {Store, StoreRecordIdSpec, StoreTransaction} from '../Store'; import {StoreRecord} from '../StoreRecord'; +import {BucketSpec} from './BucketSpec'; +import {CubeField, CubeFieldSpec} from './CubeField'; +import {Query, QueryConfig} from './Query'; import {AggregateRow} from './row/AggregateRow'; -import {BucketRow} from './row/BucketRow'; import {BaseRow} from './row/BaseRow'; -import {BucketSpec} from './BucketSpec'; -import {defaultsDeep, isEmpty} from 'lodash'; +import {BucketRow} from './row/BucketRow'; +import {View} from './View'; +import {ViewRowData} from './ViewRowData'; export interface CubeConfig { fields: CubeField[] | CubeFieldSpec[]; From a877ce75ae59e5e83dd00b608eee398cca6e4e71 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Fri, 16 Jan 2026 12:24:32 -0800 Subject: [PATCH 3/7] Remove GridModel's shape-based validation methods - Rely on TS coverage for this concern --- cmp/grid/GridModel.ts | 47 ++++++++++++------------------------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/cmp/grid/GridModel.ts b/cmp/grid/GridModel.ts index aad6d382c..f8359fece 100644 --- a/cmp/grid/GridModel.ts +++ b/cmp/grid/GridModel.ts @@ -91,7 +91,6 @@ import { isEmpty, isFunction, isNil, - isPlainObject, isString, isUndefined, keysIn, @@ -1172,7 +1171,6 @@ export class GridModel extends HoistModel { @action setColumns(colConfigs: ColumnOrGroupSpec[]) { - this.validateColConfigs(colConfigs); colConfigs = this.enhanceColConfigsFromStore(colConfigs); const columns = compact(colConfigs.map(c => this.buildColumn(c))); @@ -1694,45 +1692,26 @@ export class GridModel extends HoistModel { // in this case GridModel should work out the required Store fields from column definitions. private parseAndSetColumnsAndStore( colConfigs: ColumnOrGroupSpec[], - store: Store | StoreConfig = {} + storeOrConfig: Store | StoreConfig = {} ) { - // 1) Validate configs. - this.validateStoreConfig(store); - this.validateColConfigs(colConfigs); + // Enhance colConfigs with field-level metadata provided by store, if any. + colConfigs = this.enhanceColConfigsFromStore(colConfigs, storeOrConfig); - // 2) Enhance colConfigs with field-level metadata provided by store, if any. - colConfigs = this.enhanceColConfigsFromStore(colConfigs, store); - - // 3) Create and set columns with (possibly) enhanced configs. + // Create and set columns with (possibly) enhanced configs. this.setColumns(colConfigs); - let newStore: Store; - // 4) Create store if needed - if (isPlainObject(store)) { - store = this.enhanceStoreConfigFromColumns(store); - newStore = new Store({loadTreeData: this.treeMode, ...store}); - newStore.xhImpl = this.xhImpl; - this.markManaged(newStore); + // Set or create Store as needed. + let store: Store; + if (storeOrConfig instanceof Store) { + store = storeOrConfig; } else { - newStore = store as Store; + storeOrConfig = this.enhanceStoreConfigFromColumns(storeOrConfig); + store = new Store({loadTreeData: this.treeMode, ...storeOrConfig}); + store.xhImpl = this.xhImpl; + this.markManaged(store); } - this.store = newStore; - } - - private validateStoreConfig(store: Store | StoreConfig) { - throwIf( - !(store instanceof Store || isPlainObject(store)), - 'GridModel.store config must be either an instance of a Store or a config to create one.' - ); - } - - private validateColConfigs(colConfigs: ColumnOrGroupSpec[]) { - throwIf(!isArray(colConfigs), 'GridModel.columns config must be an array.'); - throwIf( - colConfigs.some(c => !isPlainObject(c)), - 'GridModel.columns config only accepts plain objects for Column or ColumnGroup configs.' - ); + this.store = store; } private validateColumns(cols: ColumnOrGroup[]) { From b2b8078a84cc41b7eaed203e38387e322be5dc9f Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Fri, 16 Jan 2026 13:27:28 -0800 Subject: [PATCH 4/7] Remove unnecessary type assertion --- cmp/grid/GridModel.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmp/grid/GridModel.ts b/cmp/grid/GridModel.ts index f8359fece..dc0995507 100644 --- a/cmp/grid/GridModel.ts +++ b/cmp/grid/GridModel.ts @@ -25,6 +25,7 @@ import { Awaitable, HoistModel, HSide, + LoadSpec, managed, PlainObject, SizingMode, @@ -1149,7 +1150,7 @@ export class GridModel extends HoistModel { this.sortBy = newSorters; } - override async doLoadAsync(loadSpec) { + override async doLoadAsync(loadSpec: LoadSpec) { // Delegate to any store that has load support return (this.store as any).loadSupport?.loadAsync(loadSpec); } @@ -1612,9 +1613,7 @@ export class GridModel extends HoistModel { if (this.isGroupSpec(config)) { if (config.borders !== false) borderedGroup = config; const children = compact(config.children.map(c => this.buildColumn(c, borderedGroup))); - return !isEmpty(children) - ? new ColumnGroup(config as ColumnGroupSpec, this, children) - : null; + return !isEmpty(children) ? new ColumnGroup(config, this, children) : null; } if (borderedGroup) { From efecc0a785adc0b8d872267dc1365b0cae552efe Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Fri, 16 Jan 2026 13:30:52 -0800 Subject: [PATCH 5/7] Use `extends` for `GridFilterBindTarget` --- cmp/grid/Types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmp/grid/Types.ts b/cmp/grid/Types.ts index 98e1bdb32..7e054be2d 100644 --- a/cmp/grid/Types.ts +++ b/cmp/grid/Types.ts @@ -110,7 +110,11 @@ export interface GridFilterModelConfig { fieldSpecDefaults?: Omit; } -export type GridFilterBindTarget = FilterBindTarget & FilterValueSource; +/** + * {@link GridFilterModel} currently accepts a single `bind` target that also provides available + * values. Note that both `Store` and `View` satisfy this intersection. + */ +export interface GridFilterBindTarget extends FilterBindTarget, FilterValueSource {} /** * Renderer for a group row From 97136e5efbc9f782fdcd082d3fea958cf0b57893 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Fri, 16 Jan 2026 13:41:10 -0800 Subject: [PATCH 6/7] Update `isColumnSpec` typeguard as per CR suggestion --- cmp/grid/Types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmp/grid/Types.ts b/cmp/grid/Types.ts index 7e054be2d..d058a4af5 100644 --- a/cmp/grid/Types.ts +++ b/cmp/grid/Types.ts @@ -153,7 +153,7 @@ export type ColumnOrGroup = Column | ColumnGroup; export type ColumnOrGroupSpec = ColumnSpec | ColumnGroupSpec; export function isColumnSpec(spec: ColumnOrGroupSpec): spec is ColumnSpec { - return !(spec as ColumnGroupSpec).children; + return !('children' in spec); } /** From b31aa0c3d4cba910c0b091f9e1c3355f5b84add8 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Fri, 16 Jan 2026 13:46:40 -0800 Subject: [PATCH 7/7] Add `getFieldName` helper function --- cmp/grid/GridModel.ts | 5 +++-- data/Field.ts | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmp/grid/GridModel.ts b/cmp/grid/GridModel.ts index dc0995507..8946e8add 100644 --- a/cmp/grid/GridModel.ts +++ b/cmp/grid/GridModel.ts @@ -38,6 +38,7 @@ import { import { Field, FieldSpec, + getFieldName, Store, StoreConfig, StoreRecord, @@ -1811,7 +1812,7 @@ export class GridModel extends HoistModel { }; } - const colFieldName = isString(col.field) ? col.field : col.field?.name, + const colFieldName = getFieldName(col.field), field = fieldsByName[colFieldName]; if (!field) return col; @@ -1841,7 +1842,7 @@ export class GridModel extends HoistModel { // config object, not an instance. private enhanceStoreConfigFromColumns(storeConfig: StoreConfig) { const fields = storeConfig.fields ?? [], - storeFieldNames = fields.map(it => (isString(it) ? it : it.name)), + storeFieldNames = fields.map(it => getFieldName(it)), leafColsByFieldName = this.leafColsByFieldName(); const newFields: FieldSpec[] = []; diff --git a/data/Field.ts b/data/Field.ts index e9cfad2e7..dbd6637b5 100644 --- a/data/Field.ts +++ b/data/Field.ts @@ -181,3 +181,8 @@ export function genDisplayName(fieldName: string): string { // Handle common cases of "id" -> "ID" and "foo_id" -> "Foo ID" (vs "Foo Id") return startCase(fieldName).replace(/(^| )Id\b/g, '$1ID'); } + +/** Convenience function to return the name of a field from one of several common inputs. */ +export function getFieldName(field: string | Field | FieldSpec): string { + return field ? (isString(field) ? field : field.name) : null; +}