@xh/hoist 86.1.0 → 86.2.0

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 (47) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/admin/AdminJsonDisplay.ts +31 -0
  3. package/admin/App.scss +3 -11
  4. package/admin/jsonsearch/impl/JsonSearchImplModel.ts +4 -4
  5. package/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel.ts +3 -11
  6. package/admin/tabs/cluster/instances/services/DetailsPanel.ts +2 -13
  7. package/admin/tabs/cluster/objects/DetailPanel.ts +3 -12
  8. package/build/types/admin/AdminJsonDisplay.d.ts +7 -0
  9. package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +2 -2
  10. package/build/types/cmp/grid/Grid.d.ts +6 -3
  11. package/build/types/cmp/grid/GridModel.d.ts +11 -0
  12. package/build/types/cmp/grid/filter/GridFilterFieldSpec.d.ts +24 -6
  13. package/build/types/desktop/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  14. package/build/types/desktop/cmp/input/CodeInput.d.ts +7 -0
  15. package/build/types/desktop/cmp/input/NumberInput.d.ts +1 -1
  16. package/build/types/desktop/cmp/input/impl/CalcWindowedMenuWidth.d.ts +20 -0
  17. package/build/types/desktop/cmp/rest/impl/RestFormModel.d.ts +1 -1
  18. package/build/types/format/FormatNumber.d.ts +11 -3
  19. package/build/types/icon/Icon.d.ts +4 -3
  20. package/build/types/mobile/cmp/button/grid/ExpandToLevelButton.d.ts +2 -1
  21. package/build/types/mobile/cmp/input/NumberInput.d.ts +1 -1
  22. package/cmp/grid/Grid.ts +17 -0
  23. package/cmp/grid/GridModel.ts +17 -5
  24. package/cmp/grid/filter/GridFilterFieldSpec.ts +28 -5
  25. package/cmp/grid/impl/MenuSupport.ts +2 -4
  26. package/desktop/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  27. package/desktop/cmp/grid/editors/NumberEditor.ts +16 -17
  28. package/desktop/cmp/grid/editors/SelectEditor.ts +15 -10
  29. package/desktop/cmp/grid/impl/filter/GridFilterDialog.ts +2 -2
  30. package/desktop/cmp/grid/impl/filter/headerfilter/values/ValuesTabModel.ts +46 -7
  31. package/desktop/cmp/input/CheckboxButton.ts +21 -1
  32. package/desktop/cmp/input/CodeInput.ts +20 -3
  33. package/desktop/cmp/input/NumberInput.ts +1 -1
  34. package/desktop/cmp/input/Select.ts +13 -2
  35. package/desktop/cmp/input/impl/CalcWindowedMenuWidth.ts +106 -0
  36. package/desktop/cmp/rest/impl/RestFormField.ts +1 -1
  37. package/desktop/cmp/tab/Tabs.scss +72 -25
  38. package/format/FormatNumber.ts +69 -32
  39. package/icon/Icon.scss +13 -0
  40. package/icon/Icon.ts +4 -3
  41. package/icon/impl/IconHtml.ts +1 -1
  42. package/mcp/data/ts-registry.ts +1 -1
  43. package/mobile/cmp/button/grid/ExpandToLevelButton.ts +4 -5
  44. package/mobile/cmp/input/CheckboxButton.ts +18 -1
  45. package/mobile/cmp/input/NumberInput.ts +1 -1
  46. package/package.json +6 -6
  47. package/styles/vars.scss +15 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Changelog
2
2
 
