@xh/hoist 59.2.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 (104) hide show
  1. package/CHANGELOG.md +63 -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/impl/InstanceManager.ts +24 -1
  39. package/core/model/HoistModel.ts +4 -4
  40. package/data/RecordAction.ts +7 -4
  41. package/data/StoreRecord.ts +8 -1
  42. package/desktop/appcontainer/AppContainer.ts +2 -0
  43. package/desktop/cmp/appbar/AppBar.ts +8 -6
  44. package/desktop/cmp/button/Button.ts +14 -3
  45. package/desktop/cmp/button/ButtonGroup.ts +14 -3
  46. package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
  47. package/desktop/cmp/button/index.ts +1 -0
  48. package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
  49. package/desktop/cmp/dash/container/DashContainer.ts +11 -4
  50. package/desktop/cmp/error/ErrorMessage.ts +9 -8
  51. package/desktop/cmp/form/FormField.ts +34 -10
  52. package/desktop/cmp/grid/columns/Actions.ts +2 -1
  53. package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
  54. package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
  55. package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
  56. package/desktop/cmp/input/Checkbox.ts +3 -3
  57. package/desktop/cmp/input/CodeInput.ts +2 -1
  58. package/desktop/cmp/input/DateInput.ts +128 -123
  59. package/desktop/cmp/input/JsonInput.ts +1 -1
  60. package/desktop/cmp/input/NumberInput.ts +3 -2
  61. package/desktop/cmp/input/RadioInput.ts +3 -1
  62. package/desktop/cmp/input/Select.ts +31 -4
  63. package/desktop/cmp/input/SwitchInput.ts +2 -1
  64. package/desktop/cmp/input/TextArea.ts +3 -3
  65. package/desktop/cmp/input/TextInput.ts +51 -47
  66. package/desktop/cmp/panel/Panel.ts +21 -19
  67. package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
  68. package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
  69. package/desktop/cmp/record/RecordActionBar.ts +12 -3
  70. package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
  71. package/desktop/cmp/rest/Actions.ts +10 -5
  72. package/desktop/cmp/rest/RestGrid.ts +20 -6
  73. package/desktop/cmp/rest/impl/RestForm.ts +5 -4
  74. package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
  75. package/desktop/cmp/tab/TabSwitcher.ts +8 -3
  76. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  77. package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
  78. package/desktop/cmp/toolbar/Toolbar.ts +3 -1
  79. package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
  80. package/desktop/cmp/treemap/TreeMap.ts +5 -3
  81. package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
  82. package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
  83. package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
  84. package/dynamics/desktop.ts +2 -0
  85. package/dynamics/mobile.ts +2 -0
  86. package/inspector/instances/InstancesModel.ts +2 -2
  87. package/mobile/appcontainer/AppContainer.ts +2 -0
  88. package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
  89. package/mobile/cmp/button/index.ts +1 -0
  90. package/mobile/cmp/error/ErrorMessage.ts +4 -4
  91. package/mobile/cmp/input/Select.scss +1 -0
  92. package/mobile/cmp/input/Select.ts +7 -0
  93. package/mobile/cmp/input/TextInput.ts +1 -0
  94. package/mobile/cmp/panel/DialogPanel.scss +18 -6
  95. package/mobile/cmp/panel/DialogPanel.ts +3 -1
  96. package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
  97. package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
  98. package/package.json +4 -3
  99. package/styles/vars.scss +3 -3
  100. package/svc/InspectorService.ts +1 -1
  101. package/utils/js/DomUtils.ts +10 -0
  102. package/utils/js/LangUtils.ts +10 -0
  103. package/utils/js/TestUtils.ts +9 -0
  104. package/utils/js/index.ts +1 -0
@@ -9,7 +9,7 @@ import {hoistCmp, HoistProps, HSide} from '@xh/hoist/core';
9
9
  import '@xh/hoist/desktop/register';
10
10
  import {radio, radioGroup} from '@xh/hoist/kit/blueprint';
11
11
  import {computed, makeObservable} from '@xh/hoist/mobx';
