@xh/hoist 59.2.0 → 59.3.1

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 (106) hide show
  1. package/CHANGELOG.md +71 -14
  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/config/ConfigPanel.ts +1 -0
  6. package/admin/tabs/monitor/MonitorTab.ts +1 -1
  7. package/admin/tabs/server/ServerTab.ts +1 -1
  8. package/admin/tabs/userData/UserDataTab.ts +1 -1
  9. package/cmp/ag-grid/AgGrid.scss +51 -25
  10. package/cmp/ag-grid/AgGrid.ts +8 -2
  11. package/cmp/badge/Badge.ts +18 -5
  12. package/cmp/chart/Chart.ts +13 -11
  13. package/cmp/clock/Clock.ts +6 -5
  14. package/cmp/dataview/DataView.ts +5 -3
  15. package/cmp/form/Form.ts +25 -6
  16. package/cmp/grid/Grid.ts +41 -25
  17. package/cmp/grid/GridModel.ts +2 -2
  18. package/cmp/grid/Types.ts +1 -1
  19. package/cmp/grid/columns/Column.ts +45 -2
  20. package/cmp/grid/impl/GridHScrollbar.ts +140 -0
  21. package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
  22. package/cmp/input/HoistInputModel.ts +4 -4
  23. package/cmp/input/HoistInputProps.ts +3 -1
  24. package/cmp/layout/Box.ts +4 -2
  25. package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
  26. package/cmp/store/StoreFilterField.ts +2 -2
  27. package/cmp/tab/TabContainer.ts +1 -1
  28. package/cmp/zoneGrid/Types.ts +47 -0
  29. package/cmp/zoneGrid/ZoneGrid.ts +62 -0
  30. package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
  31. package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
  32. package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
  33. package/cmp/zoneGrid/index.ts +3 -0
  34. package/core/HoistComponent.ts +23 -10
  35. package/core/HoistProps.ts +25 -6
  36. package/core/XH.ts +49 -27
  37. package/core/elem.ts +11 -3
  38. package/core/exception/ExceptionHandler.ts +1 -0
  39. package/core/impl/InstanceManager.ts +24 -1
  40. package/core/model/HoistModel.ts +4 -4
  41. package/data/RecordAction.ts +7 -4
  42. package/data/StoreRecord.ts +8 -1
  43. package/desktop/appcontainer/AppContainer.ts +2 -0
  44. package/desktop/appcontainer/ExceptionDialog.ts +1 -1
  45. package/desktop/cmp/appbar/AppBar.ts +8 -6
  46. package/desktop/cmp/button/Button.ts +14 -3
  47. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  48. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  49. package/desktop/cmp/button/index.ts +1 -0
  50. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  51. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  52. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  53. package/desktop/cmp/form/FormField.ts +34 -10
  54. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  55. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  56. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  57. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  58. package/desktop/cmp/input/Checkbox.ts +3 -3
  59. package/desktop/cmp/input/CodeInput.ts +2 -1
  60. package/desktop/cmp/input/DateInput.ts +128 -123
  61. package/desktop/cmp/input/JsonInput.ts +1 -1
  62. package/desktop/cmp/input/NumberInput.ts +3 -2
  63. package/desktop/cmp/input/RadioInput.ts +3 -1
  64. package/desktop/cmp/input/Select.ts +31 -4
  65. package/desktop/cmp/input/SwitchInput.ts +2 -1
  66. package/desktop/cmp/input/TextArea.ts +3 -3
  67. package/desktop/cmp/input/TextInput.ts +51 -47
  68. package/desktop/cmp/panel/Panel.ts +19 -15
  69. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  70. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  71. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  72. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  73. package/desktop/cmp/rest/Actions.ts +10 -5
  74. package/desktop/cmp/rest/RestGrid.ts +20 -6
  75. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  76. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  77. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  78. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  79. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  80. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  81. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  82. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  83. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  84. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  85. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  86. package/dynamics/desktop.ts +2 -0
  87. package/dynamics/mobile.ts +2 -0
  88. package/inspector/instances/InstancesModel.ts +2 -2
  89. package/mobile/appcontainer/AppContainer.ts +2 -0
  90. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  91. package/mobile/cmp/button/index.ts +1 -0
  92. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  93. package/mobile/cmp/input/Select.scss +1 -0
  94. package/mobile/cmp/input/Select.ts +7 -0
  95. package/mobile/cmp/input/TextInput.ts +1 -0
  96. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  97. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  98. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  99. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  100. package/package.json +4 -3
  101. package/styles/vars.scss +3 -3
  102. package/svc/InspectorService.ts +1 -1
  103. package/utils/js/DomUtils.ts +10 -0
  104. package/utils/js/LangUtils.ts +10 -0
  105. package/utils/js/TestUtils.ts +9 -0
  106. package/utils/js/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,29 +1,85 @@