3
+ ## 86.2.0 - 2026-06-25
4
+
5
+ ### 🎁 New Features
6
+
7
+ * `CodeInput` / `JsonInput` now auto-format content for display via their configured `formatter`,
8
+ controlled by a new `autoFormat` prop. On editable inputs, content is tidied automatically on blur
9
+ (never mid-edit) - users get clean, consistently-formatted JSON without reaching for the format
10
+ button. For `readonly` inputs it defaults to true, so applications can bind directly to raw source
11
+ values and drop their pre-formatting logic, simplifying call sites substantially.
12
+ * Grid column filter specs now support a `sortValue` config, letting the Values tab of the filter
13
+ dialog sort its entries the same way the underlying grid column sorts them. When not provided,
14
+ the column's own `sortValue` is used.
15
+ * `GridModel.levelLabels` now accepts a partial array covering only the top levels of a tree or
16
+ grouped grid. The "Expand to..." menu and `ExpandToLevelButton` offer one entry per labelled
17
+ level, so deeper, unlabelled levels (e.g. system-managed) are no longer required and are omitted
18
+ as expand-to targets - previously a too-short array disabled the feature entirely.
19
+ * Added a `lossless` option to `fmtQuantity` to compact values to millions / billions units only
20
+ when doing so loses no precision, rendering the full value otherwise (e.g. `7,100,100` stays
21
+ `7,100,100` rather than collapsing to `7.10m`).
22
+
23
+ ### 🐞 Bug Fixes
24
+
25
+ * Fixed grid `NumberEditor` to allow starting an edit by typing `-`, `+`, or `.` (e.g. to enter a
26
+ negative or decimal value), while reliably rejecting other non-numeric keypresses.
27
+ * `fmtNumber` now treats a `precision` of `null` as full, unrestricted precision rather than
28
+ `'auto'`, aligning a `NumberInput` with `precision: null` so its blurred display matches its
29
+ focused and committed (full-precision) value.
30
+ * Fixed inline grid editing not ending when clicking empty grid space to the right of the last
31
+ column or below the last row - such clicks now commit the active edit.
32
+ * Fixed `Icon.placeholder()` to render with the correct width.
33
+ * `CheckboxButton` no longer leaks `HoistInputProps` into underlying HTML `<button>` element.
34
+ * Fixed `Select` (and `SelectEditor`) dropdown menu sizing: windowed menus now auto-size to their
35
+ option labels instead of the control/cell width, and an explicit `menuWidth` is respected rather
36
+ than overridden by content auto-sizing.
37
+
38
+ ### ⚙️ Typescript API Adjustments
39
+
40
+ * `GridFilterFieldSpec.renderer` is now typed as a pure value transform (`GridFilterRenderer`),
41
+ rather than a `ColumnRenderer`. This more accurately represents the existing run-time limitation
42
+ that a complex column renderer would throw.
43
+
44
+ ### ✨ Styles
45
+
46
+ * Vertical (left/right) `TabContainer` switchers now render a modern rounded-pill treatment by
47
+ default, customizable via new `--xh-tab-switcher-vertical-*` CSS variables.
48
+
3
49
  ## 86.1.0 - 2026-06-22
4
50
 
5
51
  ### 🎁 New Features
@@ -0,0 +1,31 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2026 Extremely Heavy Industries Inc.
6
+ */
7
+ import {hoistCmp} from '@xh/hoist/core';
8
+ import {jsonInput, JsonInputProps} from '@xh/hoist/desktop/cmp/input';
9
+ import {fmtJson, timestampReplacer} from '@xh/hoist/format';
10
+
11
+ /**
12
+ * Readonly `jsonInput` preconfigured for Admin Console stat / config displays - sized to fill its
13
+ * container, without line numbers or a fullscreen button, and rendering timestamp-suffixed fields
14
+ * as readable dates. Pass the data via `value` (any object) or `bind`, plus any additional
15
+ * `jsonInput` props (e.g. `enableSearch`) to extend or override the defaults.
16
+ */
17
+ export const adminJsonDisplay = hoistCmp.factory<JsonInputProps>({
18
+ displayName: 'AdminJsonDisplay',
19
+ render(props) {
20
+ return jsonInput({
21
+ readonly: true,
22
+ flex: 1,
23
+ width: '100%',
24
+ height: '100%',
25
+ showFullscreenButton: false,
26
+ lineNumbers: false,
27
+ formatter: v => fmtJson(v, {replacer: timestampReplacer()}),
28
+ ...props
29
+ });
30
+ }
31
+ });
package/admin/App.scss CHANGED
@@ -6,19 +6,11 @@
6
6
  */
7
7
 
