@xh/hoist 56.2.0 → 56.4.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.
package/CHANGELOG.md CHANGED
@@ -1,18 +1,79 @@
1
1
  # Changelog
2
2
 
3
- ## 57.0.0-SNAPSHOT - unreleased
3
+ ## v56.4.0 - 2023-05-10
4
4
 
5
- ## 56.2.0 - 2023-04-28
6
- * Expose `margin` property on DashContainerModel.
5
+ ### 🎁 New Features
6
+
7
+ * Ensure that non-committed values are also checked when filtering a store with a FieldFilter.
8
+ This will maximize chances that records under edit will not disappear from user view due to
9
+ active filters.
10
+
11
+ ### 🐞 Bug Fixes
12
+
13
+ * Fix bug where Grid ColumnHeaders could throw when `groupDisplayType` was set to `singleColumn`.
14
+
15
+ ### ⚙️ Technical
16
+ * Adjustment to core model lookup in Hoist components to better support automated testing.
17
+ Components no longer strictly require rendering within an `AppContainer`.
18
+
19
+ ### ⚙️ Typescript API Adjustments
20
+
21
+ * Improved return types for `FetchService` methods and corrected `FetchOptions` interface.
22
+
23
+ ## v56.3.0 - 2023-05-08
24
+
25
+ ### 🎁 New Features
26
+
27
+ * Added support for new `sortOrder` argument to `XH.showBanner()`. A default sort order is applied
28
+ if unspecified, ensuring banners do not unexpectedly change order when refreshed.
29
+
30
+ ### ⚙️ Typescript API Adjustments
31
+
32
+ * Improved the recommendation for the app `declare` statement within
33
+ our [TypeScript migration docs](https://github.com/xh/hoist-react/blob/develop/docs/upgrade-to-typescript.md#bootstrapts--service-declarations).
34
+ * See this [Toolbox commit](https://github.com/xh/toolbox/commit/8df642cf) for a small,
35
+ recommended app-level change to improve autocompletion and usage checks within IntelliJ.
36
+ * Added generic support to `XH.message()` and `XH.prompt()` signatures with return type
37
+ of `Promise<T | boolean>`.
38
+ * Moved declaration of optional `children` prop to base `HoistProps` interface - required for TSX
39
+ support.
40
+
41
+ ### ✨ Styles
42
+
43
+ * Removed `--xh-banner-height` CSS var.
44
+ * Desktop banners are implemented via `Toolbar`, which correctly sets a min height.
45
+ * Mobile banners now specify `min-height: 40px` via the `.xh-banner` class.
46
+ * This change allows banners containing custom components to grow to fit their contents without
47
+ requiring app-level CSS overrides.
48
+ * Added new `--xh-grid-filter-popover-[height|width]-px` CSS variables to support easier custom
49
+ sizing for grid column header filter popovers.
7
50
 
8
51
  ### ⚙️ Technical
9
- * Optimize scrolling performance for `Grid` and `DataView`
10
52
 
11
- ## 56.1.0 - 2023-04-14
12
- * Add support for new memory management diagnostics provided by hoist-core
13
- (requires hoist-core 16.1.0 for full operation).
53
+ * Updated internal config defaults to support latest AG Grid v29.3.4+ with use of
54
+ AG `suppressBrowserResizeObserver` config. Applications are encouraged to update to the latest AG
55
+ Grid dependencies to take advantage of ongoing performance updates.
56
+
57
+ ## v56.2.0 - 2023-04-28
58
+
59
+ ### 🎁 New Features
60
+
61
+ * Added `DashContainerModel.margin` config to customize the width of the resize splitters
62
+ between widgets.
63
+
64
+ ### ⚙️ Technical
65
+
66
+ * Improve scrolling performance for `Grid` and `DataView` via internal configuration updates.
67
+
68
+ ## v56.1.0 - 2023-04-14
69
+
70
+ ### 🎁 New Features
71
+
72
+ * Display improved memory management diagnostics within Admin console Memory Monitor.
73
+ * New metrics require optional-but-recommended update to `hoist-core >= v16.1.0`.
14
74
 
15
75
  ### 🐞 Bug Fixes
76
+
16
77
  * Fixes bug with display/reporting of exceptions during app initialization sequence.
17
78
 
18
79
  ## v56.0.0 - 2023-03-29
@@ -35,10 +96,12 @@
35
96
  * Add a dependency on `@ag-grid-community/styles` to import new dedicated styles package.
36
97
  * Imports of AG Grid CSS files within your app's `Bootstrap.ts` file will also need to be
37
98
  updated to import styles from their new location. The recommended imports are now:
99
+
38
100
  ```typescript
39
101
  import '@ag-grid-community/styles/ag-grid.css';
40
102
  import '@ag-grid-community/styles/ag-theme-balham.css';
41
103
  ```
104
+
42
105
  * New `xhActivityTrackingConfig` soft-configuration entry places new limits on the size of
43
106
  any `data` objects passed to `XH.track()` calls.
44
107
  * Any track requests with data objects exceeding this length will be persisted, but without the
@@ -72,20 +135,14 @@ import '@ag-grid-community/styles/ag-theme-balham.css';
72
135
 
73
136
  ## v55.4.0 - 2023-03-23
74
137
 
75
- ### 🐞 Bug Fixes
76
- * Addresses `AgGrid` v28 regression whereby changing column visibility via state throws an
77
- exception and doesn't
78
- render the grid when column groups are set via the `groupId` property.
79
-
80
138
  ### 💥 Breaking Changes
81
- * Hoist now requires `AgGrid` v29.1.0 or higher - update your `AgGrid` dependency in your app's
82
- `package.json` file. See the [ag-Grid Changelog](https://www.ag-grid.com/changelog) for details.
83
- * `AgGrid` stylesheets are now imported from the new `@ag-grid-community/styles` module. Update
84
- your app's `Bootstrap.ts` file to import `ag-grid.css` and `ag-theme-balham.css` from this
85
- module, and include it as a dependency in your app's `package.json` file.
86
139
 
87
- ### ⚙️ Technical
88
- * AgGrid `28.1.0 -> 29.1.0`
140
+ * Requires AG Grid v29.0.0 or higher - see release notes for v56.0.0 above.
141
+
142
+ ### 🐞 Bug Fixes
143
+
144
+ * Addresses `AgGrid` v28 regression whereby changing column visibility via state breaks grid
145
+ rendering when column groups are set via the `groupId` property.
89
146
 
90
147
  ## v55.3.2 - 2023-03-22
91
148
 
@@ -1026,7 +1083,7 @@ to use TypeScript for its own app-level code.
1026
1083
  with the select library component and touch devices.
1027
1084
  * Ensure `Column.autosizeBufferPx` is respected if provided.
1028
1085
 
1029
- ### ✨ Style
1086
+ ### ✨ Styles
1030
1087
 
1031
1088
  * New `--xh-menu-item` CSS vars added, with tweaks to default desktop menu styling.
1032
1089
  * Highlight background color added to mobile menu items while pressed.
@@ -1150,7 +1207,7 @@ to use TypeScript for its own app-level code.
1150
1207
  * Triggering inline editing of text or select editor cells by typing characters will no longer lose
1151
1208
  the first character pressed.
1152
1209
 
1153
- ### ✨ Style
1210
+ ### ✨ Styles
1154
1211
 
1155
1212
  * New `TreeStyle.COLORS` and `TreeStyle.COLORS_AND_BORDERS` tree grid styles have been added. Use
1156
1213
  the `--xh-grid-tree-group-color-level-*` CSS vars to customize colors as needed.
@@ -1192,7 +1249,7 @@ to use TypeScript for its own app-level code.
1192
1249
  `agOptions`.
1193
1250
  * Fixes an issue on iOS where `NumberInput` would incorrectly bring up a text keyboard.
1194
1251
 
1195
- ### ✨ Style
1252
+ ### ✨ Styles
1196
1253
 
1197
1254
  * Reduced default Grid header and group row heights to minimize their use of vertical space,
1198
1255
  especially at larger sizing modes. As before, apps can override via the `AgGrid.HEADER_HEIGHTS`
@@ -1217,7 +1274,7 @@ to use TypeScript for its own app-level code.
1217
1274
  * The in-app changelog will no longer prompt the user with the "What's New" button if category-based
1218
1275
  filtering results in a version without any release notes.
1219
1276
 
1220
- ### ✨ Style
1277
+ ### ✨ Styles
1221
1278
 
1222
1279
  * New CSS vars added to support easier customization of desktop Tab font/size/color. Tabs now
1223
1280
  respect standard `--xh-font-size` by default.
@@ -1239,7 +1296,7 @@ to use TypeScript for its own app-level code.
1239
1296
  * Mobile `Select` input now supports async `queryFn` prop for parity with desktop.
1240
1297
  * `TreeMapModel` now supports new `maxLabels` config for improved performance.
1241
1298
 
1242
- ### ✨ Style
1299
+ ### ✨ Styles
1243
1300
 
1244
1301
  * Hoist's default font is now [Inter](https://rsms.me/inter/), shipped and bundled via the
1245
1302
  `inter-ui` npm package. Inter is a modern, open-source font that leverages optical sizing to
@@ -1300,7 +1357,7 @@ to use TypeScript for its own app-level code.
1300
1357
 
1301
1358
  * Fixed an issue preventing `FormField` labels from rendering if `fieldDefaults` was undefined.
1302
1359
 
1303
- ### ✨ Style
1360
+ ### ✨ Styles
1304
1361
 
1305
1362
  * New `Badge.compact` prop sets size to half that of parent element when true (default false). The
1306
1363
  `position` prop has been removed in favor of customizing placement of the component.
@@ -1354,7 +1411,7 @@ to use TypeScript for its own app-level code.
1354
1411
  and
1355
1412
  `selectedIds`, respectively, in `StoreSelectionModel`
1356
1413
 
1357
- ### ✨ Style
1414
+ ### ✨ Styles
1358
1415
 
1359
1416
  * Higher contrast on grid context menus for improved legibility.
1360
1417
 
@@ -1422,7 +1479,7 @@ to use TypeScript for its own app-level code.
1422
1479
  custom handling in a raw `AgGrid` component, see the example here:
1423
1480
  https://www.ag-grid.com/javascript-grid/row-selection/#example-selection-with-keyboard-arrow-keys
1424
1481
 
1425
- ### ✨ Style
1482
+ ### ✨ Styles
1426
1483
 
1427
1484
  * The red and green color values applied in dark mode have been lightened for improved legibility.
1428
1485
  * The default `colorSpec` config for number formatters has changed to use new dedicated CSS classes
@@ -1478,7 +1535,7 @@ to use TypeScript for its own app-level code.
1478
1535
  * `withShortDebug` has been deprecated. Use `withDebug` instead, which has the identical behavior.
1479
1536
  This API simplification mirrors a recent change to `hoist-core`.
1480
1537
 
1481
- ### ✨ Style
1538
+ ### ✨ Styles
1482
1539
 
1483
1540
  * If the first child of a `Placeholder` component is a Hoist icon, it will not automatically be
1484
1541
  styled to 4x size with reduced opacity. (See new Toolbox example under the "Other" tab.)
@@ -1647,7 +1704,7 @@ your dev-utils dependency for your project to build.
1647
1704
  * Improvements to exception serialization, especially for any raw javascript `Error` thrown by
1648
1705
  client-side code.
1649
1706
 
1650
- ### ✨ Style
1707
+ ### ✨ Styles
1651
1708
 
1652
1709
  * Buttons nested inline within desktop input components (e.g. clear buttons) tweaked to avoid
1653
1710
  odd-looking background highlight on hover.
@@ -2000,7 +2057,7 @@ decorators, in favor of a simpler inheritance-based approach to defining models
2000
2057
  * Fix issue where grid row striping inadvertently disabled by default for non-tree grids.
2001
2058
  * Fix issue where grid empty text cleared on autosize.
2002
2059
 
2003
- ### ✨ Style
2060
+ ### ✨ Styles
2004
2061
 
2005
2062
  * Default `Chart` themes reworked in both light and dark modes to better match overall Hoist theme.
2006
2063
 
@@ -2309,7 +2366,7 @@ below regarding related updates to `GridModel.columns` config processing.
2309
2366
  * `StoreFilterField.filterOptions` has been removed. Set `filterIncludesChildren` directly on
2310
2367
  the store instead.
2311
2368
 
2312
- ### ✨ Style
2369
+ ### ✨ Styles
2313
2370
 
2314
2371
  * CSS variables for "intents" - most commonly used on buttons - have been reworked to use HSL color
2315
2372
  values and support several standard variations of lightness and transparency.
@@ -3658,7 +3715,7 @@ leverage the context for model support discussed above.
3658
3715
  * When checking for a possible expired session within `XH.handleException()`, prompt for app login
3659
3716
  only for Ajax requests made to relative URLs (not e.g. remote APIs accessed via CORS). #1189
3660
3717
 
3661
- ### ✨ Style
3718
+ ### ✨ Styles
3662
3719
 
3663
3720
  * Panel splitter collapse button more visible in dark theme. CSS vars to customize further fixed.
3664
3721
  * The mobile app menu button has been moved to the right side of the top appBar, consistent with its
@@ -3915,7 +3972,7 @@ leverage the context for model support discussed above.
3915
3972
  * FetchService's fetch methods no longer support `acceptJson` parameter. Instead, pass an {"Accept":
3916
3973
  "application/json"} header using the `headers` parameter.
3917
3974
 
3918
- ### ✨ Style
3975
+ ### ✨ Styles
3919
3976
 
3920
3977
  * Black point + grid colors adjusted in dark theme to better blend with overall blue-gray tint.
3921
3978
  * Mobile styles have been adjusted to increase the default font size and grid row height, in
@@ -14,6 +14,7 @@ import {numberInput, switchInput, textInput} from '@xh/hoist/desktop/cmp/input';
14
14
  import {panel} from '@xh/hoist/desktop/cmp/panel';
15
15
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
16
16
  import {Icon} from '@xh/hoist/icon';
17
+ import {fmtTimeZone} from '@xh/hoist/utils/impl';
17
18
  import {LogDisplayModel} from './LogDisplayModel';
18
19
  import './LogViewer.scss';
19
20
 
@@ -87,11 +88,11 @@ const bbar = hoistCmp.factory(() => {
87
88
 
88
89
  return toolbar({
89
90
  items: [
90
- 'Server time:',
91
+ 'Server time: ',
91
92
  clock({
92
93
  timezone: zone,
93
- format: 'HH:mm [GMT]Z',
94
- className: 'xh-font-family-mono xh-font-size-small'
94
+ format: 'HH:mm',
95
+ suffix: fmtTimeZone(zone, XH.getEnv('serverTimeZoneOffset'))
95
96
  })
96
97
  ],
97
98
  omit: !zone // zone env support requires hoist-core 7.1+
@@ -87,7 +87,8 @@ export class MemoryMonitorModel extends HoistModel {
87
87
  floor: 0,
88
88
  top: '30%',
89
89
  height: '70%',
90
- title: {text: 'Heap (mb)'}
90
+ title: {text: 'Heap (mb)'},
91
+ offset: 0
91
92
  }
92
93
  ],
93
94
  tooltip: {outside: true, shared: true}
@@ -185,7 +186,7 @@ export class MemoryMonitorModel extends HoistModel {
185
186
  async dumpHeapAsync() {
186
187
  try {
187
188
  const appEnv = XH.getEnv('appEnvironment').toLowerCase(),
188
- filename = await XH.prompt({
189
+ filename = await XH.prompt<string>({
189
190
  title: 'Dump Heap',
190
191
  icon: Icon.fileArchive(),
191
192
  message: `Specify a filename for the heap dump (to be saved in ${this.heapDumpDir})`,
@@ -93,7 +93,7 @@ export class WebSocketModel extends HoistModel {
93
93
  const {selectedRecords} = this.gridModel;
94
94
  if (isEmpty(selectedRecords)) return;
95
95
 
96
- const message = await XH.prompt({
96
+ const message = await XH.prompt<string>({
97
97
  title: 'Force suspend',
98
98
  icon: Icon.stopCircle(),
99
99
  confirmProps: {text: 'Force Suspend', icon: Icon.stopCircle(), intent: 'danger'},
@@ -19,6 +19,7 @@ import {SizingModeModel} from './SizingModeModel';
19
19
  import {ViewportSizeModel} from './ViewportSizeModel';
20
20
  import {ThemeModel} from './ThemeModel';
21
21
  import {ToastSourceModel} from './ToastSourceModel';
22
+ import {BannerModel} from './BannerModel';
22
23
 
23
24
  /**
24
25
  * Root object for Framework GUI State.
@@ -87,10 +88,11 @@ export class AppContainerModel extends HoistModel {
87
88
  buttonText = mobile ? version : `Update to ${version}`;
88
89
 
89
90
  XH.showBanner({
90
- category: 'app-update',
91
+ category: 'xhAppUpdate',
91
92
  message,
92
93
  icon: Icon.rocket({size: 'lg'}),
93
94
  intent: 'warning',
95
+ sortOrder: BannerModel.BANNER_SORTS.APP_UPDATE,
94
96
  enableClose: false,
95
97
  actionButtonProps: {
96
98
  icon: Icon.refresh(),
@@ -18,36 +18,46 @@ export class BannerModel extends HoistModel {
18
18
  icon;
19
19
  message;
20
20
  intent;
21
+ sortOrder;
21
22
  className;
22
23
  enableClose;
23
24
  onClose;
24
25
  onClick;
25
26
  actionButtonProps;
26
- props;
27
27
 
28
- constructor({
29
- category = 'default',
30
- icon,
31
- message,
32
- intent = 'primary',
33
- className,
34
- enableClose = true,
35
- onClose,
36
- onClick,
37
- actionButtonProps,
38
- ...props
39
- }: BannerSpec) {
28
+ /**
29
+ * Sort order for Hoist-provided banners.
30
+ */
31
+ static BANNER_SORTS = {
32
+ APP_UPDATE: -2,
33
+ ADMIN_ALERT: -1
34
+ };
35
+
36
+ constructor(spec: BannerSpec) {
40
37
  super();
41
38
 
39
+ const {
40
+ category = 'default',
41
+ icon,
42
+ message,
43
+ intent = 'primary',
44
+ sortOrder,
45
+ className,
46
+ enableClose = true,
47
+ onClose,
48
+ onClick,
49
+ actionButtonProps
50
+ } = spec;
51
+
42
52
  this.category = category;
43
53
  this.icon = icon;
44
54
  this.message = message;
45
55
  this.intent = intent;
56
+ this.sortOrder = sortOrder;
46
57
  this.className = className;
47
58
  this.enableClose = enableClose;
48
59
  this.onClose = onClose;
49
60
  this.onClick = onClick;
50
61
  this.actionButtonProps = actionButtonProps;
51
- this.props = props;
52
62
  }
53
63
  }
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {XH, HoistModel, managed, BannerSpec} from '@xh/hoist/core';
8
8
  import {action, observable, makeObservable} from '@xh/hoist/mobx';
9
- import {find, reject} from 'lodash';
9
+ import {find, reject, sortBy, without, last} from 'lodash';
10
10
 
11
11
  import {BannerModel} from './BannerModel';
12
12
 
@@ -22,17 +22,29 @@ export class BannerSourceModel extends HoistModel {
22
22
  @observable.ref
23
23
  bannerModels: BannerModel[] = [];
24
24
 
25
- MAX_BANNERS = 4;
26
-
27
25
  constructor() {
28
26
  super();
29
27
  makeObservable(this);
30
28
  }
31
29
 
32
30
  @action
33
- show(config: BannerSpec): BannerModel {
34
- const ret = new BannerModel(config);
35
- this.addModel(ret);
31
+ show(spec: BannerSpec): BannerModel {
32
+ let {bannerModels} = this,
33
+ ret = new BannerModel(spec);
34
+
35
+ // Removes banner from new banner's category if it exists.
36
+ const existing = find(bannerModels, {category: ret.category});
37
+ if (existing) {
38
+ bannerModels = without(bannerModels, existing);
39
+ XH.safeDestroy(existing);
40
+ }
41
+
42
+ // Place in requested pos, existing pos, or last
43
+ const maxSortOrder = last(bannerModels)?.sortOrder ?? 0;
44
+ ret.sortOrder = spec.sortOrder ?? existing?.sortOrder ?? maxSortOrder + 1;
45
+ bannerModels = sortBy([...bannerModels, ret], 'sortOrder');
46
+
47
+ this.bannerModels = bannerModels;
36
48
  return ret;
37
49
  }
38
50
 
@@ -43,24 +55,7 @@ export class BannerSourceModel extends HoistModel {
43
55
  this.bannerModels = reject(this.bannerModels, {category});
44
56
  }
45
57
 
46
- //-----------------------------------
47
- // Implementation
48
- //------------------------------------
49
- @action
50
- addModel(model: BannerModel) {
51
- // Remove existing banner for category
52
- this.hide(model.category);
53
-
54
- // Add new banner, removing old banners if limit exceeded
55
- const models = [...this.bannerModels, model];
56
- while (models.length > this.MAX_BANNERS) {
57
- const bannerModel = models.shift();
58
- XH.safeDestroy(bannerModel);
59
- }
60
- this.bannerModels = models;
61
- }
62
-
63
- getBanner(category: string): BannerModel {
58
+ private getBanner(category: string): BannerModel {
64
59
  return find(this.bannerModels, {category});
65
60
  }
66
61
  }
@@ -84,6 +84,7 @@ export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
84
84
  item: createElement(AgGridReact, {
85
85
  // Default some ag-grid props, but allow overriding.
86
86
  getRowHeight: impl.getRowHeight,
87
+ suppressBrowserResizeObserver: true,
87
88
  // Pass others on directly.
88
89
  ...agGridProps,
89
90
 
package/cmp/grid/Grid.ts CHANGED
@@ -197,8 +197,8 @@ class GridLocalModel extends HoistModel {
197
197
  clipboardCopy: Icon.copy({asHtml: true})
198
198
  },
199
199
  components: {
200
- agColumnHeader: props => columnHeader(props),
201
- agColumnGroupHeader: props => columnGroupHeader(props)
200
+ agColumnHeader: props => columnHeader({...props, gridModel: model}),
201
+ agColumnGroupHeader: props => columnGroupHeader({...props, gridModel: model})
202
202
  },
203
203
  rowSelection: selModel.mode == 'disabled' ? undefined : selModel.mode,
204
204
  suppressRowClickSelection: !selModel.isEnabled,
@@ -703,7 +703,7 @@ export class Column {
703
703
  lockPinned: !gridModel.enableColumnPinning || XH.isMobileApp,
704
704
  pinned: this.pinned,
705
705
  lockVisible: !this.hideable || !gridModel.colChooserModel || XH.isMobileApp,
706
- headerComponentParams: {gridModel, xhColumn: this},
706
+ headerComponentParams: {xhColumn: this},
707
707
  suppressColumnsToolPanel: this.excludeFromChooser,
708
708
  suppressFiltersToolPanel: this.excludeFromChooser,
709
709
  enableCellChangeFlash: this.highlightOnChange,
@@ -43,8 +43,10 @@ import {
43
43
  } from 'react';
44
44
 
45
45
  /**
46
- * Configuration for creating a Component. May be specified either as a render function,
47
- * or an object containing a render function and associated metadata.
46
+ * Type representing props passed to a HoistComponent's render function.
47
+ *
48
+ * This type removes from its base type several props that are used by HoistComponent itself and
49
+ * not provided to the render function.
48
50
  */
49
51
  export type RenderPropsOf<P extends HoistProps> = P & {
50
52
  /** Pre-processed by HoistComponent internals into a mounted model. Never passed to render. */
@@ -52,15 +54,12 @@ export type RenderPropsOf<P extends HoistProps> = P & {
52
54
 
53
55
  /** Pre-processed by HoistComponent internals and attached to model. Never passed to render. */
54
56
  modelRef: never;
55
-
56
- /**
57
- * React Children. Populated on props by React internally, before rendering. Applications
58
- * will typically provide children to a component via JSX or the `item(s)` property passed to
59
- * an element factory.
60
- */
61
- children?: ReactNode;
62
57
  };
63
58
 
59
+ /**
60
+ * Configuration for creating a Component. May be specified either as a render function,
61
+ * or an object containing a render function and associated metadata.
62
+ */
64
63
  export type ComponentConfig<P extends HoistProps> =
65
64
  | ((props: RenderPropsOf<P>, ref?: ForwardedRef<any>) => ReactNode)
66
65
  | {
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {HoistModel} from '@xh/hoist/core/model';
8
- import {CSSProperties, HTMLAttributes, Ref} from 'react';
8
+ import {CSSProperties, HTMLAttributes, ReactNode, Ref} from 'react';
9
9
 
10
10
  /**
11
11
  * Props interface for Hoist Components.
@@ -39,6 +39,9 @@ export interface HoistProps<M extends HoistModel = HoistModel> {
39
39
  * any base class name provided by the component definition itself.
40
40
  */
41
41
  className?: string;
42
+
43
+ /** React children. */
44
+ children?: ReactNode;
42
45
  }
43
46
 
44
47
  /**
package/core/XH.ts CHANGED
@@ -453,7 +453,7 @@ export class XHApi {
453
453
  * @returns true if user confirms, false if user cancels. If an input is provided, the
454
454
  * Promise will resolve to the input value if user confirms.
455
455
  */
456
- message(config: MessageSpec): Promise<any> {
456
+ message<T = unknown>(config: MessageSpec): Promise<T | boolean> {
457
457
  return this.acm.messageSourceModel.message(config);
458
458
  }
459
459
 
@@ -484,7 +484,7 @@ export class XHApi {
484
484
  *
485
485
  * @returns value of input if user confirms, false if user cancels.
486
486
  */
487
- prompt(config: MessageSpec): Promise<any> {
487
+ prompt<T = unknown>(config: MessageSpec): Promise<T | false> {
488
488
  return this.acm.messageSourceModel.prompt(config);
489
489
  }
490
490
 
@@ -524,9 +524,9 @@ export class XHApi {
524
524
  * Show a Banner across the top of the viewport. Banners are unique by their
525
525
  * category prop - showing a new banner with an existing category will replace it.
526
526
  */
527
- showBanner(config: BannerSpec | string): BannerModel {
528
- if (isString(config)) config = {message: config};
529
- return this.acm.bannerSourceModel.show(config);
527
+ showBanner(spec: BannerSpec | string): BannerModel {
528
+ if (isString(spec)) spec = {message: spec};
529
+ return this.acm.bannerSourceModel.show(spec);
530
530
  }
531
531
 
532
532
  /**
@@ -67,7 +67,7 @@ export function useModelLinker(model: HoistModel, modelLookup: ModelLookup, prop
67
67
  if (isLinking) {
68
68
  model._modelLookup = modelLookup;
69
69
  each(model['_xhInjectedParentProperties'], (selector, name) => {
70
- const parentModel = modelLookup.lookupModel(selector);
70
+ const parentModel = modelLookup?.lookupModel(selector);
71
71
  if (!parentModel) {
72
72
  throw XH.exception(
73
73
  `Failed to resolve @lookup for property '${name}' with selector ${formatSelector(
@@ -81,7 +81,7 @@ export function useModelLinker(model: HoistModel, modelLookup: ModelLookup, prop
81
81
 
82
82
  // Linked models with an impl parent that are not explicitly marked should be marked as impl.
83
83
  if (isUndefined(model.xhImpl)) {
84
- const parentModel = modelLookup.lookupModel('*');
84
+ const parentModel = modelLookup?.lookupModel('*');
85
85
  if (parentModel?.xhImpl === true) {
86
86
  model.xhImpl = true;
87
87
  }
@@ -134,7 +134,6 @@ export interface MessageSpec {
134
134
 
135
135
  /**
136
136
  * Configuration object for an app-wide banner.
137
- * Additional properties passed to this object will be passed directly to the banner component.
138
137
  */
139
138
  export interface BannerSpec {
140
139
  message?: ReactNode;
@@ -142,7 +141,17 @@ export interface BannerSpec {
142
141
  intent?: Intent;
143
142
  className?: string;
144
143
 
145
- /** The category for the banner. Defaults to 'default'.*/
144
+ /**
145
+ * Determines order in which banner will be displayed.
146
+ * If not provided, banner will be placed below any existing banners.
147
+ * @see BannerModel.BANNER_SORTS
148
+ */
149
+ sortOrder?: number;
150
+
151
+ /**
152
+ * Showing a banner with a given category will hide any
153
+ * preexisting banner with the same category.
154
+ */
146
155
  category?: string;
147
156
 
148
157
  /**
@@ -19,6 +19,7 @@ import {
19
19
  } from 'lodash';
20
20
  import {parseFieldValue} from '../Field';
21
21
  import {Store} from '../Store';
22
+ import {StoreRecord} from '../StoreRecord';
22
23
  import {Filter} from './Filter';
23
24
  import {FieldFilterOperator, FieldFilterSpec, FilterTestFn} from './Types';
24
25
 
@@ -111,91 +112,76 @@ export class FieldFilter extends Filter {
111
112
  ? value.map(v => parseFieldValue(v, fieldType))
112
113
  : parseFieldValue(value, fieldType);
113
114
  }
114
- const getVal = store ? r => r.committedData[field] : r => r[field],
115
- doNotFilter = r => store && isNil(r.committedData); // Ignore (do not filter out) record if part of a store and it has no committed data
116
115
 
117
116
  if (FieldFilter.ARRAY_OPERATORS.includes(op)) {
118
117
  value = castArray(value);
119
118
  }
120
119
 
120
+ let opFn: (v: any) => boolean;
121
121
  switch (op) {
122
122
  case '=':
123
- return r => {
124
- if (doNotFilter(r)) return true;
125
- let v = getVal(r);
123
+ opFn = v => {
126
124
  if (isNil(v) || v === '') v = null;
127
125
  return value.includes(v);
128
126
  };
127
+ break;
129
128
  case '!=':
130
- return r => {
131
- if (doNotFilter(r)) return true;
132
- let v = getVal(r);
129
+ opFn = v => {
133
130
  if (isNil(v) || v === '') v = null;
134
131
  return !value.includes(v);
135
132
  };
133
+ break;
136
134
  case '>':
137
- return r => {
138
- if (doNotFilter(r)) return true;
139
- const v = getVal(r);
140
- return !isNil(v) && v > value;
141
- };
135
+ opFn = v => !isNil(v) && v > value;
136
+ break;
142
137
  case '>=':
143
- return r => {
144
- if (doNotFilter(r)) return true;
145
- const v = getVal(r);
146
- return !isNil(v) && v >= value;
147
- };
138
+ opFn = v => !isNil(v) && v >= value;
139
+ break;
148
140
  case '<':
149
- return r => {
150
- if (doNotFilter(r)) return true;
151
- const v = getVal(r);
152
- return !isNil(v) && v < value;
153
- };
141
+ opFn = v => !isNil(v) && v < value;
142
+ break;
154
143
  case '<=':
155
- return r => {
156
- if (doNotFilter(r)) return true;
157
- const v = getVal(r);
158
- return !isNil(v) && v <= value;
159
- };
144
+ opFn = v => !isNil(v) && v <= value;
145
+ break;
160
146
  case 'like':
161
147
  regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
162
- return r => {
163
- if (doNotFilter(r)) return true;
164
- return regExps.some(re => re.test(getVal(r)));
165
- };
148
+ opFn = v => regExps.some(re => re.test(v));
149
+ break;
166
150
  case 'not like':
167
151
  regExps = value.map(v => new RegExp(escapeRegExp(v), 'i'));
168
- return r => {
169
- if (doNotFilter(r)) return true;
170
- return regExps.every(re => !re.test(getVal(r)));
171
- };
152
+ opFn = v => regExps.every(re => !re.test(v));
153
+ break;
172
154
  case 'begins':
173
155
  regExps = value.map(v => new RegExp('^' + escapeRegExp(v), 'i'));
174
- return r => {
175
- if (doNotFilter(r)) return true;
176
- return regExps.some(re => re.test(getVal(r)));
177
- };
156
+ opFn = v => regExps.some(re => re.test(v));
157
+ break;
178
158
  case 'ends':
179
159
  regExps = value.map(v => new RegExp(escapeRegExp(v) + '$', 'i'));
180
- return r => {
181
- if (doNotFilter(r)) return true;
182
- return regExps.some(re => re.test(getVal(r)));
183
- };
160
+ opFn = v => regExps.some(re => re.test(v));
161
+ break;
184
162
  case 'includes':
185
- return r => {
186
- if (doNotFilter(r)) return true;
187
- const v = getVal(r);
188
- return !isNil(v) && v.some(it => value.includes(it));
189
- };
163
+ opFn = v => !isNil(v) && v.some(it => value.includes(it));
164
+ break;
190
165
  case 'excludes':
191
- return r => {
192
- if (doNotFilter(r)) return true;
193
- const v = getVal(r);
194
- return isNil(v) || !v.some(it => value.includes(it));
195
- };
166
+ opFn = v => isNil(v) || !v.some(it => value.includes(it));
167
+ break;
196
168
  default:
197
169
  throw XH.exception(`Unknown operator: ${op}`);
198
170
  }
171
+
172
+ if (!store) return r => opFn(r[field]);
173
+
174
+ return (r: StoreRecord) => {
175
+ const val = r.get(field);
176
+ if (opFn(val)) return true;
177
+
178
+ // Maximize chances of matching. Always pass adds ...
179
+ if (r.isAdd) return true;
180
+
181
+ // ... and check any differing original value as well
182
+ const committedVal = r.committedData[field];
183
+ return committedVal !== val && opFn(committedVal);
184
+ };
199
185
  }
200
186
 
201
187
  override equals(other: Filter): boolean {
@@ -5,8 +5,6 @@
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
7
  body.xh-app .xh-banner {
8
- height: var(--xh-banner-height-px);
9
-
10
8
  &__click_target {
11
9
  &--clickable {
12
10
  cursor: pointer;
@@ -25,7 +25,7 @@ export const banner = hoistCmp.factory({
25
25
  model: uses(BannerModel),
26
26
 
27
27
  render({model}) {
28
- const {icon, message, intent, onClick, className, props} = model;
28
+ const {icon, message, intent, onClick, className} = model;
29
29
 
30
30
  return toolbar({
31
31
  className: classNames(
@@ -50,8 +50,7 @@ export const banner = hoistCmp.factory({
50
50
  }),
51
51
  actionButton(),
52
52
  dismissButton()
53
- ],
54
- ...props
53
+ ]
55
54
  });
56
55
  }
57
56
  });
@@ -299,7 +299,7 @@ export class DashCanvasModel extends DashModel<
299
299
 
300
300
  if (!allowRename) return;
301
301
 
302
- XH.prompt({
302
+ XH.prompt<string>({
303
303
  message: `Rename '${view.title}' to`,
304
304
  title: 'Rename...',
305
305
  icon: Icon.edit(),
@@ -6,8 +6,8 @@
6
6
  */
7
7
  .xh-app {
8
8
  .xh-column-header-filter {
9
- width: 240px;
10
- height: 350px;
9
+ height: var(--xh-grid-filter-popover-height-px);
10
+ width: var(--xh-grid-filter-popover-width-px);
11
11
 
12
12
  &__icon {
13
13
  flex: none !important;
@@ -98,7 +98,9 @@ rename with a `.ts` extension to ensure you have at least one TS file in your bu
98
98
  In that same file, add a declaration statement to let TS know about any of your application
99
99
  services (`HoistService` instances) that you are initializing. Those are installed on and referenced
100
100
  from the `XH` object; for TS to consider references to those services valid, it needs to know that
101
- the type of the `XH` singleton (`XHApi`) has a property for each of your services.
101
+ the type of the `XH` singleton (`XHApi`) has a property for each of your services. For users of
102
+ IntelliJ, an ignored re-declaration of the XH singleton with this interface helps the IDE properly
103
+ notice uses of these services.
102
104
 
103
105
  In Toolbox, we have the following within `Bootstrap.ts` to declare five TB-specific services (your
104
106
  services will vary of course) and a custom property installed on `HoistUser`, the type returned
@@ -106,6 +108,7 @@ by `XH.getUser()`:
106
108
 
107
109
  ```typescript
108
110
  declare module '@xh/hoist/core' {
111
+ // Merge interface with XHApi class to include injected services.
109
112
  export interface XHApi {
110
113
  contactService: ContactService;
111
114
  gitHubService: GitHubService;
@@ -113,6 +116,8 @@ declare module '@xh/hoist/core' {
113
116
  portfolioService: PortfolioService;
114
117
  taskService: TaskService;
115
118
  }
119
+ // @ts-ignore - Help IntelliJ recognize uses of injected service methods from the `XH` singleton.
120
+ export const XH: XHApi;
116
121
 
117
122
  export interface HoistUser {
118
123
  profilePicUrl: string;
@@ -8,7 +8,7 @@ body.xh-app .xh-banner {
8
8
  padding: var(--xh-pad-half-px) var(--xh-pad-half-px) var(--xh-pad-half-px) var(--xh-pad-px);
9
9
  display: flex;
10
10
  align-items: center;
11
- height: var(--xh-banner-height-px);
11
+ min-height: 40px;
12
12
 
13
13
  &__click_target > .xh-icon {
14
14
  margin-right: var(--xh-pad-px);
@@ -23,7 +23,7 @@ export const banner = hoistCmp.factory({
23
23
  displayName: 'Banner',
24
24
  model: uses(BannerModel),
25
25
  render({model}) {
26
- const {icon, message, intent, onClick, className, props} = model;
26
+ const {icon, message, intent, onClick, className} = model;
27
27
 
28
28
  return div({
29
29
  className: classNames(
@@ -48,8 +48,7 @@ export const banner = hoistCmp.factory({
48
48
  }),
49
49
  actionButton(),
50
50
  dismissButton()
51
- ],
52
- ...props
51
+ ]
53
52
  });
54
53
  }
55
54
  });
@@ -51,7 +51,7 @@ export const exceptionDialogDetails = hoistCmp.factory({
51
51
  icon: Icon.envelope(),
52
52
  text: 'Send Message',
53
53
  onClick: () => {
54
- XH.prompt({
54
+ XH.prompt<string>({
55
55
  title: 'Send Message',
56
56
  message: null,
57
57
  input: {
@@ -63,8 +63,8 @@ export const exceptionDialogDetails = hoistCmp.factory({
63
63
  },
64
64
  confirmProps: {icon: Icon.envelope(), text: 'Send'}
65
65
  }).then(userMessage => {
66
- model.userMessage = userMessage;
67
- if (model.userMessage) {
66
+ if (userMessage) {
67
+ model.userMessage = userMessage;
68
68
  model.sendReportAsync();
69
69
  }
70
70
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "56.2.0",
3
+ "version": "56.4.0",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",
@@ -45,7 +45,7 @@
45
45
  "classnames": "~2.3.2",
46
46
  "clipboard-copy": "~4.0.1",
47
47
  "codemirror": "~5.65.0",
48
- "core-js": "^3.29.1",
48
+ "core-js": "^3.0",
49
49
  "debounce-promise": "~3.1.0",
50
50
  "dompurify": "~3.0.1",
51
51
  "downloadjs": "~1.4.7",
@@ -90,15 +90,15 @@
90
90
  "@xh/hoist-dev-utils": "^6.1.2",
91
91
  "eslint-config-prettier": "8.x",
92
92
  "eslint-plugin-tsdoc": "^0.2.17",
93
- "husky": "^8.0.2",
94
- "lint-staged": "^13.0.3",
95
- "postcss": "^8.4.19",
96
- "prettier": "^2.8.4",
93
+ "husky": "^8.0.3",
94
+ "lint-staged": "^13.2.2",
95
+ "postcss": "^8.4.23",
96
+ "prettier": "^2.8.8",
97
97
  "react": "^18.2.0",
98
98
  "react-dom": "^18.2.0",
99
- "stylelint": "^15.3.0",
99
+ "stylelint": "^15.6.1",
100
100
  "stylelint-config-standard-scss": "^7.0.1",
101
- "type-fest": "^3.6.1",
101
+ "type-fest": "^3.10.0",
102
102
  "typescript": "~4.9.5"
103
103
  },
104
104
  "resolutions": {
package/styles/vars.scss CHANGED
@@ -502,6 +502,10 @@ body {
502
502
  --xh-grid-multifield-line-height: var(--grid-multifield-line-height, 14);
503
503
  --xh-grid-multifield-line-height-px: calc(var(--xh-grid-multifield-line-height) * 1px);
504
504
 
505
+ // Grid column-header-based filter popover (desktop only)
506
+ --xh-grid-filter-popover-height-px: var(--grid-filter-popover-height-px, 350px);
507
+ --xh-grid-filter-popover-width-px: var(--grid-filter-popover-width-px, 240px);
508
+
505
509
  // Dark Grid
506
510
  &.xh-dark {
507
511
  --xh-grid-bg-hover: var(--grid-bg-hover, hsl(200, 50%, 22%));
@@ -664,13 +668,6 @@ body {
664
668
  }
665
669
 
666
670
 
667
- //------------------------
668
- // Banners
669
- //------------------------
670
- --xh-banner-height: var(--banner-height, 40);
671
- --xh-banner-height-px: calc(var(--xh-banner-height) * 1px);
672
-
673
-
674
671
  //------------------------
675
672
  // Tabs
676
673
  //------------------------
@@ -4,6 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {BannerModel} from '@xh/hoist/appcontainer/BannerModel';
7
8
  import {div, p} from '@xh/hoist/cmp/layout';
8
9
  import {BannerSpec, HoistService, Intent, managed, XH} from '@xh/hoist/core';
9
10
  import {Icon} from '@xh/hoist/icon';
@@ -96,6 +97,7 @@ export class AlertBannerService extends HoistService {
96
97
  intent,
97
98
  icon,
98
99
  enableClose,
100
+ sortOrder: BannerModel.BANNER_SORTS.ADMIN_ALERT,
99
101
  actionButtonProps,
100
102
  onClick
101
103
  };
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Copyright © 2022 Extremely Heavy Industries Inc.
6
6
  */
7
- import {HoistService, XH, Exception, PlainObject, Thunkable} from '@xh/hoist/core';
7
+ import {HoistService, XH, Exception, PlainObject, Thunkable, FetchResponse} from '@xh/hoist/core';
8
8
  import {isLocalDate, SECONDS, ONE_MINUTE, olderThan} from '@xh/hoist/utils/datetime';
9
9
  import {throwIf} from '@xh/hoist/utils/js';
10
10
  import {StatusCodes} from 'http-status-codes';
@@ -61,10 +61,8 @@ export class FetchService extends HoistService {
61
61
  * Send a request via the underlying fetch API.
62
62
  * @returns Promise which resolves to a Fetch Response.
63
63
  */
64
- fetch(opts: FetchOptions): Promise<any> {
65
- return this.managedFetchAsync(opts, aborter =>
66
- this.fetchInternalAsync(opts, aborter)
67
- ) as any;
64
+ fetch(opts: FetchOptions): Promise<FetchResponse> {
65
+ return this.managedFetchAsync(opts, aborter => this.fetchInternalAsync(opts, aborter));
68
66
  }
69
67
 
70
68
  /**
@@ -81,7 +79,7 @@ export class FetchService extends HoistService {
81
79
  aborter
82
80
  );
83
81
  return this.NO_JSON_RESPONSES.includes(r.status) ? null : r.json();
84
- }) as any;
82
+ });
85
83
  }
86
84
 
87
85
  /**
@@ -142,7 +140,10 @@ export class FetchService extends HoistService {
142
140
  //-----------------------
143
141
  // Implementation
144
142
  //-----------------------
145
- private async managedFetchAsync(opts: FetchOptions, fn: (ctl: AbortController) => any) {
143
+ private async managedFetchAsync(
144
+ opts: FetchOptions,
145
+ fn: (ctl: AbortController) => Promise<FetchResponse>
146
+ ): Promise<FetchResponse> {
146
147
  const {autoAborters, defaultTimeout} = this,
147
148
  {autoAbortKey, timeout = defaultTimeout} = opts,
148
149
  aborter = new AbortController();
@@ -177,7 +178,7 @@ export class FetchService extends HoistService {
177
178
  }
178
179
  }
179
180
 
180
- private async fetchInternalAsync(opts, aborter): Promise<any> {
181
+ private async fetchInternalAsync(opts, aborter): Promise<FetchResponse> {
181
182
  const {defaultHeaders} = this;
182
183
  let {url, method, headers, body, params} = opts;
183
184
  throwIf(!url, 'No url specified in call to fetchService.');
@@ -236,7 +237,7 @@ export class FetchService extends HoistService {
236
237
  }
237
238
  }
238
239
 
239
- const ret: any = await fetch(url, fetchOpts);
240
+ const ret = (await fetch(url, fetchOpts)) as FetchResponse;
240
241
 
241
242
  if (!ret.ok) {
242
243
  ret.responseText = await this.safeResponseTextAsync(ret);
@@ -300,8 +301,11 @@ export interface FetchOptions {
300
301
  /** URL for the request. Relative urls will be appended to XH.baseUrl. */
301
302
  url: string;
302
303
 
303
- /** Data to send in the request body (for POSTs/PUTs of JSON).*/
304
- body?: PlainObject;
304
+ /**
305
+ * Data to send in the request body (for POSTs/PUTs of JSON).
306
+ * When using `fetch`, provide a string. Otherwise, provide a PlainObject.
307
+ */
308
+ body?: PlainObject | string;
305
309
 
306
310
  /**
307
311
  * Parameters to encode and append as a query string, or send with the request body
@@ -13,7 +13,7 @@ import {fmtNumber} from '@xh/hoist/format';
13
13
  export function fmtTimeZone(name: string, offset: number): string {
14
14
  if (!name) return '';
15
15
 
16
- return name !== 'GMT'
17
- ? `${name} (GMT${fmtNumber(offset / HOURS, {withPlusSign: true, asHtml: true})})`
18
- : `${name}`;
16
+ return name === 'GMT' || name === 'UTC'
17
+ ? name
18
+ : `${name} (GMT${fmtNumber(offset / HOURS, {withPlusSign: true, asHtml: true})})`;
19
19
  }