@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
@@ -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,28 @@ 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
+ // For modalSupport, create additional frame that will follow content to portal and apply
232
+ // className and testId accordingly
227
233
  if (modalSupportModel) {
228
234
  item = modalSupport({
229
235
  model: modalSupportModel,
230
236
  item: frame({
231
- // Frame ensures className is still present when rendered in Dialog
232
237
  item,
233
- className: model.isModal ? className : undefined
238
+ className: model.isModal ? className : undefined,
239
+ testId: model.isModal ? testId : undefined
234
240
  })
235
241
  });
236
242
  }
237
243
 
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});
244
+ testId = model.isModal ? undefined : testId; // Only apply testId once
243
245
 
244
- return item;
246
+ return useResizeContainer
247
+ ? resizeContainer({ref, item, className, testId})
248
+ : box({ref, item, className, testId, ...layoutProps});
245
249
  }
246
250
  });
247
251
 
@@ -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}`,
@@ -26,7 +26,7 @@ export const tab = hoistCmp.factory({
26
26
  className: 'xh-tab',
27
27
  model: uses(TabModel, {publishMode: 'limited'}),
28
28
 
29
- render({model, className}) {
29
+ render({model, className, testId}) {
30
30
  let {content, isActive, renderMode, refreshContextModel} = model,
31
31
  wasActivated = useRef(false);
32
32
 
@@ -42,6 +42,7 @@ export const tab = hoistCmp.factory({
42
42
  return frame({
43
43
  display: isActive ? 'flex' : 'none',
44
44
  className,
45
+ testId,
45
46
  item: refreshContextView({
46
47
  model: refreshContextModel,
47
48
  item: errorBoundary(elementFromContent(content, {flex: 1}))
@@ -5,18 +5,19 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, hbox, placeholder, vbox} from '@xh/hoist/cmp/layout';
8
+ import {TabContainerModel, TabContainerProps} from '@xh/hoist/cmp/tab';
9
+ import {getTestId} from '@xh/hoist/utils/js';
8
10
  import {getLayoutProps} from '@xh/hoist/utils/react';
9
11
  import {isEmpty} from 'lodash';
10
12
  import '../Tabs.scss';
11
13
  import {tabSwitcher} from '../TabSwitcher';
12
14
  import {tab} from './Tab';
13
- import {TabContainerModel, TabContainerProps} from '@xh/hoist/cmp/tab';
14
15
 
15
16
  /**
16
17
  * Desktop implementation of TabContainer.
17
18
  * @internal
18
19
  */
19
- export function tabContainerImpl({model, className, ...props}: TabContainerProps) {
20
+ export function tabContainerImpl({model, className, testId, ...props}: TabContainerProps) {
20
21
  const layoutProps = getLayoutProps(props),
21
22
  vertical = ['left', 'right'].includes(model.switcher?.orientation),
22
23
  container = vertical ? hbox : vbox;
@@ -29,24 +30,26 @@ export function tabContainerImpl({model, className, ...props}: TabContainerProps
29
30
  return container({
30
31
  ...layoutProps,
31
32
  className,
32
- item: getChildren(model)
33
+ testId,
34
+ item: getChildren(model, testId)
33
35
  });
34
36
  }
35
37
 
36
- function getChildren(model: TabContainerModel) {
37
- const {tabs, activeTabId, switcher} = model,
38
- switcherBefore = ['left', 'top'].includes(switcher?.orientation),
39
- switcherAfter = ['right', 'bottom'].includes(switcher?.orientation);
40
-
38
+ function getChildren(model: TabContainerModel, testId: string) {
39
+ const {tabs} = model;
41
40
  if (isEmpty(tabs)) {
42
- return div({
43
- className: 'xh-tab-wrapper',
44
- item: placeholder(model.emptyText)
45
- });
41
+ return div({className: 'xh-tab-wrapper', item: placeholder(model.emptyText)});
46
42
  }
47
43
 
44
+ const {activeTabId, switcher} = model,
45
+ switcherBefore = ['left', 'top'].includes(switcher?.orientation),
46
+ switcherAfter = ['right', 'bottom'].includes(switcher?.orientation),
47
+ switcherCmp = switcher
48
+ ? tabSwitcher({key: 'switcher', testId: getTestId(testId, 'switcher'), ...switcher})
49
+ : null;
50
+
48
51
  return [
49
- switcherBefore ? tabSwitcher({key: 'switcher', ...switcher}) : null,
52
+ switcherBefore ? switcherCmp : null,
50
53
  ...tabs.map(tabModel => {
51
54
  const tabId = tabModel.id,
52
55
  style = activeTabId !== tabId ? hideStyle : undefined;
@@ -55,10 +58,10 @@ function getChildren(model: TabContainerModel) {
55
58
  className: 'xh-tab-wrapper',
56
59
  style,
57
60
  key: tabId,
58
- item: tab({model: tabModel})
61
+ item: tab({model: tabModel, testId: getTestId(testId, tabId)})
59
62
  });
60
63
  }),
61
- switcherAfter ? tabSwitcher({key: 'switcher', ...switcher}) : null
64
+ switcherAfter ? switcherCmp : null
62
65
  ];
63
66
  }
64
67
 
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout';
8
- import {hoistCmp, BoxProps, HoistProps} from '@xh/hoist/core';
8
+ import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core';
9
9
  import {button} from '@xh/hoist/desktop/cmp/button';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {Icon} from '@xh/hoist/icon';
@@ -65,6 +65,7 @@ export const [Toolbar, toolbar] = hoistCmp.withFactory<ToolbarProps>({
65
65
  enableOverflowMenu = false,
66
66
  collapseFrom = 'end',
67
67
  minVisibleItems,
68
+ testId,
68
69
  ...rest
69
70
  },
70
71
  ref
@@ -83,6 +84,7 @@ export const [Toolbar, toolbar] = hoistCmp.withFactory<ToolbarProps>({
83
84
 
84
85
  return container({
85
86
  ref,
87
+ testId,
86
88
  ...rest,
87
89
  className: classNames(
88
90
  className,
@@ -28,12 +28,13 @@ export const [SplitTreeMap, splitTreeMap] = hoistCmp.withFactory<SplitTreeMapPro
28
28
  model: uses(SplitTreeMapModel),
29
29
  className: 'xh-split-treemap',
30
30
 
31
- render({model, className, ...props}, ref) {
31
+ render({model, className, testId, ...props}, ref) {
32
32
  const {primaryMapModel, secondaryMapModel, orientation} = model,
33
33
  errors = uniq(compact([primaryMapModel.error, secondaryMapModel.error])),
34
34
  container = orientation === 'horizontal' ? hframe : vframe;
35
35
 
36
36
  return container({
37
+ testId,
37
38
  ref,
38
39
  className,
39
40
  items: errors.length ? errorPanel({errors}) : childMaps(),
@@ -9,9 +9,10 @@ import {box, div, placeholder} from '@xh/hoist/cmp/layout';
9
9
  import {
10
10
  hoistCmp,
11
11
  HoistModel,
12
- LayoutProps,
13
12
  HoistProps,
13
+ LayoutProps,
14
14
  lookup,
15
+ TestSupportProps,
15
16
  useLocalModel,
16
17
  uses,
17
18
  XH
@@ -35,7 +36,7 @@ import {assign, cloneDeep, debounce, isFunction, merge, omit} from 'lodash';
35
36
  import './TreeMap.scss';
36
37
  import {TreeMapModel} from './TreeMapModel';
37
38
 
38
- export interface TreeMapProps extends HoistProps<TreeMapModel>, LayoutProps {}
39
+ export interface TreeMapProps extends HoistProps<TreeMapModel>, LayoutProps, TestSupportProps {}
39
40
 
40
41
  /**
41
42
  * Component for rendering a TreeMap.
@@ -50,7 +51,7 @@ export const [TreeMap, treeMap] = hoistCmp.withFactory<TreeMapProps>({
50
51
  model: uses(TreeMapModel),
51
52
  className: 'xh-treemap',
52
53
 
53
- render({model, className, ...props}, ref) {
54
+ render({model, className, testId, ...props}, ref) {
54
55
  if (!Highcharts) {
55
56
  console.error(
56
57
  'Highcharts has not been imported in to this application. Please import and ' +
@@ -99,6 +100,7 @@ export const [TreeMap, treeMap] = hoistCmp.withFactory<TreeMapProps>({
99
100
  ...layoutProps,
100
101
  className: classNames(className, `xh-treemap--${impl.theme}`),
101
102
  ref,
103
+ testId,
102
104
  items
103
105
  });
104
106
  }
@@ -0,0 +1,71 @@
1
+ .xh-zone-mapper {
2
+ width: 350px;
3
+ height: 500px;
4
+
5
+ &__zone-picker {
6
+ padding: var(--xh-pad-px);
7
+ background: var(--xh-bg-alt);
8
+ border-bottom: var(--xh-border-solid);
9
+
10
+ &__zone-cell {
11
+ display: flex;
12
+ align-items: center;
13
+ overflow: hidden;
14
+ flex: 1;
15
+ min-height: 30px;
16
+ white-space: nowrap;
17
+ padding: var(--xh-pad-half-px);
18
+ background: var(--xh-bg);
19
+ border: var(--xh-border-solid);
20
+ cursor: pointer;
21
+
22
+ & > span:not(:last-child) {
23
+ margin-right: 3px;
24
+ }
25
+
26
+ &--selected {
27
+ box-shadow: var(--xh-form-field-focused-box-shadow);
28
+ background: var(--xh-grid-tree-group-bg);
29
+ }
30
+
31
+ &.tl {
32
+ font-size: var(--xh-grid-multifield-top-font-size-px);
33
+ border-radius: var(--xh-border-radius-px) 0 0 0;
34
+ }
35
+
36
+ &.tr {
37
+ justify-content: flex-end;
38
+ margin: 0 0 0 -1px;
39
+ font-size: var(--xh-grid-multifield-top-font-size-px);
40
+ border-radius: 0 var(--xh-border-radius-px) 0 0;
41
+ }
42
+
43
+ &.bl {
44
+ margin: -1px 0 0 0;
45
+ font-size: var(--xh-grid-multifield-bottom-font-size-px);
46
+ border-radius: 0 0 0 var(--xh-border-radius-px);
47
+ }
48
+
49
+ &.br {
50
+ justify-content: flex-end;
51
+ margin: -1px 0 0 -1px;
52
+ font-size: var(--xh-grid-multifield-bottom-font-size-px);
53
+ border-radius: 0 0 var(--xh-border-radius-px);
54
+ }
55
+ }
56
+ }
57
+
58
+ &__sort-picker {
59
+ flex: none !important;
60
+ border-top: var(--xh-border-solid);
61
+
62
+ .xh-panel__content .xh-hframe {
63
+ padding: var(--xh-pad-px);
64
+ align-items: center;
65
+
66
+ & > *:not(:last-child) {
67
+ margin-right: var(--xh-pad-px);
68
+ }
69
+ }
70
+ }
71
+ }