8
8
  .xh-admin-app {
9
+ // The admin console uses left side-tab rails throughout - give them a consistent minimum width.
10
+ // All other vertical-switcher styling is inherited from the framework default.
9
11
  .xh-tab-switcher--left,
10
12
  .xh-tab-switcher--right {
11
- min-width: 160px;
12
- background-color: var(--xh-grid-bg-odd);
13
- border-right: var(--xh-border-solid);
14
-
15
- .bp6-tab {
16
- border-radius: 0 !important;
17
- }
18
-
19
- .xh-tab-switcher__tab {
20
- line-height: 40px;
21
- }
13
+ --xh-tab-switcher-vertical-min-width: 160px;
22
14
  }
23
15
 
24
16
  &__editor-dialog {
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
9
- import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
9
+ import {HoistModel, managed, PlainObject, TaskObserver, XH} from '@xh/hoist/core';
10
10
  import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
11
11
  import {pluralize} from '@xh/hoist/utils/js';
12
12
  import {isEmpty, zipWith} from 'lodash';
@@ -32,7 +32,7 @@ export class JsonSearchImplModel extends HoistModel {
32
32
  @bindable path: string = '';
33
33
  @bindable readerContentType: 'document' | 'matches' = 'matches';
34
34
  @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
35
- @bindable readerContent: string = '';
35
+ @bindable readerContent: string | PlainObject[] = '';
36
36
  @bindable matchingNodeCount: number = 0;
37
37
 
38
38
  get subjectName(): string {
@@ -137,7 +137,7 @@ export class JsonSearchImplModel extends HoistModel {
137
137
  const {json} = this.selectedRecord.data;
138
138
 
139
139
  if (this.readerContentType === 'document') {
140
- this.readerContent = JSON.stringify(JSON.parse(json), null, 2);
140
+ this.readerContent = json;
141
141
  return;
142
142
  }
143
143
 
@@ -162,7 +162,7 @@ export class JsonSearchImplModel extends HoistModel {
162
162
  value
163
163
  };
164
164
  });
165
- this.readerContent = JSON.stringify(nodes, null, 2);
165
+ this.readerContent = nodes;
166
166
  });
167
167
  }
168
168
 
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {AppModel} from '@xh/hoist/admin/AppModel';
8
9
  import {ConnPoolMonitorModel} from '@xh/hoist/admin/tabs/cluster/instances/connpool/ConnPoolMonitorModel';
9
10
  import {chart} from '@xh/hoist/cmp/chart';
@@ -12,7 +13,6 @@ import {filler, hframe, vframe} from '@xh/hoist/cmp/layout';
12
13
  import {creates, hoistCmp} from '@xh/hoist/core';
13
14
  import {button, exportButton} from '@xh/hoist/desktop/cmp/button';
14
15
  import {errorMessage} from '@xh/hoist/cmp/error';
15
- import {jsonInput} from '@xh/hoist/desktop/cmp/input';
16
16
  import {panel} from '@xh/hoist/desktop/cmp/panel';
17
17
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
18
18
  import {Icon} from '@xh/hoist/icon';
@@ -74,7 +74,7 @@ const bbar = hoistCmp.factory<ConnPoolMonitorModel>({
74
74
  });
75
75
 