1
1
  # Changelog
2
2
 
3
+ ## 59.3.1 - 2023-11-10
4
+
5
+ ### 🐞 Bug Fixes
6
+
7
+ * Ensure an unauthorized response from a proxy service endpoint does not prompt the user to refresh
8
+ and log in again on an SSO-enabled application.
9
+ * Revert change to `Panel` which affected where `className` was applied with `modalSupport` enabled
10
+
11
+ ## 59.3.0 - 2023-11-09
12
+
13
+ ### 🎁 New Features
14
+
15
+ * Improved Hoist support for automated testing via Playwright, Cypress, and similar tools:
16
+ * Core Hoist components now accept an optional `testId` prop, to be rendered at an appropriate
17
+ level of the DOM (within a `data-testid` HTML attribute). This can minimize the need to select
18
+ components using criteria such as CSS classes or labels that are more likely to change and
19
+ break tests.
20
+ * When given a `testId`, certain composite components will generate and set "sub-testIds" on
21
+ selected internal components. For example, a `TabContainer` will set a testId on each switcher
22
+ button (derived from its tabId), and a `Form` will set testIds on nested `FormField`
23
+ and `HoistInput` components (derived from their bound field names).
24
+ * This release represents a first step in ongoing work to facilitate automated end-to-end
25
+ testing of Hoist applications. Additional Hoist-specific utilities for writing tests in
26
+ libraries such as Cypress and Playwright are coming soon.
27
+ * Added new `ZoneGrid` component, a highly specialized `Grid` that always displays its data with
28
+ multi-line, full-width rows. Each row is broken into four zones (top/bottom and left/right),
29
+ each of which can mapped by the user to render data from one or more fields.
30
+ * Primarily intended for mobile, where horizontal scrolling can present usability issues, but
31
+ also available on desktop, where it can serve as an easily user-configurable `DataView`.
32
+ * Added `Column.sortToBottom` to force specified values to sort the bottom, regardless of sort
33
+ direction. Intended primarily to force null values to sort below all others.
34
+ * Upgraded the `RelativeTimestamp` component with a new `localDateMode` option to customize how
35
+ near-term date/time differences are rendered with regards to calendar days.
36
+
37
+ ### 🐞 Bug Fixes
38
+
39
+ * Fixed bug where interacting with a `Select` within a `Popover` can inadvertently cause the
40
+ popover to close. If your app already has special handling in place to prevent this, you should
41
+ be able to unwind it after upgrading.
42
+ * Improved the behavior of the clear button in `TextInput`. Clearing a field no longer drops focus,
43
+ allowing the user to immediately begin typing in a new value.
44
+ * Fixed arguments passed to `ErrorMessageProps.actionFn` and `ErrorMessageProps.detailsFn`.
45
+ * Improved default error text in `ErrorMessage`.
46
+
47
+ ### ⚙️ Technical
48
+
49
+ * Improved core `HoistComponent` performance by preventing unnecessary re-renderings triggered by
50
+ spurious model lookup changes.
51
+ * New flag `GridModel.experimental.enableFullWidthScroll` enables scrollbars to span pinned columns.
52
+ * Early test release behind the flag, expected to made the default behavior in next release.
53
+ * Renamed `XH.getActiveModels()` to `XH.getModels()` for clarity / consistency.
54
+ * API change, but not expected to impact applications.
55
+ * Added `XH.getModel()` convenience method to return the first matching model.
56
+
3
57
  ## 59.2.0 - 2023-10-16
4
58
 
5
59
  ### 🎁 New Features
6
60
 
7
- * New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view
8
- * Add `GridModel` APIs to lookup and show / hide entire column groups
9
- * Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default. Control
10
- with new boolean property `ColumnGroupSpec.borders`
11
- * The Cube package has been enhanced to support `Query` specific post-processing functions. See
12
- new properties `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default
13
- to their respective properties on `Cube`.
61
+ * New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view.
62
+ * Added `GridModel` APIs to lookup and show / hide entire column groups.
63
+ * Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default, controllable
64
+ with new `ColumnGroupSpec.borders` config.
65
+ * Enhanced the `CubeQuery` to support per-query post-processing functions
66
+ with `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default to their
67
+ respective properties on `Cube`.
14
68
 
