@xh/hoist 59.1.0 → 59.3.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 (117) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/admin/AppComponent.ts +1 -1
  3. package/admin/tabs/activity/ActivityTab.ts +1 -1
  4. package/admin/tabs/general/GeneralTab.ts +2 -2
  5. package/admin/tabs/general/alertBanner/AlertBannerModel.ts +2 -2
  6. package/admin/tabs/general/config/ConfigPanel.ts +1 -0
  7. package/admin/tabs/monitor/MonitorTab.ts +1 -1
  8. package/admin/tabs/server/ServerTab.ts +1 -1
  9. package/admin/tabs/userData/UserDataTab.ts +1 -1
  10. package/cmp/ag-grid/AgGrid.scss +60 -12
  11. package/cmp/ag-grid/AgGrid.ts +8 -2
  12. package/cmp/badge/Badge.ts +18 -5
  13. package/cmp/chart/Chart.ts +13 -11
  14. package/cmp/clock/Clock.ts +6 -5
  15. package/cmp/dataview/DataView.ts +5 -3
  16. package/cmp/form/Form.ts +25 -6
  17. package/cmp/grid/Grid.scss +10 -0
  18. package/cmp/grid/Grid.ts +41 -25
  19. package/cmp/grid/GridModel.ts +138 -34
  20. package/cmp/grid/Types.ts +1 -1
  21. package/cmp/grid/columns/Column.ts +45 -2
  22. package/cmp/grid/columns/ColumnGroup.ts +11 -0
  23. package/cmp/grid/impl/GridHScrollbar.ts +140 -0
  24. package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
  25. package/cmp/input/HoistInputModel.ts +4 -4
  26. package/cmp/input/HoistInputProps.ts +3 -1
  27. package/cmp/layout/Box.ts +4 -2
  28. package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
  29. package/cmp/store/StoreFilterField.ts +2 -2
  30. package/cmp/tab/TabContainer.ts +1 -1
  31. package/cmp/zoneGrid/Types.ts +47 -0
  32. package/cmp/zoneGrid/ZoneGrid.ts +62 -0
  33. package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
  34. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
  35. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
  36. package/cmp/zoneGrid/index.ts +3 -0
  37. package/core/HoistComponent.ts +23 -10
  38. package/core/HoistProps.ts +25 -6
  39. package/core/XH.ts +49 -27
  40. package/core/elem.ts +11 -3
  41. package/core/impl/InstanceManager.ts +24 -1
  42. package/core/model/HoistModel.ts +4 -4
  43. package/data/RecordAction.ts +8 -5
  44. package/data/StoreRecord.ts +8 -1
  45. package/data/cube/Query.ts +37 -3
  46. package/data/cube/View.ts +2 -2
  47. package/data/cube/row/BaseRow.ts +2 -2
  48. package/desktop/appcontainer/AppContainer.ts +2 -0
  49. package/desktop/appcontainer/Banner.ts +2 -2
  50. package/desktop/cmp/appbar/AppBar.ts +8 -6
  51. package/desktop/cmp/button/Button.ts +14 -3
  52. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  53. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  54. package/desktop/cmp/button/index.ts +1 -0
  55. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  56. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  57. package/desktop/cmp/dash/container/DashContainerModel.ts +3 -2
  58. package/desktop/cmp/dash/container/impl/DashContainerUtils.ts +4 -4
  59. package/desktop/cmp/dock/DockViewModel.ts +10 -8
  60. package/desktop/cmp/dock/impl/DockContainer.ts +1 -0
  61. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  62. package/desktop/cmp/form/FormField.ts +34 -10
  63. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  64. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  65. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  66. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  67. package/desktop/cmp/input/Checkbox.ts +3 -3
  68. package/desktop/cmp/input/CodeInput.ts +2 -1
  69. package/desktop/cmp/input/DateInput.ts +128 -123
  70. package/desktop/cmp/input/JsonInput.ts +1 -1
  71. package/desktop/cmp/input/NumberInput.ts +3 -2
  72. package/desktop/cmp/input/RadioInput.ts +3 -1
  73. package/desktop/cmp/input/Select.ts +31 -4
  74. package/desktop/cmp/input/SwitchInput.ts +2 -1
  75. package/desktop/cmp/input/TextArea.ts +3 -3
  76. package/desktop/cmp/input/TextInput.ts +51 -47
  77. package/desktop/cmp/panel/Panel.ts +21 -19
  78. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  79. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  80. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  81. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  82. package/desktop/cmp/rest/Actions.ts +10 -5
  83. package/desktop/cmp/rest/RestGrid.ts +20 -6
  84. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  85. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  86. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  87. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  88. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  89. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  90. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  91. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  92. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  93. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  94. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  95. package/dynamics/desktop.ts +2 -0
  96. package/dynamics/mobile.ts +2 -0
  97. package/inspector/instances/InstancesModel.ts +3 -3
  98. package/mobile/appcontainer/AppContainer.ts +2 -0
  99. package/mobile/appcontainer/Banner.ts +2 -2
  100. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  101. package/mobile/cmp/button/index.ts +1 -0
  102. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  103. package/mobile/cmp/input/Select.scss +1 -0
  104. package/mobile/cmp/input/Select.ts +7 -0
  105. package/mobile/cmp/input/TextInput.ts +1 -0
  106. package/mobile/cmp/menu/impl/Menu.scss +7 -1
  107. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  108. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  109. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  110. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  111. package/package.json +4 -3
  112. package/styles/vars.scss +3 -3
  113. package/svc/InspectorService.ts +1 -1
  114. package/utils/js/DomUtils.ts +10 -0
  115. package/utils/js/LangUtils.ts +10 -0
  116. package/utils/js/TestUtils.ts +9 -0
  117. package/utils/js/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 59.3.0 - 2023-11-09