76
76
  const poolConfigPanel = hoistCmp.factory<ConnPoolMonitorModel>({
77
- render({model}) {
77
+ render() {
78
78
  return panel({
79
79
  title: 'Pool Configuration',
80
80
  icon: Icon.info(),
@@ -84,15 +84,7 @@ const poolConfigPanel = hoistCmp.factory<ConnPoolMonitorModel>({
84
84
  defaultSize: 450,
85
85
  defaultCollapsed: true
86
86
  },
87
- item: jsonInput({
88
- readonly: true,
89
- height: '100%',
90
- width: '100%',
91
- enableSearch: true,
92
- showFullscreenButton: false,
93
- lineNumbers: false,
94
- value: JSON.stringify(model.poolConfiguration, null, 2)
95
- })
87
+ item: adminJsonDisplay({enableSearch: true, bind: 'poolConfiguration'})
96
88
  });
97
89
  }
98
90
  });
@@ -4,15 +4,14 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {DetailsModel} from '@xh/hoist/admin/tabs/cluster/instances/services/DetailsModel';
8
9
  import {placeholder} from '@xh/hoist/cmp/layout';
9
10
  import {creates, hoistCmp, XH} from '@xh/hoist/core';
10
11
  import {errorMessage} from '@xh/hoist/cmp/error';
11
12
  import {panel} from '@xh/hoist/desktop/cmp/panel';
12
- import {jsonInput} from '@xh/hoist/desktop/cmp/input';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {isEmpty} from 'lodash';
15
- import {fmtJson, timestampReplacer} from '@xh/hoist/format';
16
15
 
17
16
  export const detailsPanel = hoistCmp.factory({
18
17
  model: creates(DetailsModel),
@@ -50,16 +49,6 @@ const stats = hoistCmp.factory<DetailsModel>({
50
49
  ? []
51
50
  : [Icon.questionCircle(), 'This service does not report any admin stats.'])
52
51
  )
53
- : panel(
54
- jsonInput({
55
- readonly: true,
56
- width: '100%',
57
- height: '100%',
58
- enableSearch: true,
59
- showFullscreenButton: false,
60
- lineNumbers: false,
61
- value: fmtJson(stats, {replacer: timestampReplacer()})
62
- })
63
- );
52
+ : panel(adminJsonDisplay({enableSearch: true, bind: 'stats'}));
64
53
  }
65
54
  });
@@ -4,21 +4,20 @@
4
4
  *
5
5
  * Copyright © 2026 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {adminJsonDisplay} from '@xh/hoist/admin/AdminJsonDisplay';
7
8
  import {grid} from '@xh/hoist/cmp/grid';
8
9
  import {placeholder} from '@xh/hoist/cmp/layout';
9
10
  import {creates, hoistCmp} from '@xh/hoist/core';
10
- import {jsonInput} from '@xh/hoist/desktop/cmp/input';
11
11
  import {panel} from '@xh/hoist/desktop/cmp/panel';
12
12
  import {Icon} from '@xh/hoist/icon';
13
13
  import {DetailModel} from './DetailModel';
14
14
  import './ClusterObjects.scss';
15
- import {fmtJson, timestampReplacer} from '@xh/hoist/format';
16
15
 
17
16
  export const detailPanel = hoistCmp.factory({
18
17
  model: creates(DetailModel),
19
18
 
20
19
  render({model}) {
21
- const {instanceName, selectedAdminStats, objectName, objectType} = model;
20
+ const {instanceName, objectName, objectType} = model;
22
21
  if (!objectName) return placeholder(Icon.grip(), 'Select an object to view details...');
23
22
 
24
23
  return panel({
@@ -36,15 +35,7 @@ export const detailPanel = hoistCmp.factory({
36
35
  defaultSize: '80%',
37
36
  collapsible: false
38
37
  },
39
- item: jsonInput({
40
- readonly: true,
41
- flex: 1,
42
- width: '100%',
43
- height: '100%',
44
- showFullscreenButton: false,
45
- lineNumbers: false,
46
- value: fmtJson(selectedAdminStats, {replacer: timestampReplacer()})
47
- })
38
+ item: adminJsonDisplay({bind: 'selectedAdminStats'})
48
39
  })
49
40
  ]
50
41
  });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Readonly `jsonInput` preconfigured for Admin Console stat / config displays - sized to fill its
3
+ * container, without line numbers or a fullscreen button, and rendering timestamp-suffixed fields
4
+ * as readable dates. Pass the data via `value` (any object) or `bind`, plus any additional
5
+ * `jsonInput` props (e.g. `enableSearch`) to extend or override the defaults.
6
+ */
7
+ export declare const adminJsonDisplay: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/desktop/cmp/input").CodeInputProps>;
@@ -1,5 +1,5 @@
1
1
  import { GridConfig, GridModel } from '@xh/hoist/cmp/grid';
2
- import { HoistModel, TaskObserver } from '@xh/hoist/core';
2
+ import { HoistModel, PlainObject, TaskObserver } from '@xh/hoist/core';
3
3
  /**
4
4
  * @internal
5
5
  */
@@ -16,7 +16,7 @@ export declare class JsonSearchImplModel extends HoistModel {
16
16
  path: string;
17
17
  readerContentType: 'document' | 'matches';
18
18
  pathFormat: 'XPath' | 'JSONPath';
19
- readerContent: string;
19
+ readerContent: string | PlainObject[];
20
20
  matchingNodeCount: number;
21
21
  get subjectName(): string;
22
22
  get docSearchUrl(): string;
@@ -3,6 +3,7 @@ import { ColumnState } from '@xh/hoist/cmp/grid';
3
3
  import { HoistModel, HoistProps, LayoutProps, PlainObject, ReactionSpec, TestSupportProps } from '@xh/hoist/core';
4
4
  import { RecordSet } from '@xh/hoist/data/impl/RecordSet';
5
5
  import type { ColDef, ColGroupDef, GetContextMenuItemsParams, GridOptions, GridReadyEvent, ProcessCellForExportParams } from '@xh/hoist/kit/ag-grid';
6
+ import { type MouseEvent } from 'react';
6
7
  import './Grid.scss';
7
8
  import { GridModel } from './GridModel';
8
9
  export interface GridProps<M extends GridModel = GridModel> extends HoistProps<M>, LayoutProps, TestSupportProps {
@@ -41,6 +42,7 @@ export interface GridProps<M extends GridModel = GridModel> extends HoistProps<M
41
42
  export declare const Grid: import("react").FC<GridProps<GridModel>>, grid: import("@xh/hoist/core").ElementFactory<GridProps<GridModel>>;
42
43
  export declare class GridLocalModel extends HoistModel {
43
44
  xhImpl: boolean;
45
+ private static EMPTY_SPACE_SELECTOR;
44
46
  private model;
45
47
  agOptions: GridOptions;
46
48
  viewRef: import("react").RefObject<HTMLElement> & import("react").RefCallback<HTMLElement>;
@@ -55,7 +57,7 @@ export declare class GridLocalModel extends HoistModel {
55
57
  getColumnDefs(): Array<ColDef | ColGroupDef>;
56
58
  getContextMenuItems: (params: GetContextMenuItemsParams) => (string | import("ag-grid-community").MenuItemDef<any, any>)[];
57
59
  dataReaction(): {
58
- track: () => (boolean | import("../../data").StoreRecord[] | RecordSet | import("@xh/hoist/core").VSide)[];
60
+ track: () => (boolean | import("@xh/hoist/core").VSide | import("../../data").StoreRecord[] | RecordSet)[];
59
61
  run: () => void;
60
62
  };
61
63
  selectionReaction(): {
@@ -63,7 +65,7 @@ export declare class GridLocalModel extends HoistModel {
63
65
  run: () => void;
64
66
  };
65
67
  sortReaction(): {
66
- track: () => (import("@xh/hoist/cmp/grid").GridSorter[] | GridApi<any>)[];
68
+ track: () => (GridApi<any> | import("@xh/hoist/cmp/grid").GridSorter[])[];
67
69
  run: ([agApi, sortBy]: [any, any]) => void;
68
70
  };
69
71
  groupReaction(): {
@@ -83,7 +85,7 @@ export declare class GridLocalModel extends HoistModel {
83
85
  get useScrollOptimization(): boolean;
84
86
  applyScrollOptimization(): void;
85
87
  columnsReaction(): {
86
- track: () => (import("@xh/hoist/cmp/grid").ColumnOrGroup[] | GridApi<any>)[];
88
+ track: () => (GridApi<any> | import("@xh/hoist/cmp/grid").ColumnOrGroup[])[];
87
89
  run: ([api]: [any]) => void;
88
90
  };
89
91
  columnStateReaction(): ReactionSpec<[GridApi, ColumnState[]]>;
@@ -129,6 +131,7 @@ export declare class GridLocalModel extends HoistModel {
129
131
  writeFilterState(filterState: any): void;
130
132
  processCellForClipboard: ({ value, node, column }: ProcessCellForExportParams) => any;
131
133
  navigateToNextCell: (agParams: any) => import("ag-grid-community").CellPosition;
134
+ onViewMouseDown: (evt: MouseEvent) => void;
132
135
  onCellMouseDown: (evt: any) => void;
133
136
  onKeyDown: (evt: any) => void;
134
137
  onRowClicked: (evt: any) => void;
@@ -155,6 +155,10 @@ export interface GridConfig {
155
155
  * expand/collapse options in the default context menu will be enhanced to allow users to
156
156
  * expand/collapse to a specific level. See {@link GroupingChooserModel.valueDisplayNames}
157
157
  * for a convenient getter that will satisfy this API when a GroupingChooser is in play.
158
+ *
159
+ * Labels are matched to levels top-down and need not cover the full depth of the grid - provide
160
+ * a partial array to label only the top levels (e.g. when deeper levels should not be
161
+ * expand-to targets). Deeper, unlabelled levels are omitted from the menu.
158
162
  */
159
163
  levelLabels?: Thunkable<string[]>;
160
164
  /**
@@ -501,8 +505,15 @@ export declare class GridModel extends HoistModel {
501
505
  expandToLevel(level: number): void;
502
506
  /**
503
507
  * Get the resolved level labels for the current state of the grid.
508
+ * An over-long array is truncated to the current `maxDepth`.
504
509
  */
505
510
  get resolvedLevelLabels(): string[];
511
+ /**
512
+ * True if the given `resolvedLevelLabels` index is the grid's current expand level - used to
513
+ * mark the active item in the "Expand to..." menu. The deepest labelled level counts as current
514
+ * whenever the grid is expanded to or beyond it.
515
+ */
516
+ isCurrentExpandLevel(idx: number): boolean;
506
517
  /**
507
518
  * Sort this grid.
508
519
  * This method is a no-op if provided any sorters without a corresponding column.
@@ -1,17 +1,34 @@
1
- import { ColumnRenderer } from '@xh/hoist/cmp/grid';
2
1
  import { HoistInputProps } from '@xh/hoist/cmp/input';
3
2
  import { PlainObject } from '@xh/hoist/core';
4
3
  import { FieldFilterOperator } from '@xh/hoist/data';
5
4
  import { BaseFilterFieldSpec, BaseFilterFieldSpecConfig } from '@xh/hoist/data/filter/BaseFilterFieldSpec';
5
+ import { ReactNode } from 'react';
6
6
  import { GridFilterModel } from './GridFilterModel';
7
+ /**
8
+ * Produces the display content for an entry in the Values tab of a column filter. Must be a pure
9
+ * transform of the value - the values list carries no source record, and there is no column or
10
+ * grid context to reach for (unlike a Column's `renderer`, which runs against the source grid).
11
+ */
12
+ export type GridFilterRenderer = (value: any) => ReactNode;
13
+ /**
14
+ * Produces the value to sort by for an entry in the Values tab of a column filter. Must be a pure
15
+ * transform of the value - the values list carries no source record, and there is no column or
16
+ * grid context to reach for (unlike a Column's `sortValue`, which runs against the source grid).
17
+ */
18
+ export type GridFilterSortValueFn = (value: any) => any;
7
19
  export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
8
20
  /** GridFilterModel instance owning this fieldSpec. */
9
21
  filterModel?: GridFilterModel;
10
22
  /**
11
- * Function returning a formatted string for each value in this values filter display.
12
- * If not provided, the Column's renderer will be used.
23
+ * Pure function producing the display content for each entry in the values filter display. If
24
+ * not provided, the Column's renderer is used where it can be applied to a bare value.
25
+ */
26
+ renderer?: GridFilterRenderer;
27
+ /**
28
+ * Pure function producing the value to sort each entry of the values filter display by. If not
29
+ * provided, the Column's sortValue is used where it can be applied to a bare value.
13
30
  */
14
- renderer?: ColumnRenderer;
31
+ sortValue?: GridFilterSortValueFn;
15
32
  /**
16
33
  * Props to pass through to the HoistInput components used on the custom filter tab.
17
34
  * Note that the HoistInput component used is decided by fieldType.
@@ -26,12 +43,13 @@ export interface GridFilterFieldSpecConfig extends BaseFilterFieldSpecConfig {
26
43
  */
27
44
  export declare class GridFilterFieldSpec extends BaseFilterFieldSpec {
28
45
  filterModel: GridFilterModel;
29
- renderer: ColumnRenderer;
46
+ renderer: GridFilterRenderer;
47
+ sortValue: GridFilterSortValueFn;
30
48
  inputProps: PlainObject;
31
49
  defaultOp: FieldFilterOperator;
32
50
  /** Total number of unique values for this field in the source, regardless of other filters. */
33
51
  allValuesCount: number;
34
- constructor({ filterModel, renderer, inputProps, defaultOp, ...rest }: GridFilterFieldSpecConfig);
52
+ constructor({ filterModel, renderer, sortValue, inputProps, defaultOp, ...rest }: GridFilterFieldSpecConfig);
35
53
  getUniqueValue(value: unknown): unknown;
36
54
  loadValuesFromSource(): void;
37
55
  private cleanFilter;
@@ -13,6 +13,7 @@ export interface ExpandToLevelButtonProps extends Omit<ButtonProps, 'title'> {
13
13
  }
14
14
  /**
15
15
  * A menu button to expand a multi-level grouped or tree grid out to a desired level.
16
- * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel.
16
+ * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
17
+ * one entry per labelled level, so a partial array will limit the available expand-to targets.
17
18
  */
18
19
  export declare const ExpandToLevelButton: import("react").FC<ExpandToLevelButtonProps>, expandToLevelButton: import("@xh/hoist/core").ElementFactory<ExpandToLevelButtonProps>;
@@ -4,6 +4,13 @@ import './CodeInput.scss';
4
4
  export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps {
5
5
  /** True to focus the control on render. */
6
6
  autoFocus?: boolean;
7
+ /**
8
+ * True to automatically format content for display using the configured `formatter`.
9
+ * Defaults to true for `readonly` inputs - set false to opt out. May also be enabled on
10
+ * editable inputs, in which case content is formatted on blur (never mid-edit, so user
11
+ * edits and cursor position are preserved while typing).
12
+ */
13
+ autoFormat?: boolean;
7
14
  /** False to not commit on every change/keystroke, default true. */
8
15
  commitOnChange?: boolean;
9
16
  /**
@@ -36,7 +36,7 @@ export interface NumberInputProps extends HoistProps, LayoutProps, StyleProps, H
36
36
  onKeyDown?: KeyboardEventHandler<HTMLInputElement>;
37
37
  /** Text to display when control is empty. */
38
38
  placeholder?: string;
39
- /** Max decimal precision of the value, defaults to 4. */
39
+ /** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
40
40
  precision?: NumericPrecision;
41
41
  /** Element to display inline on the right side of the input. */
42
42
  rightElement?: ReactNode;
@@ -0,0 +1,20 @@
1
+ import { ReactNode } from 'react';
2
+ /**
3
+ * Compute an explicit pixel width for a windowed (virtualized) `Select` menu.
4
+ *
5
+ * Windowed menus can't auto-size via CSS - react-window absolutely-positions rows at width:100%,
6
+ * so option content never widens the menu and it collapses to the control width. This restores
7
+ * content-based sizing using Canvas + off-screen DOM, the same render-and-measure approach as the
8
+ * grid's `ColumnWidthCalculator`: render each option's actual menu markup (via the configured
9
+ * `optionRenderer`, if any), canvas-estimate its width to pick the widest candidates, then measure
10
+ * only those candidates' displayed width in a hidden probe - so custom `optionRenderer`s (icons,
11
+ * multi-element content, etc.) size correctly. Unlike the grid (which may autosize many columns
12
+ * over thousands of records), this renders one menu's options once per options change.
13
+ *
14
+ * @param options - normalized (possibly grouped) Select options.
15
+ * @param renderOption - renders an option to its menu `ReactNode` (e.g. model.formatOptionLabel).
16
+ * @param portal - shared menu portal div, used to host probes so menu CSS (font, padding) applies.
17
+ * @returns explicit menu width in px, or null to fall back to the control width.
18
+ * @internal
19
+ */
20
+ export declare function calcWindowedMenuWidth(options: any[], renderOption: (opt: any) => ReactNode, portal: HTMLElement): number;
@@ -20,7 +20,7 @@ export declare class RestFormModel extends HoistModel {
20
20
  del?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
21
21
  edit?: import("react").ReactNode | ((recs: import("@xh/hoist/data").StoreRecord[]) => import("react").ReactNode);
22
22
  };
23
- get actions(): (import("@xh/hoist/data").RecordActionSpec | import("@xh/hoist/data").RecordAction)[];
23
+ get actions(): (import("@xh/hoist/data").RecordAction | import("@xh/hoist/data").RecordActionSpec)[];
24
24
  get editors(): RestGridEditor[];
25
25
  get gridModel(): import("../../../../cmp/grid").GridModel;
26
26
  get store(): import("@xh/hoist/desktop/cmp/rest").RestStore;
@@ -33,8 +33,9 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
33
33
  */
34
34
  omitFourDigitComma?: boolean;
35
35
  /**
36
- * Desired number of decimal places, or 'auto' (default) to adjust the displayed precision
37
- * automatically based on the scale of the value.
36
+ * Desired number of decimal places, 'auto' (default) to adjust the displayed precision
37
+ * automatically based on the scale of the value, or null for full, unrestricted precision
38
+ * (capped at the max supported precision, with trailing zeros trimmed).
38
39
  */
39
40
  precision?: Precision;
40
41
  /** Prefix to prepend to value (between the number and its sign). */
@@ -65,13 +66,20 @@ export interface NumberFormatOptions extends Omit<FormatOptions<number>, 'toolti
65
66
  *
66
67
  * e.g. `{precision:4, zeroPad:2}` will format `1.2` → "1.20" and `1.234` → "1.234"
67
68
  *
68
- * Default is true if a fixed precision is set, false if precision is 'auto'.
69
+ * Default is true if a fixed precision is set, false if precision is 'auto' or null (full).
69
70
  */
70
71
  zeroPad?: ZeroPad;
71
72
  }
72
73
  export interface QuantityFormatOptions extends NumberFormatOptions {
74
+ /** True to compact values \>= 1 million into units of millions (m). Default true. */
73
75
  useMillions?: boolean;
76
+ /** True to compact values \>= 1 billion into units of billions (b). Default true. */
74
77
  useBillions?: boolean;
78
+ /**
79
+ * True to compact to m/b units only when no precision is lost, else render the full value.
80
+ * Default false.
81
+ */
82
+ lossless?: boolean;
75
83
  }
76
84
  /** Config for pos/neg/neutral color classes. */
77
85
  export interface ColorSpec {
@@ -3,6 +3,7 @@ import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
3
3
  import { HoistProps, Intent, Thunkable } from '@xh/hoist/core';
4
4
  import { ReactElement } from 'react';
5
5
  import { SetRequired } from 'type-fest';
6
+ import './Icon.scss';
6
7
  export interface IconProps extends HoistProps, Partial<Omit<FontAwesomeIconProps, 'ref'>> {
7
8
  /** Name of the icon in FontAwesome. */
8
9
  iconName?: IconName;
@@ -333,9 +334,9 @@ export declare const Icon: {
333
334
  filename: string;
334
335
  }): any;
335
336
  /**
336
- * Returns an empty div with FA sizing classes applied. Can be used to take up room in a layout
337
- * where an icon might otherwise go - e.g. to align a series of menu items, where some items do
338
- * not have an icon but others do.
337
+ * Returns an empty div sized to occupy the width of a standard icon. Can be used to take up
338
+ * room in a layout where an icon might otherwise go - e.g. to align a series of menu items,
339
+ * where some items do not have an icon but others do.
339
340
  */
340
341
  placeholder(opts?: IconProps): any;
341
342
  };
@@ -10,6 +10,7 @@ export interface ExpandToLevelButtonProps extends MenuButtonProps {
10
10
  }
11
11
  /**
12
12
  * A menu button to expand a multi-level grouped or tree grid out to a desired level.
13
- * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel.
13
+ * Requires {@link GridConfig.levelLabels} to be configured on the bound GridModel - the menu offers
14
+ * one entry per labelled level, so a partial array will limit the available expand-to targets.
14
15
  */
15
16
  export declare const ExpandToLevelButton: import("react").FC<ExpandToLevelButtonProps>, expandToLevelButton: import("@xh/hoist/core").ElementFactory<ExpandToLevelButtonProps>;
@@ -26,7 +26,7 @@ export interface NumberInputProps extends HoistProps, HoistInputProps, StyleProp
26
26
  modifier?: string;
27
27
  /** Text to display when control is empty. */
28
28
  placeholder?: string;
29
- /** Max decimal precision of the value, defaults to 4. */
29
+ /** Max decimal precision of the value, defaults to 4. Set to null for full, unrestricted precision. */
30
30
  precision?: NumericPrecision;
31
31
  /**
32
32
  * Scale factor to apply when converting between the internal and external value. Useful for
package/cmp/grid/Grid.ts CHANGED
@@ -48,6 +48,7 @@ import {consumeEvent, isDisplayed, logWithDebug} from '@xh/hoist/utils/js';
48
48
  import {composeRefs, createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
49
49
  import classNames from 'classnames';
50
50
  import {compact, debounce, isBoolean, isEmpty, isEqual, isNil, max, maxBy, merge} from 'lodash';
51
+ import {type MouseEvent} from 'react';
51
52
  import './Grid.scss';
52
53
  import {GridModel} from './GridModel';
53
54
  import {columnGroupHeader} from './impl/ColumnGroupHeader';
@@ -135,6 +136,7 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
135
136
  ],
136
137
  testId,
137
138
  onKeyDown: impl.onKeyDown,
139
+ onMouseDown: impl.onViewMouseDown,
138
140
  ref: composeRefs(impl.viewRef, model.viewRef, ref)
139
141
  }),
140
142
  colChooserModel ? platformColChooser({model: colChooserModel}) : null,
@@ -151,6 +153,10 @@ export const [Grid, grid] = hoistCmp.withFactory<GridProps>({
151
153
  export class GridLocalModel extends HoistModel {
152
154
  override xhImpl = true;
153
155
 
156
+ // Structural "empty" grid space.
157
+ private static EMPTY_SPACE_SELECTOR =
158
+ '.ag-body-viewport, .ag-center-cols-viewport, .ag-center-cols-container, .ag-row';
159
+
154
160
  @lookup(GridModel)
155
161
  private model: GridModel;
156
162
  agOptions: GridOptions;
@@ -844,6 +850,17 @@ export class GridLocalModel extends HoistModel {
844
850
  return this.rowKeyNavSupport?.navigateToNextCell(agParams);
845
851
  };
846
852
 
853
+ // `stopEditingWhenCellsLoseFocus` doesn't fire on clicks in empty grid space (focus stays in
854
+ // the grid), so commit the active edit on those clicks ourselves. Require exact match on empty
855
+ // space to avoid interfering with cell editors.
856
+ onViewMouseDown = (evt: MouseEvent) => {
857
+ const {model} = this,
858
+ target = evt.target as HTMLElement;
859
+ if (model.isEditing && target.matches(GridLocalModel.EMPTY_SPACE_SELECTOR)) {
860
+ model.agApi?.stopEditing();
861
+ }
862
+ };
863
+
847
864
  onCellMouseDown = evt => {
848
865
  const {model} = this;
849
866
  if (model.highlightRowOnClick) {