15
69
  ### 🐞 Bug Fixes
16
70
 
17
71
  * `DashContainerModel` fixes:
18
- * Fix bug where `addView` would throw when adding a view to a row or column
19
- * Fix bug where `allowRemove` flag was dropped from state for containers
20
- * Fix bug in `DockContainer` where adding / removing views would cause other views to be remounted
21
- * Fix erroneous `GridModel` warning when using a tree column within a column group
22
- * Fix regression to alert banners. Resume allowing elements as messages
72
+ * Fix bug where `addView` would throw when adding a view to a row or column
73
+ * Fix bug where `allowRemove` flag was dropped from state for containers
74
+ * Fix bug in `DockContainer` where adding / removing views would cause other views to be
75
+ remounted
76
+ * Fixed erroneous `GridModel` warning when using a tree column within a column group
77
+ * Fixed regression to alert banners. Resume allowing elements as messages.
78
+ * Fix `Grid` cell border styling inconsistencies.
23
79
 
24
80
  ### ⚙️ Typescript API Adjustments
25
81
 
26
- * Add type for `ActionFnData.record`
82
+ * Added type for `ActionFnData.record`.
27
83
 
28
84
  ## 59.1.0 - 2023-09-20
29
85
 
@@ -49,7 +105,8 @@ to their respective properties on `Cube`.
49
105
  * Improved styling for disabled `checkbox` inputs.
50
106
 
51
107
  ### ⚙️ Technical
52
- * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
108
+
109
+ * `XH.showException` has been deprecated. Use similar methods on `XH.exceptionHandler` instead.
53
110
 
54
111
  ### 📚 Libraries
55
112
 
@@ -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},
@@ -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',
@@ -9,8 +9,8 @@
9
9
  border-#{$side}: var(--xh-border-solid);
10
10
  }
11
11
 
12
- @mixin pinned-border {
13
- border-right: 1px solid var(--xh-grid-pinned-column-border-color);
12
+ @mixin pinned-border($side) {
13
+ border-#{$side}: 1px solid var(--xh-grid-pinned-column-border-color);
14
14
  }
15
15
 
16
16
  // Ag-grid installs an outer div around itself.
@@ -128,16 +128,17 @@
128
128
  }
129
129
 
130
130
  &--no-row-borders {
131
- .ag-row {
131
+ .ag-row,
132
+ .ag-cell {
132
133
  border-bottom: none;
133
134
  border-top: none;
135
+ }
134
136
 
135
- // Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
136
- // Without this collapsed groups (which don't stripe) blend together in a solid block.
137
- &.ag-row-group.ag-full-width-row {
138
- border-bottom: 1px solid var(--xh-grid-group-border-color);
139
- border-top: 1px solid var(--xh-grid-group-border-color);
140
- }
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);
141
142
  }
142
143
  }
143
144
 
@@ -197,8 +198,14 @@
197
198
  }
198
199
  }
199
200
 
200
- .ag-cell.ag-cell-last-left-pinned:not(.ag-cell-focus) {
201
- @include pinned-border;
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
+ }
202
209
  }
203
210
 
204
211
  // We use flexbox to consistently vertically center cell contents across
@@ -237,33 +244,52 @@
237
244
  &--no-cell-borders {
238
245
  .ag-cell,
239
246
  .ag-context-menu-open .ag-cell-focus:not(.ag-cell-range-selected) {
240
- border: none;
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;
241
252
  }
242
253
  }
243
254
 
244
255
  // Cell focus
245
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
+
246
262
  .ag-has-focus {
247
263
  .ag-cell-focus:not(.ag-cell-range-selected) {
248
- border: none;
249
-
250
- &.ag-cell.ag-cell-last-left-pinned {
251
- @include pinned-border;
252
- }
253
-
254
- &.ag-cell.xh-cell--group-border-left {
255
- @include group-border(left);
256
- }
257
-
258
- &.ag-cell.xh-cell--group-border-right {
259
- @include group-border(right);
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
+ }
260
286
  }
261
287
  }
262
288
  }
263
289
  }
264
290
 
265
291
  &--show-cell-focus {
266
- .ag-cell-focus {
292
+ .ag-cell-focus:focus-within {
267
293
  border-color: var(--xh-grid-cell-focus-border-color) !important;
268
294
  }
269
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});