12
- import {withDefault} from '@xh/hoist/utils/js';
12
+ import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js';
13
13
  import {filter, isObject} from 'lodash';
14
14
  import './RadioInput.scss';
15
15
 
@@ -99,6 +99,7 @@ const cmp = hoistCmp.factory<RadioInputModel>(({model, className, ...props}, ref
99
99
  label: opt.label,
100
100
  value: opt.value,
101
101
  className: 'xh-radio-input-option',
102
+ [TEST_ID]: getTestId(props.testId, `${opt.label}`),
102
103
  onFocus: model.onFocus,
103
104
  onBlur: model.onBlur
104
105
  });
@@ -111,6 +112,7 @@ const cmp = hoistCmp.factory<RadioInputModel>(({model, className, ...props}, ref
111
112
  inline: props.inline,
112
113
  selectedValue: model.renderValue,
113
114
  onChange: model.onChange,
115
+ testId: props.testId,
114
116
  ref
115
117
  });
116
118
  });
@@ -7,14 +7,14 @@
7
7
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
8
  import {box, div, fragment, hbox, span} from '@xh/hoist/cmp/layout';
9
9
  import {
10
+ Awaitable,
10
11
  createElement,
11
12
  hoistCmp,
12
13
  HoistProps,
13
14
  LayoutProps,
14
15
  PlainObject,
15
- XH,
16
- Awaitable,
17
- SelectOption
16
+ SelectOption,
17
+ XH
18
18
  } from '@xh/hoist/core';
19
19
  import '@xh/hoist/desktop/register';
20
20
  import {Icon} from '@xh/hoist/icon';
@@ -28,7 +28,7 @@ import {
28
28
  } from '@xh/hoist/kit/react-select';
29
29
  import {action, bindable, makeObservable, observable, override} from '@xh/hoist/mobx';
30
30
  import {wait} from '@xh/hoist/promise';