4
+
5
+ ### 🎁 New Features
6
+
7
+ * Improved Hoist support for automated testing via Playwright, Cypress, and similar tools:
8
+ * Core Hoist components now accept an optional `testId` prop, to be rendered at an appropriate
9
+ level of the DOM (within a `data-testid` HTML attribute). This can minimize the need to select
10
+ components using criteria such as CSS classes or labels that are more likely to change and
11
+ break tests.
12
+ * When given a `testId`, certain composite components will generate and set "sub-testIds" on
13
+ selected internal components. For example, a `TabContainer` will set a testId on each switcher
14
+ button (derived from its tabId), and a `Form` will set testIds on nested `FormField`
15
+ and `HoistInput` components (derived from their bound field names).
16
+ * This release represents a first step in ongoing work to facilitate automated end-to-end
17
+ testing of Hoist applications. Additional Hoist-specific utilities for writing tests in
18
+ libraries such as Cypress and Playwright are coming soon.
19
+ * Added new `ZoneGrid` component, a highly specialized `Grid` that always displays its data with
20
+ multi-line, full-width rows. Each row is broken into four zones (top/bottom and left/right),
21
+ each of which can mapped by the user to render data from one or more fields.
22
+ * Primarily intended for mobile, where horizontal scrolling can present usability issues, but
23
+ also available on desktop, where it can serve as an easily user-configurable `DataView`.
24
+ * Added `Column.sortToBottom` to force specified values to sort the bottom, regardless of sort
25
+ direction. Intended primarily to force null values to sort below all others.
26
+ * Upgraded the `RelativeTimestamp` component with a new `localDateMode` option to customize how
27
+ near-term date/time differences are rendered with regards to calendar days.
28
+
29
+ ### 🐞 Bug Fixes
30
+
31
+ * Fixed bug where interacting with a `Select` within a `Popover` can inadvertently cause the
32
+ popover to close. If your app already has special handling in place to prevent this, you should
33
+ be able to unwind it after upgrading.
34
+ * Improved the behavior of the clear button in `TextInput`. Clearing a field no longer drops focus,
35
+ allowing the user to immediately begin typing in a new value.
36
+ * Fixed arguments passed to `ErrorMessageProps.actionFn` and `ErrorMessageProps.detailsFn`.
37
+ * Improved default error text in `ErrorMessage`.
38
+
39
+ ### ⚙️ Technical
40
+
41
+ * Improved core `HoistComponent` performance by preventing unnecessary re-renderings triggered by
42
+ spurious model lookup changes.
43
+ * New flag `GridModel.experimental.enableFullWidthScroll` enables scrollbars to span pinned columns.
44
+ * Early test release behind the flag, expected to made the default behavior in next release.
45
+ * Renamed `XH.getActiveModels()` to `XH.getModels()` for clarity / consistency.
46
+ * API change, but not expected to impact applications.
47
+ * Added `XH.getModel()` convenience method to return the first matching model.
48
+
49
+ ## 59.2.0 - 2023-10-16
50
+
51
+ ### 🎁 New Features
52
+
53
+ * New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view.
54
+ * Added `GridModel` APIs to lookup and show / hide entire column groups.
55
+ * Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default, controllable
56
+ with new `ColumnGroupSpec.borders` config.
57
+ * Enhanced the `CubeQuery` to support per-query post-processing functions
58
+ with `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default to their
59
+ respective properties on `Cube`.
60
+
61
+ ### 🐞 Bug Fixes
62
+
63
+ * `DashContainerModel` fixes:
64
+ * Fix bug where `addView` would throw when adding a view to a row or column
65
+ * Fix bug where `allowRemove` flag was dropped from state for containers
66
+ * Fix bug in `DockContainer` where adding / removing views would cause other views to be
67
+ remounted
68
+ * Fixed erroneous `GridModel` warning when using a tree column within a column group
69
+ * Fixed regression to alert banners. Resume allowing elements as messages.
70
+ * Fix `Grid` cell border styling inconsistencies.
71
+
72
+ ### ⚙️ Typescript API Adjustments
73
+
74
+ * Added type for `ActionFnData.record`.
75
+
3
76
  ## 59.1.0 - 2023-09-20
4
77
 
5
78
  ### 🎁 New Features
@@ -24,7 +97,8 @@
24
97
  * Improved styling for disabled `checkbox` inputs.
25
98
 
26
99
  ### ⚙️ Technical
27
- * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
100
+
101
+ * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
28
102
 
29
103
  ### 📚 Libraries
30
104
 
@@ -30,7 +30,7 @@ export const AppComponent = hoistCmp({
30
30
  const tbar = hoistCmp.factory<AppModel>(({model}) =>
31
31
  appBar({
32
32
  icon: Icon.gears({size: '2x', prefix: 'fal'}),
33
- leftItems: [tabSwitcher({enableOverflow: true})],
33
+ leftItems: [tabSwitcher({testId: 'tab-switcher', enableOverflow: true})],
34
34
  rightItems: [
35
35
  button({
36
36
  icon: Icon.openExternal(),
@@ -16,7 +16,7 @@ export const activityTab = hoistCmp.factory(() =>
16
16
  tabContainer({
17
17
  modelConfig: {
18
18
  route: 'default.activity',
19
- switcher: {orientation: 'left'},
19
+ switcher: {orientation: 'left', testId: 'activity-tab-switcher'},
20
20
  tabs: [
21
21
  {id: 'tracking', icon: Icon.analytics(), content: activityTrackingPanel},
22
22
  {id: 'clientErrors', icon: Icon.warning(), content: clientErrorsPanel},
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {configPanel} from '@xh/hoist/admin/tabs/general/config/ConfigPanel';
8
8
  import {tabContainer} from '@xh/hoist/cmp/tab';
9
- import {XH, hoistCmp} from '@xh/hoist/core';
9
+ import {hoistCmp, XH} from '@xh/hoist/core';
10
10
  import {Icon} from '@xh/hoist/icon';
11
11
  import {aboutPanel} from './about/AboutPanel';
12
12
  import {alertBannerPanel} from './alertBanner/AlertBannerPanel';
@@ -16,7 +16,7 @@ export const generalTab = hoistCmp.factory(() =>
16
16
  tabContainer({
17
17
  modelConfig: {
18
18
  route: 'default.general',
19
- switcher: {orientation: 'left'},
19
+ switcher: {orientation: 'left', testId: 'general-tab-switcher'},
20
20
  tabs: [
21
21
  {id: 'about', icon: Icon.info(), content: aboutPanel},
22
22
  {id: 'config', icon: Icon.settings(), content: configPanel},
@@ -11,7 +11,7 @@ import {HoistModel, LoadSpec, managed, XH, Intent, PlainObject} from '@xh/hoist/
11
11
  import {dateIs, required} from '@xh/hoist/data';
12
12
  import {action, makeObservable, observable} from '@xh/hoist/mobx';
13
13
  import {AppModel} from '@xh/hoist/admin/AppModel';
14
- import _, {sortBy, without} from 'lodash';
14
+ import {some, sortBy, without} from 'lodash';
15
15
  import {computed} from 'mobx';
16
16
 
17
17
  export class AlertBannerModel extends HoistModel {
@@ -151,7 +151,7 @@ export class AlertBannerModel extends HoistModel {
151
151
  @computed
152
152
  get currentValuesSavedAsPreset() {
153
153
  const {message, intent, iconName, enableClose} = this.formModel.values;
154
- return _(this.savedPresets).some({message, intent, iconName, enableClose});
154
+ return some(this.savedPresets, {message, intent, iconName, enableClose});
155
155
  }
156
156
 
157
157
  async loadPresetsAsync() {
@@ -19,6 +19,7 @@ export const configPanel = hoistCmp.factory({
19
19
  render({model}) {
20
20
  return fragment(
21
21
  restGrid({
22
+ testId: 'config',
22
23
  extraToolbarItems: () => {
23
24
  return button({
24
25
  icon: Icon.diff(),
@@ -17,7 +17,7 @@ export const monitorTab = hoistCmp.factory(() => {
17
17
  ? tabContainer({
18
18
  modelConfig: {
19
19
  route: 'default.monitor',
20
- switcher: {orientation: 'left'},
20
+ switcher: {orientation: 'left', testId: 'monitor-tab-switcher'},
21
21
  tabs: [
22
22
  {id: 'status', icon: Icon.shieldCheck(), content: monitorResultsPanel},
23
23
  {id: 'config', icon: Icon.settings(), content: monitorEditorPanel}
@@ -20,7 +20,7 @@ export const serverTab = hoistCmp.factory(() =>
20
20
  tabContainer({
21
21
  modelConfig: {
22
22
  route: 'default.server',
23
- switcher: {orientation: 'left'},
23
+ switcher: {orientation: 'left', testId: 'server-tab-switcher'},
24
24
  tabs: [
25
25
  {id: 'logViewer', icon: Icon.fileText(), content: logViewer},
26
26
  {id: 'logLevels', icon: Icon.settings(), content: logLevelPanel},
@@ -15,7 +15,7 @@ export const userDataTab = hoistCmp.factory(() =>
15
15
  tabContainer({
16
16
  modelConfig: {
17
17
  route: 'default.userData',
18
- switcher: {orientation: 'left'},
18
+ switcher: {orientation: 'left', testId: 'user-data-tab-switcher'},
19
19
  tabs: [
20
20
  {
21
21
  id: 'prefs',
@@ -5,6 +5,14 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
+ @mixin group-border($side) {
9
+ border-#{$side}: var(--xh-border-solid);
10
+ }
11
+
12
+ @mixin pinned-border($side) {
13
+ border-#{$side}: 1px solid var(--xh-grid-pinned-column-border-color);
14
+ }
15
+
8
16
  // Ag-grid installs an outer div around itself.
9
17
  .xh-ag-grid > div {
10
18
  width: 100%;
@@ -120,14 +128,17 @@
120
128
  }
121
129
 
122
130
  &--no-row-borders {
123
- .ag-row {
124
- border-color: transparent;
131
+ .ag-row,
132
+ .ag-cell {
133
+ border-bottom: none;
134
+ border-top: none;
135
+ }
125
136
 
126
- // Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
127
- // Without this collapsed groups (which don't stripe) blend together in a solid block.
128
- &.ag-row-group.ag-full-width-row {
129
- border-color: var(--xh-grid-group-border-color);
130
- }
137
+ // Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
138
+ // Without this collapsed groups (which don't stripe) blend together in a solid block.
139
+ .ag-row.ag-row-group.ag-full-width-row {
140
+ border-bottom: 1px solid var(--xh-grid-group-border-color);
141
+ border-top: 1px solid var(--xh-grid-group-border-color);
131
142
  }
132
143
  }
133
144
 
@@ -187,8 +198,14 @@
187
198
  }
188
199
  }
189
200
 
190
- .ag-cell.ag-cell-last-left-pinned:not(.ag-cell-focus) {
191
- border-right: 1px solid var(--xh-grid-pinned-column-border-color);
201
+ .ag-cell {
202
+ &.ag-cell-last-left-pinned:not(.ag-cell-focus) {
203
+ @include pinned-border(right);
204
+ }
205
+
206
+ &.ag-cell-first-right-pinned:not(.ag-cell-focus) {
207
+ @include pinned-border(left);
208
+ }
192
209
  }
193
210
 
194
211
  // We use flexbox to consistently vertically center cell contents across
@@ -227,21 +244,52 @@
227
244
  &--no-cell-borders {
228
245
  .ag-cell,
229
246
  .ag-context-menu-open .ag-cell-focus:not(.ag-cell-range-selected) {
230
- border-color: transparent;
247
+ // Preserve left and right borders to avoid jumpiness upon cell focus
248
+ border-top: none;
249
+ border-bottom: none;
250
+ border-left-color: transparent;
251
+ border-right-color: transparent;
231
252
  }
232
253
  }
233
254
 
234
255
  // Cell focus
235
256
  &--no-cell-focus {
257
+ // Preserve cell borders when enabled upon "invisible" cell focus
258
+ &.xh-ag-grid--cell-borders .ag-has-focus .ag-cell-focus:not(.ag-cell-range-selected) {
259
+ border-right-color: var(--xh-grid-border-color);
260
+ }
261
+
236
262
  .ag-has-focus {
237
263
  .ag-cell-focus:not(.ag-cell-range-selected) {
238
- border-color: transparent;
264
+ // Preserve left and right borders to avoid jumpiness upon cell focus
265
+ border-top: none;
266
+ border-bottom: none;
267
+ border-left-color: transparent;
268
+ border-right-color: transparent;
269
+
270
+ &.ag-cell {
271
+ &.ag-cell-last-left-pinned {
272
+ @include pinned-border(right);
273
+ }
274
+
275
+ &.ag-cell-first-right-pinned {
276
+ @include pinned-border(left);
277
+ }
278
+
279
+ &.xh-cell--group-border-left {
280
+ @include group-border(left);
281
+ }
282
+
283
+ &.xh-cell--group-border-right {
284
+ @include group-border(right);
285
+ }
286
+ }
239
287
  }
240
288
  }
241
289
  }
242
290
 
243
291
  &--show-cell-focus {
244
- .ag-cell-focus {
292
+ .ag-cell-focus:focus-within {
245
293
  border-color: var(--xh-grid-cell-focus-border-color) !important;
246
294
  }
247
295
  }
@@ -12,6 +12,7 @@ import {
12
12
  HoistProps,
13
13
  LayoutProps,
14
14
  lookup,
15
+ TestSupportProps,
15
16
  useLocalModel,
16
17
  uses,
17
18
  XH
@@ -23,7 +24,11 @@ import {isNil} from 'lodash';
23
24
  import './AgGrid.scss';
24
25
  import {AgGridModel} from './AgGridModel';
25
26
 
26
- export interface AgGridProps extends HoistProps<AgGridModel>, GridOptions, LayoutProps {}
27
+ export interface AgGridProps
28
+ extends HoistProps<AgGridModel>,
29
+ GridOptions,
30
+ LayoutProps,
31
+ TestSupportProps {}
27
32
 
28
33
  /**
29
34
  * Minimal wrapper for AgGridReact, supporting direct use of the ag-Grid component with limited
@@ -53,7 +58,7 @@ export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
53
58
  className: 'xh-ag-grid',
54
59
  model: uses(AgGridModel),
55
60
 
56
- render({model, className, ...props}, ref) {
61
+ render({model, className, testId, ...props}, ref) {
57
62
  if (!AgGridReact) {
58
63
  console.error(
59
64
  'ag-Grid has not been imported in to this application. Please import and ' +
@@ -90,6 +95,7 @@ export const [AgGrid, agGrid] = hoistCmp.withFactory<AgGridProps>({
90
95
  hideHeaders ? 'xh-ag-grid--hide-headers' : null
91
96
  ),
92
97
  ...layoutProps,
98
+ testId,
93
99
  item: createElement(AgGridReact, {
94
100
  ...AgGrid['DEFAULT_PROPS'],
95
101
  // Default some ag-grid props, but allow overriding.
@@ -4,9 +4,12 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {div} from '@xh/hoist/cmp/layout';
7
8
  import {BoxProps, hoistCmp, HoistProps, Intent} from '@xh/hoist/core';
9
+ import {TEST_ID} from '@xh/hoist/utils/js';
10
+ import {splitLayoutProps} from '@xh/hoist/utils/react';
8
11
  import classNames from 'classnames';
9
- import {div} from '@xh/hoist/cmp/layout';
12
+ import {merge} from 'lodash';
10
13
  import './Badge.scss';
11
14
 
12
15
  export interface BadgeProps extends HoistProps, BoxProps {
@@ -26,8 +29,10 @@ export const [Badge, badge] = hoistCmp.withFactory<BadgeProps>({
26
29
 
27
30
  className: 'xh-badge',
28
31
 
29
- render({className, intent, compact = false, ...props}) {
30
- const classes = [];
32
+ render(props, ref) {
33
+ const classes = [],
34
+ [layoutProps, {className, intent, compact, children, testId, ...restProps}] =
35
+ splitLayoutProps(props);
31
36
 
32
37
  if (intent) {
33
38
  classes.push(`xh-badge--intent-${intent}`);
@@ -37,9 +42,17 @@ export const [Badge, badge] = hoistCmp.withFactory<BadgeProps>({
37
42
  classes.push('xh-badge--compact');
38
43
  }
39
44
 
45
+ const divProps = merge(
46
+ {className: classNames(className, classes)},
47
+ {style: layoutProps},
48
+ {[TEST_ID]: testId},
49
+ restProps
50
+ );
51
+
40
52
  return div({
41
- className: classNames(className, classes),
42
- ...props
53
+ ref,
54
+ ...divProps,
55
+ items: children
43
56
  });
44
57
  }
45
58
  });
@@ -6,19 +6,20 @@
6
6
  */
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {box, div} from '@xh/hoist/cmp/layout';
9
- import {placeholder} from '../layout';
10
9
  import {
11
- lookup,
12
10
  hoistCmp,
13
11
  HoistModel,
12
+ HoistProps,
13
+ LayoutProps,
14
+ lookup,
15
+ PlainObject,
16
+ TestSupportProps,
14
17
  useLocalModel,
15
18
  uses,
16
- XH,
17
- BoxProps,
18
- HoistProps,
19
- PlainObject
19
+ XH
20
20
  } from '@xh/hoist/core';
21
21
  import {useContextMenu} from '@xh/hoist/dynamics/desktop';
22
+ import {Icon} from '@xh/hoist/icon';
22
23
  import {Highcharts} from '@xh/hoist/kit/highcharts';
23
24
  import {runInAction} from '@xh/hoist/mobx';
24
25
  import {
@@ -28,18 +29,18 @@ import {
28
29
  useOnVisibleChange
29
30
  } from '@xh/hoist/utils/react';
30
31
  import {assign, castArray, cloneDeep, forOwn, isEqual, isPlainObject, merge, omit} from 'lodash';
31
- import {Icon} from '@xh/hoist/icon';
32
+ import {placeholder} from '../layout';
33
+ import './Chart.scss';
32
34
  import {ChartModel} from './ChartModel';
33
- import {installZoomoutGesture} from './impl/zoomout';
34
35
  import {installCopyToClipboard} from './impl/copyToClipboard';
36
+ import {installZoomoutGesture} from './impl/zoomout';
35
37
  import {DarkTheme} from './theme/Dark';
36
38
  import {LightTheme} from './theme/Light';
37
- import './Chart.scss';
38
39
 
39
40
  installZoomoutGesture(Highcharts);
40
41
  installCopyToClipboard(Highcharts);
41
42
 
42
- export interface ChartProps extends HoistProps<ChartModel>, BoxProps {
43
+ export interface ChartProps extends HoistProps<ChartModel>, LayoutProps, TestSupportProps {
43
44
  /**
44
45
  * Ratio of width-to-height of displayed chart. If defined and greater than 0, the chart will
45
46
  * respect this ratio within the available space. Otherwise, the chart will stretch on both
@@ -58,7 +59,7 @@ export const [Chart, chart] = hoistCmp.withFactory<ChartProps>({
58
59
  model: uses(ChartModel),
59
60
  className: 'xh-chart',
60
61
 
61
- render({model, className, aspectRatio, ...props}, ref) {
62
+ render({model, className, aspectRatio, testId, ...props}, ref) {
62
63
  if (!Highcharts) {
63
64
  console.error(
64
65
  'Highcharts has not been imported in to this application. Please import and ' +
@@ -85,6 +86,7 @@ export const [Chart, chart] = hoistCmp.withFactory<ChartProps>({
85
86
  const coreContents = box({
86
87
  ...layoutProps,
87
88
  className,
89
+ testId,
88
90
  ref,
89
91
  item: div({
90
92
  style: {margin: 'auto'},
@@ -6,16 +6,16 @@
6
6
  */
7
7
  import {box, span} from '@xh/hoist/cmp/layout';
8
8
  import {
9
+ BoxProps,
9
10
  hoistCmp,
10
11
  HoistModel,
12
+ HoistProps,
11
13
  managed,
12
- BoxProps,
13
14
  useLocalModel,
14
- XH,
15
- HoistProps
15
+ XH
16
16
  } from '@xh/hoist/core';
17
17
  import {fmtDate, TIME_FMT} from '@xh/hoist/format';
18
- import {action, observable, makeObservable} from '@xh/hoist/mobx';
18
+ import {action, makeObservable, observable} from '@xh/hoist/mobx';
19
19
  import {Timer} from '@xh/hoist/utils/async';
20
20
  import {MINUTES, ONE_SECOND} from '@xh/hoist/utils/datetime';
21
21
  import {isNumber} from 'lodash';
@@ -54,11 +54,12 @@ export const [Clock, clock] = hoistCmp.withFactory<ClockProps>({
54
54
  displayName: 'Clock',
55
55
  className: 'xh-clock',
56
56
 
57
- render({className, ...props}, ref) {
57
+ render({className, testId, ...props}, ref) {
58
58
  const impl = useLocalModel(ClockLocalModel);
59
59
  return box({
60
60
  className,
61
61
  ...getLayoutProps(props),
62
+ testId,
62
63
  ref,
63
64
  item: span(impl.display)
64
65
  });
@@ -7,12 +7,13 @@
7
7
  import {AgGrid} from '@xh/hoist/cmp/ag-grid';
8
8
  import {grid} from '@xh/hoist/cmp/grid';
9
9
  import {
10
- BoxProps,
11
10
  hoistCmp,
12
11
  HoistModel,
13
12
  HoistProps,
13
+ LayoutProps,
14
14
  lookup,
15
15
  PlainObject,
16
+ TestSupportProps,
16
17
  useLocalModel,
17
18
  uses
18
19
  } from '@xh/hoist/core';
@@ -22,7 +23,7 @@ import {isFunction, merge} from 'lodash';
22
23
  import './DataView.scss';
23
24
  import {DataViewModel} from './DataViewModel';
24
25
 
25
- export interface DataViewProps extends HoistProps<DataViewModel>, BoxProps {
26
+ export interface DataViewProps extends HoistProps<DataViewModel>, LayoutProps, TestSupportProps {
26
27
  /**
27
28
  * Options for ag-Grid's API.
28
29
  *
@@ -44,13 +45,14 @@ export const [DataView, dataView] = hoistCmp.withFactory<DataViewProps>({
44
45
  model: uses(DataViewModel),
45
46
  className: 'xh-data-view',
46
47
 
47
- render({model, className, ...props}, ref) {
48
+ render({model, className, testId, ...props}, ref) {
48
49
  const [layoutProps] = splitLayoutProps(props),
49
50
  impl = useLocalModel(DataViewLocalModel);
50
51
 
51
52
  return grid({
52
53
  ...layoutProps,
53
54
  className,
55
+ testId,
54
56
  ref,
55
57
  model: model.gridModel,
56
58
  agOptions: impl.agOptions
package/cmp/form/Form.ts CHANGED
@@ -4,12 +4,19 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {DefaultHoistProps, elementFactory, hoistCmp, HoistProps, uses} from '@xh/hoist/core';
7
+ import {
8
+ DefaultHoistProps,
9
+ elementFactory,
10
+ hoistCmp,
11
+ HoistProps,
12
+ TestSupportProps,
13
+ uses
14
+ } from '@xh/hoist/core';
15
+ import {useCached} from '@xh/hoist/utils/react';
8
16
  import equal from 'fast-deep-equal';
9
17
  import {createContext, useContext} from 'react';
10
- import {useCached} from '@xh/hoist/utils/react';
11
- import {FormModel} from './FormModel';
12
18
  import {BaseFormFieldProps} from './BaseFormFieldProps';
19
+ import {FormModel} from './FormModel';
13
20
 
14
21
  /** @internal */
15
22
  export interface FormContextType {
@@ -18,13 +25,21 @@ export interface FormContextType {
18
25
 
19
26
  /** Reference to associated FormModel. */
20
27
  model?: FormModel;
28
+
29
+ /**
30
+ * Not rendered into the DOM directly - `Form` is a context provider and not a concrete
31
+ * component - but will auto-generate and apply a testId of `${formTestId}-${fieldName}`
32
+ * for every child {@link FormField} component, providing a centralized way to wire up
33
+ * a form and all of its fields for testing.
34
+ */
35
+ testId?: string;
21
36
  }
22
37
 
23
38
  /** @internal */
24
39
  export const FormContext = createContext<FormContextType>({});
25
40
  const formContextProvider = elementFactory(FormContext.Provider);
26
41
 
27
- export interface FormProps extends HoistProps<FormModel> {
42
+ export interface FormProps extends HoistProps<FormModel>, TestSupportProps {
28
43
  /**
29
44
  * Defaults for certain props on child/nested FormFields.
30
45
  * @see FormField (note there are both desktop and mobile implementations).
@@ -50,14 +65,18 @@ export const [Form, form] = hoistCmp.withFactory<FormProps>({
50
65
  displayName: 'Form',
51
66
  model: uses(FormModel, {publishMode: 'none'}),
52
67
 
53
- render({model, fieldDefaults = {}, children}) {
68
+ render({model, fieldDefaults = {}, testId, children}) {
54
69
  // gather own and inherited field defaults...
55
70
  const parentDefaults = useContext(FormContext).fieldDefaults;
56
71
  if (parentDefaults) fieldDefaults = {...parentDefaults, ...fieldDefaults};
57
72
 
58
73
  // ...and deliver as a cached context to avoid spurious re-renders
59
74
  const formContext = useCached(
60
- {model, fieldDefaults},
75
+ {
76
+ model,
77
+ fieldDefaults,
78
+ testId
79
+ },
61
80
  (a, b) => a.model === b.model && equal(a.fieldDefaults, b.fieldDefaults)
62
81
  );
63
82
  return formContextProvider({value: formContext, items: children});
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  @use 'sass:math';
9
+ @use '../ag-grid/AgGrid';
9
10
 
10
11
  .xh-grid:not(.xh-data-view) {
11
12
  //------------------------
@@ -124,6 +125,15 @@
124
125
  }
125
126
  }
126
127
 
128
+ // Render left / right group borders
129
+ .ag-cell.xh-cell--group-border-left {
130
+ @include AgGrid.group-border(left);
131
+ }
132
+
133
+ .ag-cell.xh-cell--group-border-right {
134
+ @include AgGrid.group-border(right);
135
+ }
136
+
127
137
  .xh-ag-grid {
128
138
  &--tiny {
129
139
  .ag-cell.xh-cell--invalid::before {