31
- import {throwIf, withDefault} from '@xh/hoist/utils/js';
31
+ import {elemWithin, getTestId, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
32
32
  import {createObservableRef, getLayoutProps} from '@xh/hoist/utils/react';
33
33
  import classNames from 'classnames';
34
34
  import debouncePromise from 'debounce-promise';
@@ -591,6 +591,21 @@ class SelectInputModel extends HoistInputModel {
591
591
  return this._valueContainerCmp;
592
592
  }
593
593
 
594
+ _menuCmp = null;
595
+ getMenuCmp() {
596
+ if (!this._menuCmp) {
597
+ const testId = getTestId(this.componentProps, 'menu');
598
+ this._menuCmp = testId
599
+ ? props =>
600
+ createElement(components.Menu, {
601
+ ...props,
602
+ innerProps: {[TEST_ID]: testId, ...props.innerProps}
603
+ })
604
+ : components.Menu;
605
+ }
606
+ return this._menuCmp;
607
+ }
608
+
594
609
  getDropdownIndicatorCmp() {
595
610
  return this.hideDropdownIndicator
596
611
  ? () => null
@@ -604,6 +619,7 @@ class SelectInputModel extends HoistInputModel {
604
619
  return div({
605
620
  ...restInnerProps,
606
621
  ref,
622
+ [TEST_ID]: getTestId(this.componentProps, 'clear-btn'),
607
623
  item: Icon.x({className: 'xh-select__indicator'})
608
624
  });
609
625
  };
@@ -701,6 +717,7 @@ const cmp = hoistCmp.factory<SelectInputModel>(({model, className, ...props}, re
701
717
  components: {
702
718
  DropdownIndicator: model.getDropdownIndicatorCmp(),
703
719
  ClearIndicator: model.getClearIndicatorCmp(),
720
+ Menu: model.getMenuCmp(),
704
721
  IndicatorSeparator: () => null,
705
722
  ValueContainer: model.getValueContainerCmp(),
706
723
  MultiValueLabel: model.getMultiValueLabelCmp(),
@@ -772,6 +789,16 @@ const cmp = hoistCmp.factory<SelectInputModel>(({model, className, ...props}, re
772
789
  e.stopPropagation();
773
790
  }
774
791
  },
792
+ onMouseDown: e => {
793
+ // Some internal elements, like the dropdown indicator and the rendered single value,
794
+ // fire 'mousedown' events. These can bubble and inadvertently close Popovers that
795
+ // contain Selects.
796
+ const target = e?.target as HTMLElement;
797
+ if (target && elemWithin(target, 'bp4-popover')) {
798
+ e.stopPropagation();
799
+ }
800
+ },
801
+ testId: props.testId,
775
802
  ...layoutProps,
776
803
  width: withDefault(width, 200),
777
804
  height: height,
@@ -8,7 +8,7 @@ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cm
8
8
  import {hoistCmp, HoistProps, HSide, StyleProps} from '@xh/hoist/core';
9
9
  import '@xh/hoist/desktop/register';
10
10
  import {switchControl} from '@xh/hoist/kit/blueprint';
11
- import {withDefault} from '@xh/hoist/utils/js';
11
+ import {TEST_ID, withDefault} from '@xh/hoist/utils/js';
12
12
  import {ReactNode} from 'react';
13
13
  import './SwitchInput.scss';
14
14
 
@@ -62,6 +62,7 @@ const cmp = hoistCmp.factory<SwitchInputModel>(({model, className, ...props}, re
62
62
  id: props.id,
63
63
  className,
64
64
 
65
+ [TEST_ID]: props.testId,
65
66
  onBlur: model.onBlur,
66
67
  onFocus: model.onFocus,
67
68
  onChange: e => model.noteValueChange(e.target.checked),
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
9
- import {hoistCmp, LayoutProps, HoistProps, StyleProps} from '@xh/hoist/core';
9
+ import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {textArea as bpTextarea} from '@xh/hoist/kit/blueprint';
12
- import {apiRemoved, withDefault} from '@xh/hoist/utils/js';
12
+ import {apiRemoved, TEST_ID, withDefault} from '@xh/hoist/utils/js';
13
13
  import {getLayoutProps} from '@xh/hoist/utils/react';
14
14
  import {Ref} from 'react';
15
15
  import './TextArea.scss';
@@ -91,7 +91,7 @@ const cmp = hoistCmp.factory<TextAreaInputModel>(({model, className, ...props},
91
91
  placeholder: props.placeholder,
92
92
  spellCheck: withDefault(props.spellCheck, false),
93
93
  tabIndex: props.tabIndex,
94
-
94
+ [TEST_ID]: props.testId,
95
95
  id: props.id,
96
96
  className,
97
97
  style: {
@@ -12,10 +12,10 @@ import {button} from '@xh/hoist/desktop/cmp/button';
12
12
  import '@xh/hoist/desktop/register';
13
13
  import {Icon} from '@xh/hoist/icon';
14
14
  import {inputGroup} from '@xh/hoist/kit/blueprint';
15
- import {withDefault} from '@xh/hoist/utils/js';
15
+ import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js';
16
16
  import {getLayoutProps} from '@xh/hoist/utils/react';
17
17
  import {isEmpty} from 'lodash';
18
- import {ReactElement, ReactNode, Ref, FocusEvent} from 'react';
18
+ import {FocusEvent, ReactElement, ReactNode, Ref} from 'react';
19
19
 
20
20
  export interface TextInputProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps {
21
21
  value?: string;
@@ -114,63 +114,67 @@ export class TextInputModel extends HoistInputModel {
114
114
  };
115
115
  }
116
116
 
117
- const cmp = hoistCmp.factory<TextInputModel>(({model, className, ...props}, ref) => {
118
- const {width, flex, ...layoutProps} = getLayoutProps(props);
119
-
120
- const isClearable = !isEmpty(model.internalValue);
121
-
122
- return div({
123
- item: inputGroup({
124
- value: model.renderValue || '',
125
-
126
- autoComplete: withDefault(
127
- props.autoComplete,
128
- props.type === 'password' ? 'new-password' : 'off'
129
- ),
130
- autoFocus: props.autoFocus,
131
- disabled: props.disabled,
132
- inputRef: composeRefs(model.inputRef, props.inputRef),
133
- leftIcon: props.leftIcon,
134
- placeholder: props.placeholder,
135
- rightElement:
136
- props.rightElement ||
137
- (props.enableClear && !props.disabled && isClearable ? clearButton() : null),
138
- round: withDefault(props.round, false),
139
- spellCheck: withDefault(props.spellCheck, false),
140
- tabIndex: props.tabIndex,
141
- type: props.type,
142
-
143
- id: props.id,
117
+ const cmp = hoistCmp.factory<TextInputProps & {model: TextInputModel}>(
118
+ ({model, className, ...props}, ref) => {
119
+ const {width, flex, ...layoutProps} = getLayoutProps(props);
120
+
121
+ const isClearable = !isEmpty(model.internalValue);
122
+
123
+ return div({
124
+ item: inputGroup({
125
+ value: model.renderValue || '',
126
+
127
+ autoComplete: withDefault(
128
+ props.autoComplete,
129
+ props.type === 'password' ? 'new-password' : 'off'
130
+ ),
131
+ autoFocus: props.autoFocus,
132
+ disabled: props.disabled,
133
+ inputRef: composeRefs(model.inputRef, props.inputRef),
134
+ leftIcon: props.leftIcon,
135
+ placeholder: props.placeholder,
136
+ rightElement:
137
+ props.rightElement ||
138
+ (props.enableClear && !props.disabled && isClearable ? clearButton() : null),
139
+ round: withDefault(props.round, false),
140
+ spellCheck: withDefault(props.spellCheck, false),
141
+ tabIndex: props.tabIndex,
142
+ type: props.type,
143
+
144
+ id: props.id,
145
+ style: {
146
+ ...props.style,
147
+ ...layoutProps,
148
+ textAlign: withDefault(props.textAlign, 'left')
149
+ },
150
+ [TEST_ID]: props.testId,
151
+ onChange: model.onChange,
152
+ onKeyDown: model.onKeyDown
153
+ }),
154
+
155
+ className,
144
156
  style: {
145
- ...props.style,
146
- ...layoutProps,
147
- textAlign: withDefault(props.textAlign, 'left')
157
+ width: withDefault(width, 200),
158
+ flex: withDefault(flex, null)
148
159
  },
149
160
 
150
- onChange: model.onChange,
151
- onKeyDown: model.onKeyDown
152
- }),
153
-
154
- className,
155
- style: {
156
- width: withDefault(width, 200),
157
- flex: withDefault(flex, null)
158
- },
159
-
160
- onBlur: model.onBlur,
161
- onFocus: model.onFocus,
162
- ref
163
- });
164
- });
161
+ onBlur: model.onBlur,
162
+ onFocus: model.onFocus,
163
+ ref
164
+ });
165
+ }
166
+ );
165
167
 
166
168
  const clearButton = hoistCmp.factory<TextInputModel>(({model}) =>
167
169
  button({
168
170
  icon: Icon.cross(),
169
171
  tabIndex: -1,
170
172
  minimal: true,
173
+ testId: getTestId(model.componentProps, 'clear-btn'),
171
174
  onClick: () => {
172
175
  model.noteValueChange(null);
173
176
  model.doCommit();
177
+ model.focus();
174
178
  }
175
179
  })
176
180
  );
@@ -4,34 +4,34 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
7
8
  import {box, frame, vbox, vframe} from '@xh/hoist/cmp/layout';
8
9
  import {
9
10
  BoxProps,
11
+ hoistCmp,
12
+ HoistModel,
10
13
  HoistProps,
11
14
  refreshContextView,
12
15
  Some,
13
16
  TaskObserver,
14
17
  useContextModel,
15
- uses,
16
- hoistCmp,
17
- HoistModel
18
+ uses
18
19
  } from '@xh/hoist/core';
19
20
  import {loadingIndicator} from '@xh/hoist/desktop/cmp/loadingindicator';
20
21
  import {mask} from '@xh/hoist/desktop/cmp/mask';
21
22
  import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
22
23
  import {useContextMenu, useHotkeys} from '@xh/hoist/desktop/hooks';
23
24
  import '@xh/hoist/desktop/register';
25
+ import {HotkeyConfig} from '@xh/hoist/kit/blueprint';
24
26
  import {splitLayoutProps} from '@xh/hoist/utils/react';
25
27
  import {castArray, omitBy} from 'lodash';
26
28
  import {Children, isValidElement, ReactElement, ReactNode, useLayoutEffect, useRef} from 'react';
29
+ import {ContextMenuSpec} from '../contextmenu/ContextMenu';
27
30
  import {modalSupport} from '../modalsupport/ModalSupport';
28
31
  import {panelHeader} from './impl/PanelHeader';
29
32
  import {resizeContainer} from './impl/ResizeContainer';
30
33
  import './Panel.scss';
31
34
  import {PanelModel} from './PanelModel';
32
- import {HotkeyConfig} from '@xh/hoist/kit/blueprint';
33
- import {ContextMenuSpec} from '../contextmenu/ContextMenu';
34
- import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
35
35
 
36
36
  export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'title'> {
37
37
  /** True to style panel header (if displayed) with reduced padding and font-size. */
@@ -113,7 +113,7 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
113
113
  }),
114
114
  className: 'xh-panel',
115
115
 
116
- render({model, className, ...props}, ref) {
116
+ render({model, className, testId, ...props}, ref) {
117
117
  const contextModel = useContextModel('*');
118
118
 
119
119
  let wasDisplayed = useRef(false),
@@ -224,24 +224,26 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
224
224
  item = refreshContextView({model: refreshContextModel, item});
225
225
  }
226
226
 
227
+ // 5) Return wrapped in resizable + modal affordances if needed, or equivalent layout box
228
+
229
+ const useResizeContainer = resizable || collapsible || showSplitter;
230
+
231
+ // 5a) For modalSupport, className + testId need additional frame that will follow content
227
232
  if (modalSupportModel) {
228
233
  item = modalSupport({
229
234
  model: modalSupportModel,
230
- item: frame({
231
- // Frame ensures className is still present when rendered in Dialog
232
- item,
233
- className: model.isModal ? className : undefined
234
- })
235
+ item: frame({item, className, testId})
235
236
  });
236
- }
237
237
 
238
- // 5) Return wrapped in resizable affordances if needed, or equivalent layout box
239
- item =
240
- resizable || collapsible || showSplitter
241
- ? resizeContainer({ref, item, className})
242
- : box({ref, item, className, ...layoutProps});
238
+ return useResizeContainer
239
+ ? resizeContainer({ref, item})
240
+ : box({ref, item, ...layoutProps});
241
+ }
243
242
 
244
- return item;
243
+ // 5b) No modalSupport, className + testId applied directly to parent
244
+ return useResizeContainer
245
+ ? resizeContainer({ref, item, className, testId})
246
+ : box({ref, item, className, testId, ...layoutProps});
245
247
  }
246
248
  });
247
249
 
@@ -7,8 +7,8 @@
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {box, hbox, vbox} from '@xh/hoist/cmp/layout';
9
9
  import {hoistCmp, useContextModel} from '@xh/hoist/core';
10
- import {Children} from 'react';
11
10
  import {isString} from 'lodash';
11
+ import {Children} from 'react';
12
12
  import {PanelModel} from '../PanelModel';
13
13
  import {dragger} from './dragger/Dragger';
14
14
  import {splitter} from './Splitter';
@@ -18,7 +18,7 @@ export const resizeContainer = hoistCmp.factory({
18
18
  model: false,
19
19
  className: 'xh-resizable',
20
20
 
21
- render({className, children}, ref) {
21
+ render({className, children, testId}, ref) {
22
22
  const panelModel = useContextModel(PanelModel),
23
23
  {size, resizable, collapsed, vertical, contentFirst, showSplitter} = panelModel,
24
24
  dim = vertical ? 'height' : 'width',
@@ -54,6 +54,7 @@ export const resizeContainer = hoistCmp.factory({
54
54
  [dim]: cmpSize,
55
55
  [maxDim]: '100%',
56
56
  [minDim]: dragBarWidth,
57
+ testId,
57
58
  items
58
59
  });
59
60
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {div, frame, h1, hbox, p, span, vbox, vframe} from '@xh/hoist/cmp/layout';
9
+ import {PinPadModel} from '@xh/hoist/cmp/pinpad';
9
10
  import {hoistCmp} from '@xh/hoist/core';
10
11
  import {button} from '@xh/hoist/desktop/cmp/button';
11
12
  import '@xh/hoist/desktop/register';
@@ -13,19 +14,19 @@ import {Icon} from '@xh/hoist/icon/Icon';
13
14
  import {isNumber} from 'lodash';
14
15
 
15
16
  import './PinPad.scss';
16
- import {PinPadModel} from '@xh/hoist/cmp/pinpad';
17
17
 
18
18
  /**
19
19
  * Desktop Implementation of PinPad.
20
20
  *
21
21
  * @internal
22
22
  */
23
- export function pinPadImpl({model}, ref) {
23
+ export function pinPadImpl({model, testId}, ref) {
24
24
  return frame({
25
25
  ref: composeRefs(model.ref, ref),
26
26
  item: vframe({
27
27
  className: 'xh-pinpad__frame',
28
- items: [header(), display(), errorDisplay(), keypad()]
28
+ items: [header(), display(), errorDisplay(), keypad()],
29
+ testId
29
30
  })
30
31
  });
31
32
  }
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {Column, GridModel} from '@xh/hoist/cmp/grid';
9
9
  import {hoistCmp} from '@xh/hoist/core';
10
- import {RecordActionSpec, RecordAction, StoreRecord, StoreSelectionModel} from '@xh/hoist/data';
10
+ import {RecordAction, RecordActionSpec, StoreRecord, StoreSelectionModel} from '@xh/hoist/data';
11
11
  import {buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/button';
12
12
  import '@xh/hoist/desktop/register';
13
13
  import {throwIf} from '@xh/hoist/utils/js';
@@ -52,8 +52,17 @@ export const [RecordActionBar, recordActionBar] = hoistCmp.withFactory<RecordAct
52
52
  className: 'xh-record-action-bar',
53
53
 
54
54
  render(props) {
55
- const {actions, record, selModel, gridModel, column, buttonProps, vertical, ...rest} =
56
- props;
55
+ const {
56
+ actions,
57
+ record,
58
+ selModel,
59
+ gridModel,
60
+ column,
61
+ buttonProps,
62
+ vertical,
63
+ testId,
64
+ ...rest
65
+ } = props;
57
66
 
58
67
  throwIf(
59
68
  !record && !selModel,
@@ -74,6 +74,7 @@ export const [RecordActionButton, recordActionButton] =
74
74
  intent,
75
75
  title,
76
76
  disabled,
77
+ testId: action.testId,
77
78
  onClick: () => action.call({record, selectedRecords, gridModel, column}),
78
79
  ...rest
79
80
  });
@@ -14,7 +14,8 @@ export const addAction: RecordActionSpec = {
14
14
  icon: Icon.add(),
15
15
  intent: 'success',
16
16
  actionFn: ({gridModel}) => gridModel.appData.restGridModel.addRecord(),
17
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly})
17
+ displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
18
+ testId: 'add-action-button'
18
19
  };
19
20
 
20
21
  export const editAction: RecordActionSpec = {
@@ -23,14 +24,16 @@ export const editAction: RecordActionSpec = {
23
24
  intent: 'primary',
24
25
  recordsRequired: 1,
25
26
  actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.editRecord(record),
26
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly})
27
+ displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
28
+ testId: 'edit-action-button'
27
29
  };
28
30
 
29
31
  export const viewAction: RecordActionSpec = {
30
32
  text: 'View',
31
33
  icon: Icon.search(),
32
34
  recordsRequired: 1,
33
- actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.viewRecord(record)
35
+ actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.viewRecord(record),
36
+ testId: 'view-action-button'
34
37
  };
35
38
 
36
39
  export const cloneAction: RecordActionSpec = {
@@ -38,7 +41,8 @@ export const cloneAction: RecordActionSpec = {
38
41
  icon: Icon.copy(),
39
42
  recordsRequired: 1,
40
43
  actionFn: ({record, gridModel}) => gridModel.appData.restGridModel.cloneRecord(record),
41
- displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly})
44
+ displayFn: ({gridModel}) => ({hidden: gridModel.appData.restGridModel.readonly}),
45
+ testId: 'clone-action-button'
42
46
  };
43
47
 
44
48
  export const deleteAction: RecordActionSpec = {
@@ -49,5 +53,6 @@ export const deleteAction: RecordActionSpec = {
49
53
  displayFn: ({gridModel, record}) => ({
50
54
  hidden: (record && record.id === null) || gridModel.appData.restGridModel.readonly // Hide this action if we are acting on a "new" record
51
55
  }),
52
- actionFn: ({gridModel}) => gridModel.appData.restGridModel.confirmDeleteRecords()
56
+ actionFn: ({gridModel}) => gridModel.appData.restGridModel.confirmDeleteRecords(),
57
+ testId: 'delete-action-button'
53
58
  };
@@ -11,6 +11,7 @@ import {hoistCmp, HoistProps, PlainObject, Some, uses} from '@xh/hoist/core';
11
11
  import {MaskProps} from '@xh/hoist/desktop/cmp/mask';
12
12
  import {panel, PanelProps} from '@xh/hoist/desktop/cmp/panel';
13
13
  import '@xh/hoist/desktop/register';
14
+ import {getTestId} from '@xh/hoist/utils/js';
14
15
  import {cloneElement, isValidElement, ReactElement, ReactNode} from 'react';
15
16
 
16
17
  import {restForm} from './impl/RestForm';
@@ -46,18 +47,31 @@ export const [RestGrid, restGrid] = hoistCmp.withFactory<RestGridProps>({
46
47
  model: uses(RestGridModel, {publishMode: 'limited'}),
47
48
  className: 'xh-rest-grid',
48
49
 
49
- render({model, extraToolbarItems, mask = true, agOptions, formClassName, ...props}, ref) {
50
- const {formModel, gridModel} = model;
50
+ render(props, ref) {
51
+ const {
52
+ model,
53
+ extraToolbarItems,
54
+ mask = true,
55
+ agOptions,
56
+ formClassName,
57
+ testId,
58
+ ...restProps
59
+ } = props,
60
+ {formModel, gridModel} = model;
51
61
 
52
62
  return fragment(
53
63
  panel({
54
64
  ref,
55
- ...props,
56
- tbar: restGridToolbar({model, extraToolbarItems}),
57
- item: grid({model: gridModel, agOptions}),
65
+ ...restProps,
66
+ tbar: restGridToolbar({model, extraToolbarItems, testId}),
67
+ item: grid({model: gridModel, agOptions, testId: getTestId(testId, 'grid')}),
58
68
  mask: getMaskFromProp(model, mask)
59
69
  }),
60
- restForm({model: formModel, className: formClassName})
70
+ restForm({
71
+ model: formModel,
72
+ className: formClassName,
73
+ testId: getTestId(testId, 'form')
74
+ })
61
75
  );
62
76
  }
63
77
  });
@@ -25,7 +25,7 @@ export const restForm = hoistCmp.factory({
25
25
  model: uses(RestFormModel),
26
26
  className: 'xh-rest-form',
27
27
 
28
- render({model, className}) {
28
+ render({model, className, testId}) {
29
29
  const {isAdd, readonly, isOpen, dialogRef} = model;
30
30
  if (!isOpen) return null;
31
31
 
@@ -36,7 +36,7 @@ export const restForm = hoistCmp.factory({
36
36
  isOpen: true,
37
37
  isCloseButtonShown: false,
38
38
  item: panel({
39
- item: formDisplay(),
39
+ item: formDisplay({testId}),
40
40
  bbar: tbar(),
41
41
  ref: dialogRef,
42
42
  mask: 'onLoad'
@@ -45,7 +45,7 @@ export const restForm = hoistCmp.factory({
45
45
  }
46
46
  });
47
47
 
48
- const formDisplay = hoistCmp.factory<RestFormModel>(({model}) => {
48
+ const formDisplay = hoistCmp.factory<RestFormModel>(({model, testId}) => {
49
49
  const formFields = model.editors.map(editor => restFormField({editor}));
50
50
 
51
51
  return form({
@@ -59,7 +59,8 @@ const formDisplay = hoistCmp.factory<RestFormModel>(({model}) => {
59
59
  item: div({
60
60
  className: 'xh-rest-form__body',
61
61
  items: formFields
62
- })
62
+ }),
63
+ testId
63
64
  });
64
65
  });
65
66
 
@@ -20,7 +20,7 @@ import {RestGridModel} from '../RestGridModel';
20
20
  export const restGridToolbar = hoistCmp.factory({
21
21
  model: uses(RestGridModel, {publishMode: 'limited'}),
22
22
 
23
- render({model, extraToolbarItems}) {
23
+ render({model, extraToolbarItems, testId}) {
24
24
  const {unit, toolbarActions: actions, gridModel, readonly} = model;
25
25
 
26
26
  let extraItems = extraToolbarItems;
@@ -31,7 +31,8 @@ export const restGridToolbar = hoistCmp.factory({
31
31
  recordActionBar({
32
32
  actions,
33
33
  gridModel,
34
- selModel: gridModel.selModel
34
+ selModel: gridModel.selModel,
35
+ testId
35
36
  }),
36
37
  toolbarSep({
37
38
  omit: isEmpty(extraItems) || readonly
@@ -19,8 +19,8 @@ import {
19
19
  tabs as bpTabs,
20
20
  tooltip as bpTooltip
21
21
  } from '@xh/hoist/kit/blueprint';
22
- import {makeObservable, bindable} from '@xh/hoist/mobx';
23
- import {consumeEvent, debounced, isDisplayed, throwIf} from '@xh/hoist/utils/js';
22
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
23
+ import {consumeEvent, debounced, getTestId, isDisplayed, throwIf} from '@xh/hoist/utils/js';
24
24
  import {
25
25
  createObservableRef,
26
26
  getLayoutProps,
@@ -90,7 +90,9 @@ export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory<TabSwitcherProps>
90
90
  if (!vertical && isFinite(tabMaxWidth)) tabStyle.maxWidth = tabMaxWidth + 'px';
91
91
 
92
92
  const items = tabs.map(tab => {
93
- const {id, title, icon, disabled, tooltip, showRemoveAction, excludeFromSwitcher} = tab;
93
+ const {id, title, icon, disabled, tooltip, showRemoveAction, excludeFromSwitcher} = tab,
94
+ testId = getTestId(props, id);
95
+
94
96
  if (excludeFromSwitcher) return null;
95
97
  return bpTab({
96
98
  id,
@@ -105,10 +107,12 @@ export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory<TabSwitcherProps>
105
107
  item: hframe({
106
108
  className: 'xh-tab-switcher__tab',
107
109
  tabIndex: -1,
110
+ testId,
108
111
  items: [
109
112
  icon,
110
113
  span(title),
111
114
  button({
115
+ testId: getTestId(testId, 'remove-btn'),
112
116
  omit: !showRemoveAction,
113
117
  tabIndex: -1,
114
118
  icon: Icon.x(),
@@ -122,6 +126,7 @@ export const [TabSwitcher, tabSwitcher] = hoistCmp.withFactory<TabSwitcherProps>
122
126
 
123
127
  return box({
124
128
  ...layoutProps,
129
+ testId: props.testId,
125
130
  className: classNames(
126
131
  className,
127
132
  `xh-tab-switcher--${orientation}`,