@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
@@ -4,22 +4,23 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {PopoverPosition, PopperBoundary} from '@blueprintjs/core';
7
8
  import composeRefs from '@seznam/compose-react-refs/composeRefs';
8
- import {FieldModel, FormContext, FormContextType, BaseFormFieldProps} from '@xh/hoist/cmp/form';
9
+ import {BaseFormFieldProps, FieldModel, FormContext, FormContextType} from '@xh/hoist/cmp/form';
9
10
  import {box, div, label as labelEl, li, span, ul} from '@xh/hoist/cmp/layout';
10
11
  import {DefaultHoistProps, hoistCmp, HSide, uses, XH} from '@xh/hoist/core';
11
12
  import '@xh/hoist/desktop/register';
13
+ import {instanceManager} from '@xh/hoist/core/impl/InstanceManager';
12
14
  import {fmtDate, fmtDateTime, fmtJson, fmtNumber} from '@xh/hoist/format';
13
15
  import {Icon} from '@xh/hoist/icon';
14
16
  import {tooltip} from '@xh/hoist/kit/blueprint';
15
17
  import {isLocalDate} from '@xh/hoist/utils/datetime';
16
- import {errorIf, throwIf, withDefault} from '@xh/hoist/utils/js';
17
- import {getLayoutProps, getReactElementName} from '@xh/hoist/utils/react';
18
+ import {errorIf, getTestId, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
19
+ import {getLayoutProps, getReactElementName, useOnMount, useOnUnmount} from '@xh/hoist/utils/react';
18
20
  import classNames from 'classnames';
19
21
  import {isBoolean, isDate, isEmpty, isFinite, isNil, isUndefined, kebabCase} from 'lodash';
20
22
  import {Children, cloneElement, ReactElement, ReactNode, useContext, useState} from 'react';
21
23
  import './FormField.scss';
22
- import {PopoverPosition, PopperBoundary} from '@blueprintjs/core';
23
24
 
24
25
  export interface FormFieldProps extends BaseFormFieldProps {
25
26
  /**
@@ -162,10 +163,18 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
162
163
  if (disabled) classes.push('xh-form-field-disabled');
163
164
  if (displayNotValid) classes.push('xh-form-field-invalid');
164
165
 
166
+ const testId = getFormFieldTestId(props, formContext, model.name);
167
+ useOnMount(() => instanceManager.registerModelWithTestId(testId, model));
168
+ useOnUnmount(() => instanceManager.unregisterModelWithTestId(testId));
169
+
165
170
  // generate actual element child to render
166
171
  let childEl: ReactElement =
167
172
  !child || readonly
168
- ? readonlyChild({model, readonlyRenderer})
173
+ ? readonlyChild({
174
+ model,
175
+ readonlyRenderer,
176
+ testId: getTestId(testId, 'readonly-display')
177
+ })
169
178
  : editableChild({
170
179
  model,
171
180
  child,
@@ -174,7 +183,8 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
174
183
  disabled,
175
184
  displayNotValid,
176
185
  leftErrorIcon,
177
- commitOnChange
186
+ commitOnChange,
187
+ testId: getTestId(testId, 'input')
178
188
  });
179
189
 
180
190
  if (minimal) {
@@ -195,6 +205,7 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
195
205
  key: model?.xhId,
196
206
  className: classNames(className, classes),
197
207
  ...getLayoutProps(props),
208
+ testId,
198
209
  items: [
199
210
  labelEl({
200
211
  omit: !label,
@@ -236,9 +247,13 @@ export const [FormField, formField] = hoistCmp.withFactory<FormFieldProps>({
236
247
  const readonlyChild = hoistCmp.factory({
237
248
  model: false,
238
249
 
239
- render({model, readonlyRenderer}) {
250
+ render({model, readonlyRenderer, testId}) {
240
251
  const value = model ? model['value'] : null;
241
- return div({className: 'xh-form-field-readonly-display', item: readonlyRenderer(value)});
252
+ return div({
253
+ className: 'xh-form-field-readonly-display',
254
+ [TEST_ID]: testId,
255
+ item: readonlyRenderer(value)
256
+ });
242
257
  }
243
258
  });
244
259
 
@@ -253,7 +268,8 @@ const editableChild = hoistCmp.factory<FieldModel>({
253
268
  disabled,
254
269
  displayNotValid,
255
270
  leftErrorIcon,
256
- commitOnChange
271
+ commitOnChange,
272
+ testId
257
273
  }) {
258
274
  const {props} = child;
259
275
 
@@ -263,7 +279,8 @@ const editableChild = hoistCmp.factory<FieldModel>({
263
279
  bind: 'value',
264
280
  id: childId,
265
281
  disabled: props.disabled || disabled,
266
- ref: composeRefs(model?.boundInputRef, child.ref)
282
+ ref: composeRefs(model?.boundInputRef, child.ref),
283
+ testId: props.testId ?? testId
267
284
  };
268
285
 
269
286
  // If a sizeable child input doesn't specify its own dimensions,
@@ -347,3 +364,10 @@ function defaultProp(
347
364
  const fieldDefault = formContext.fieldDefaults ? formContext.fieldDefaults[name] : null;
348
365
  return withDefault(props[name], fieldDefault, defaultVal);
349
366
  }
367
+ function getFormFieldTestId(
368
+ props: Partial<FormFieldProps>,
369
+ formContext: FormContextType,
370
+ fieldName: string
371
+ ): string {
372
+ return props.testId ?? (formContext.testId ? `${formContext.testId}-${fieldName}` : undefined);
373
+ }
@@ -8,7 +8,7 @@ import {ColumnSpec} from '@xh/hoist/cmp/grid/columns';
8
8
  import {RecordAction} from '@xh/hoist/data';
9
9
  import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button';
10
10
  import '@xh/hoist/desktop/register';
11
- import {throwIf} from '@xh/hoist/utils/js';
11
+ import {getTestId, throwIf} from '@xh/hoist/utils/js';
12
12
  import classNames from 'classnames';
13
13
  import {isEmpty} from 'lodash';
14
14
 
@@ -64,6 +64,7 @@ export const actionCol: ColumnSpec = {
64
64
  if (hidden) return null;
65
65
 
66
66
  return button({
67
+ testId: getTestId(action.testId, `${record.id}`),
67
68
  icon,
68
69
  disabled,
69
70
  tooltip,
@@ -43,7 +43,7 @@ export const colChooser = hoistCmp.factory<ColChooserProps>({
43
43
  filler(),
44
44
  button({
45
45
  omit: !showRestoreDefaults,
46
- text: 'Restore Grid Defaults',
46
+ text: 'Restore Defaults',
47
47
  icon: Icon.undo({className: 'xh-red'}),
48
48
  onClick: () => model.restoreDefaultsAsync()
49
49
  }),
@@ -57,7 +57,8 @@ export const colChooser = hoistCmp.factory<ColChooserProps>({
57
57
  button({
58
58
  omit: commitOnChange,
59
59
  text: 'Save',
60
- icon: Icon.check({className: 'xh-green'}),
60
+ icon: Icon.check(),
61
+ intent: 'success',
61
62
  onClick: () => {
62
63
  model.commit();
63
64
  model.close();
@@ -8,12 +8,13 @@ import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
8
8
  import {box, div, filler, fragment, hbox, vbox} from '@xh/hoist/cmp/layout';
9
9
  import {hoistCmp, uses} from '@xh/hoist/core';
10
10
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
11
- import {select, MENU_PORTAL_ID} from '@xh/hoist/desktop/cmp/input';
11
+ import {select} from '@xh/hoist/desktop/cmp/input';
12
12
  import {panel} from '@xh/hoist/desktop/cmp/panel';
13
13
  import '@xh/hoist/desktop/register';
14
14
  import {Icon} from '@xh/hoist/icon';
15
15
  import {menu, menuDivider, menuItem, popover} from '@xh/hoist/kit/blueprint';
16
16
  import {dragDropContext, draggable, droppable} from '@xh/hoist/kit/react-beautiful-dnd';
17
+ import {elemWithin, getTestId, TEST_ID} from '@xh/hoist/utils/js';
17
18
  import {splitLayoutProps} from '@xh/hoist/utils/react';
18
19
  import classNames from 'classnames';
19
20
  import {compact, isEmpty, sortBy} from 'lodash';
@@ -58,6 +59,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
58
59
  popoverTitle = 'Group By',
59
60
  popoverPosition = 'bottom',
60
61
  styleButtonAsInput = true,
62
+ testId,
61
63
  ...rest
62
64
  },
63
65
  ref
@@ -65,7 +67,10 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
65
67
  const {editorIsOpen, favoritesIsOpen, persistFavorites, value, allowEmpty} = model,
66
68
  isOpen = editorIsOpen || favoritesIsOpen,
67
69
  label = isEmpty(value) && allowEmpty ? emptyText : model.getValueLabel(value),
68
- [layoutProps, buttonProps] = splitLayoutProps(rest);
70
+ [layoutProps, buttonProps] = splitLayoutProps(rest),
71
+ favoritesMenuTestId = getTestId(testId, 'favorites-menu'),
72
+ favoritesIconTestId = getTestId(testId, 'favorites-icon'),
73
+ editorTestId = getTestId(testId, 'editor');
69
74
 
70
75
  return box({
71
76
  ref,
@@ -91,21 +96,28 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
91
96
  ),
92
97
  minimal: styleButtonAsInput,
93
98
  ...buttonProps,
94
- onClick: () => model.toggleEditor()
99
+ onClick: () => model.toggleEditor(),
100
+ testId
95
101
  }),
96
- favoritesIcon()
102
+ favoritesIcon({testId: favoritesIconTestId})
97
103
  ),
98
104
  content: favoritesIsOpen
99
- ? favoritesMenu()
105
+ ? favoritesMenu({testId: favoritesMenuTestId})
100
106
  : editorIsOpen
101
- ? editor({popoverWidth, popoverMinHeight, popoverTitle, emptyText})
107
+ ? editor({
108
+ popoverWidth,
109
+ popoverMinHeight,
110
+ popoverTitle,
111
+ emptyText,
112
+ testId: editorTestId
113
+ })
102
114
  : null,
103
115
  onInteraction: (nextOpenState, e) => {
104
116
  if (
105
117
  isOpen &&
106
118
  nextOpenState === false &&
107
119
  e?.target &&
108
- !targetIsControlButtonOrPortal(e.target)
120
+ !elemWithin(e.target, 'xh-grouping-chooser-button--with-favorites')
109
121
  ) {
110
122
  model.commitPendingValueAndClose();
111
123
  }
@@ -119,7 +131,7 @@ export const [GroupingChooser, groupingChooser] = hoistCmp.withFactory<GroupingC
119
131
  // Editor
120
132
  //------------------
121
133
  const editor = hoistCmp.factory<GroupingChooserModel>({
122
- render({popoverWidth, popoverMinHeight, popoverTitle, emptyText}) {
134
+ render({popoverWidth, popoverMinHeight, popoverTitle, emptyText, testId}) {
123
135
  return panel({
124
136
  width: popoverWidth,
125
137
  minHeight: popoverMinHeight,
@@ -128,7 +140,8 @@ const editor = hoistCmp.factory<GroupingChooserModel>({
128
140
  dimensionList({emptyText}),
129
141
  addDimensionControl(),
130
142
  filler()
131
- ]
143
+ ],
144
+ testId
132
145
  });
133
146
  }
134
147
  });
@@ -292,32 +305,16 @@ function getDimOptions(dims, model) {
292
305
  return sortBy(ret, 'label');
293
306
  }
294
307
 
295
- function targetIsControlButtonOrPortal(target) {
296
- const selectPortal = document.getElementById(MENU_PORTAL_ID)?.contains(target),
297
- selectClick = targetWithin(target, 'xh-select__single-value'),
298
- editorClick = targetWithin(target, 'xh-grouping-chooser-button--with-favorites');
299
- return selectPortal || selectClick || editorClick;
300
- }
301
-
302
- /**
303
- * Determines whether any of the target's parents have a specific class name
304
- */
305
- function targetWithin(target, className): boolean {
306
- for (let elem = target; elem; elem = elem.parentElement) {
307
- if (elem.classList.contains(className)) return true;
308
- }
309
- return false;
310
- }
311
-
312
308
  //------------------
313
309
  // Favorites
314
310
  //------------------
315
311
  const favoritesIcon = hoistCmp.factory<GroupingChooserModel>({
316
- render({model}) {
312
+ render({model, testId}) {
317
313
  if (!model.persistFavorites) return null;
318
314
  return div({
319
315
  item: Icon.favorite(),
320
316
  className: 'xh-grouping-chooser__favorite-icon',
317
+ [TEST_ID]: testId,
321
318
  onClick: e => {
322
319
  model.toggleFavoritesMenu();
323
320
  e.stopPropagation();
@@ -327,7 +324,7 @@ const favoritesIcon = hoistCmp.factory<GroupingChooserModel>({
327
324
  });
328
325
 
329
326
  const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
330
- render({model}) {
327
+ render({model, testId}) {
331
328
  const options = model.favoritesOptions,
332
329
  isFavorite = model.isFavorite(model.value),
333
330
  omitAdd = isEmpty(model.value) || isFavorite,
@@ -349,7 +346,10 @@ const favoritesMenu = hoistCmp.factory<GroupingChooserModel>({
349
346
  })
350
347
  );
351
348
 
352
- return vbox(div({className: 'xh-popup__title', item: 'Favorites'}), menu({items}));
349
+ return vbox({
350
+ testId,
351
+ items: [div({className: 'xh-popup__title', item: 'Favorites'}), menu({items})]
352
+ });
353
353
  }
354
354
  });
355
355
 
@@ -10,7 +10,7 @@ import {Button, buttonGroup, ButtonGroupProps} from '@xh/hoist/desktop/cmp/butto
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {throwIf, warnIf, withDefault} from '@xh/hoist/utils/js';
12
12
  import {getLayoutProps, getNonLayoutProps} from '@xh/hoist/utils/react';
13
- import {isEmpty, filter, without, castArray} from 'lodash';
13
+ import {castArray, filter, isEmpty, without} from 'lodash';
14
14
  import {Children, cloneElement, isValidElement} from 'react';
15
15
 
16
16
  export interface ButtonGroupInputProps
@@ -5,10 +5,10 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
- import {hoistCmp, StyleProps, HoistProps, HSide} from '@xh/hoist/core';
8
+ import {hoistCmp, HoistProps, HSide, StyleProps} from '@xh/hoist/core';
9
9
  import '@xh/hoist/desktop/register';
10
10
  import {checkbox as bpCheckbox} 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 {isNil} from 'lodash';
13
13
  import {ReactNode} from 'react';
14
14
 
@@ -73,8 +73,8 @@ const cmp = hoistCmp.factory<CheckboxInputModel>(({model, className, ...props},
73
73
  inline: withDefault(props.inline, true),
74
74
  label: props.label,
75
75
  tabIndex: props.tabIndex,
76
-
77
76
  id: props.id,
77
+ [TEST_ID]: props.testId,
78
78
  className,
79
79
  style: props.style,
80
80
 
@@ -35,8 +35,8 @@ import 'codemirror/addon/selection/mark-selection.js';
35
35
  import 'codemirror/lib/codemirror.css';
36
36
  import 'codemirror/theme/dracula.css';
37
37
  import {compact, defaultsDeep, isEqual, isFunction} from 'lodash';
38
- import {findDOMNode} from 'react-dom';
39
38
  import {ReactElement} from 'react';
39
+ import {findDOMNode} from 'react-dom';
40
40
  import './CodeInput.scss';
41
41
 
42
42
  export interface CodeInputProps extends HoistProps, HoistInputProps, LayoutProps {
@@ -455,6 +455,7 @@ const cmp = hoistCmp.factory<CodeInputModel>(({model, className, ...props}, ref)
455
455
  item: modalSupport({
456
456
  model: model.modalSupportModel,
457
457
  item: inputCmp({
458
+ testId: props.testId,
458
459
  width: '100%',
459
460
  height: '100%',
460
461
  className,
@@ -4,28 +4,28 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {PopperBoundary, PopperModifiers} from '@blueprintjs/core';
8
+ import {ITimePickerProps} from '@blueprintjs/datetime';
7
9
  import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input';
8
10
  import {div} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, HSide, Some, HoistProps, LayoutProps} from '@xh/hoist/core';
11
+ import {hoistCmp, HoistProps, HSide, LayoutProps, Some} from '@xh/hoist/core';
10
12
  import {button, buttonGroup} from '@xh/hoist/desktop/cmp/button';
11
- import {TextInputModel, textInput} from '@xh/hoist/desktop/cmp/input';
13
+ import {textInput, TextInputModel} from '@xh/hoist/desktop/cmp/input';
12
14
  import '@xh/hoist/desktop/register';
13
15
  import {fmtDate} from '@xh/hoist/format';
14
16
  import {Icon} from '@xh/hoist/icon';
15
17
  import {datePicker as bpDatePicker, popover, Position} from '@xh/hoist/kit/blueprint';
16
- import {makeObservable, bindable} from '@xh/hoist/mobx';
18
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
17
19
  import {wait} from '@xh/hoist/promise';
18
20
  import {isLocalDate, LocalDate} from '@xh/hoist/utils/datetime';
19
- import {consumeEvent, warnIf, withDefault} from '@xh/hoist/utils/js';
21
+ import {consumeEvent, getTestId, warnIf, withDefault} from '@xh/hoist/utils/js';
20
22
  import {getLayoutProps} from '@xh/hoist/utils/react';
21
23
  import classNames from 'classnames';
22
24
  import {assign, castArray, clone, trim} from 'lodash';
23
25
  import moment from 'moment';
24
26
  import {createRef, ReactElement, ReactNode} from 'react';
25
- import './DateInput.scss';
26
- import {PopperBoundary, PopperModifiers} from '@blueprintjs/core';
27
- import {ITimePickerProps} from '@blueprintjs/datetime';
28
27
  import {DayPickerProps} from 'react-day-picker';
28
+ import './DateInput.scss';
29
29
 
30
30
  export interface DateInputProps extends HoistProps, LayoutProps, HoistInputProps {
31
31
  value?: Date | LocalDate;
@@ -375,123 +375,128 @@ class DateInputModel extends HoistInputModel {
375
375
  }
376
376
  }
377
377
 
378
- const cmp = hoistCmp.factory<DateInputModel>(({model, className, ...props}, ref) => {
379
- warnIf(
380
- (props.enableClear || props.enablePicker) && props.rightElement,
381
- 'Cannot specify enableClear or enablePicker along with custom rightElement - built-in clear/picker button will not be shown.'
382
- );
383
-
384
- const enablePicker = props.enablePicker ?? true,
385
- enableTextInput = props.enableTextInput ?? true,
386
- enableClear = props.enableClear ?? false,
387
- disabled = props.disabled ?? false,
388
- isClearable = model.internalValue !== null,
389
- isOpen = enablePicker && model.popoverOpen && !disabled;
390
-
391
- const buttons = buttonGroup({
392
- padding: 0,
393
- items: [
394
- button({
395
- className: 'xh-date-input__clear-icon',
396
- omit: !enableClear || !isClearable || disabled,
397
- icon: Icon.cross(),
398
- tabIndex: -1,
399
- onClick: model.onClearBtnClick
400
- }),
401
- button({
402
- className: classNames(
403
- 'xh-date-input__picker-icon',
404
- enablePicker ? null : 'xh-date-input__picker-icon--disabled'
405
- ),
406
- icon: Icon.calendar(),
407
- tabIndex: enableTextInput || disabled ? -1 : undefined,
408
- elementRef: model.buttonRef,
409
- onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null
410
- })
411
- ]
412
- });
413
- const rightElement = withDefault(props.rightElement, buttons);
414
-
415
- let {minDate, maxDate, initialMonth, renderValue} = model;
416
-
417
- // If app has set an out-of-range date, we render it -- these bounds govern *manual* entry
418
- // But need to relax constraints on the picker, to prevent BP from breaking badly
419
- if (renderValue) {
420
- if (minDate && renderValue < minDate) minDate = renderValue;
421
- if (maxDate && renderValue > maxDate) maxDate = renderValue;
422
- }
423
-
424
- // BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead
425
- if (!initialMonth && !renderValue) {
426
- const today = new Date();
427
- if (minDate && today < minDate) initialMonth = minDate;
428
- if (maxDate && today > maxDate) initialMonth = maxDate;
429
- }
430
-
431
- return div({
432
- className: 'xh-date-input__wrapper',
433
- item: popover({
434
- isOpen,
435
- minimal: true,
436
- usePortal: true,
437
- autoFocus: false,
438
- enforceFocus: false,
439
- modifiers: props.popoverModifiers,
440
- position: props.popoverPosition ?? 'auto',
441
- boundary: props.popoverBoundary ?? 'viewport',
442
- portalContainer: props.portalContainer ?? document.body,
443
- popoverRef: model.popoverRef,
444
- onClose: model.onPopoverClose,
445
- onInteraction: nextOpenState => {
446
- if (props.showPickerOnFocus) {
447
- model.popoverOpen = nextOpenState;
448
- } else if (!nextOpenState) {
449
- model.popoverOpen = false;
450
- }
451
- },
452
-
453
- content: bpDatePicker({
454
- value: renderValue,
455
- onChange: model.onDatePickerChange,
456
- maxDate,
457
- minDate,
458
- initialMonth,
459
- showActionsBar: props.showActionsBar,
460
- dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps),
461
- timePrecision: model.timePrecision,
462
- timePickerProps: model.timePrecision
463
- ? assign({selectAllOnFocus: true}, props.timePickerProps)
464
- : undefined
465
- }),
378
+ const cmp = hoistCmp.factory<DateInputProps & {model: DateInputModel}>(
379
+ ({model, className, ...props}, ref) => {
380
+ warnIf(
381
+ (props.enableClear || props.enablePicker) && props.rightElement,
382
+ 'Cannot specify enableClear or enablePicker along with custom rightElement - built-in clear/picker button will not be shown.'
383
+ );
466
384
 
467
- item: div({
468
- item: textInput({
469
- value: model.formatDate(renderValue) as string,
385
+ const enablePicker = props.enablePicker ?? true,
386
+ enableTextInput = props.enableTextInput ?? true,
387
+ enableClear = props.enableClear ?? false,
388
+ disabled = props.disabled ?? false,
389
+ isClearable = model.internalValue !== null,
390
+ isOpen = enablePicker && model.popoverOpen && !disabled;
391
+
392
+ const buttons = buttonGroup({
393
+ padding: 0,
394
+ items: [
395
+ button({
396
+ className: 'xh-date-input__clear-icon',
397
+ omit: !enableClear || !isClearable || disabled,
398
+ icon: Icon.cross(),
399
+ tabIndex: -1,
400
+ onClick: model.onClearBtnClick,
401
+ testId: getTestId(props, 'clear')
402
+ }),
403
+ button({
470
404
  className: classNames(
471
- className,
472
- !enableTextInput && !disabled ? 'xh-date-input--picker-only' : null
405
+ 'xh-date-input__picker-icon',
406
+ enablePicker ? null : 'xh-date-input__picker-icon--disabled'
473
407
  ),
474
- onCommit: model.onInputCommit,
475
- onChange: model.onInputChange,
476
- onKeyDown: model.onInputKeyDown,
477
- rightElement,
478
- disabled: disabled || !enableTextInput,
479
- leftIcon: props.leftIcon,
480
- tabIndex: props.tabIndex,
481
- placeholder: props.placeholder,
482
- textAlign: props.textAlign,
483
- selectOnFocus: props.selectOnFocus,
484
- inputRef: model.inputRef,
485
- ref: model.textInputRef,
486
- ...getLayoutProps(props)
408
+ icon: Icon.calendar(),
409
+ tabIndex: enableTextInput || disabled ? -1 : undefined,
410
+ elementRef: model.buttonRef,
411
+ onClick: enablePicker && !disabled ? model.onOpenPopoverClick : null,
412
+ testId: getTestId(props, 'picker')
413
+ })
414
+ ]
415
+ });
416
+ const rightElement = withDefault(props.rightElement, buttons);
417
+
418
+ let {minDate, maxDate, initialMonth, renderValue} = model;
419
+
420
+ // If app has set an out-of-range date, we render it -- these bounds govern *manual* entry
421
+ // But need to relax constraints on the picker, to prevent BP from breaking badly
422
+ if (renderValue) {
423
+ if (minDate && renderValue < minDate) minDate = renderValue;
424
+ if (maxDate && renderValue > maxDate) maxDate = renderValue;
425
+ }
426
+
427
+ // BP chooses annoying mid-point if forced to guess initial month. Use closest bound instead
428
+ if (!initialMonth && !renderValue) {
429
+ const today = new Date();
430
+ if (minDate && today < minDate) initialMonth = minDate;
431
+ if (maxDate && today > maxDate) initialMonth = maxDate;
432
+ }
433
+
434
+ return div({
435
+ className: 'xh-date-input__wrapper',
436
+ item: popover({
437
+ isOpen,
438
+ minimal: true,
439
+ usePortal: true,
440
+ autoFocus: false,
441
+ enforceFocus: false,
442
+ modifiers: props.popoverModifiers,
443
+ position: props.popoverPosition ?? 'auto',
444
+ boundary: props.popoverBoundary ?? 'viewport',
445
+ portalContainer: props.portalContainer ?? document.body,
446
+ popoverRef: model.popoverRef,
447
+ onClose: model.onPopoverClose,
448
+ onInteraction: nextOpenState => {
449
+ if (props.showPickerOnFocus) {
450
+ model.popoverOpen = nextOpenState;
451
+ } else if (!nextOpenState) {
452
+ model.popoverOpen = false;
453
+ }
454
+ },
455
+
456
+ content: bpDatePicker({
457
+ value: renderValue,
458
+ onChange: model.onDatePickerChange,
459
+ maxDate,
460
+ minDate,
461
+ initialMonth,
462
+ showActionsBar: props.showActionsBar,
463
+ dayPickerProps: assign({fixedWeeks: true}, props.dayPickerProps),
464
+ timePrecision: model.timePrecision,
465
+ timePickerProps: model.timePrecision
466
+ ? assign({selectAllOnFocus: true}, props.timePickerProps)
467
+ : undefined
487
468
  }),
488
- className: 'xh-date-input__click-target',
489
- onClick: !enableTextInput && !disabled ? model.onOpenPopoverClick : null
490
- })
491
- }),
492
- onBlur: model.onBlur,
493
- onFocus: model.onFocus,
494
- onKeyDown: model.onKeyDown,
495
- ref
496
- });
497
- });
469
+
470
+ item: div({
471
+ item: textInput({
472
+ value: model.formatDate(renderValue) as string,
473
+ className: classNames(
474
+ className,
475
+ !enableTextInput && !disabled ? 'xh-date-input--picker-only' : null
476
+ ),
477
+ onCommit: model.onInputCommit,
478
+ onChange: model.onInputChange,
479
+ onKeyDown: model.onInputKeyDown,
480
+ rightElement,
481
+ disabled: disabled || !enableTextInput,
482
+ leftIcon: props.leftIcon,
483
+ tabIndex: props.tabIndex,
484
+ placeholder: props.placeholder,
485
+ textAlign: props.textAlign,
486
+ selectOnFocus: props.selectOnFocus,
487
+ inputRef: model.inputRef,
488
+ ref: model.textInputRef,
489
+ testId: getTestId(props),
490
+ ...getLayoutProps(props)
491
+ }),
492
+ className: 'xh-date-input__click-target',
493
+ onClick: !enableTextInput && !disabled ? model.onOpenPopoverClick : null
494
+ })
495
+ }),
496
+ onBlur: model.onBlur,
497
+ onFocus: model.onFocus,
498
+ onKeyDown: model.onKeyDown,
499
+ ref
500
+ });
501
+ }
502
+ );
@@ -6,9 +6,9 @@
6
6
  */
7
7
  import {hoistCmp} from '@xh/hoist/core';
8
8
  import '@xh/hoist/desktop/register';
9
+ import {fmtJson} from '@xh/hoist/format';
9
10
  import * as codemirror from 'codemirror';
10
11
  import 'codemirror/mode/javascript/javascript';
11
- import {fmtJson} from '@xh/hoist/format';
12
12
  import {codeInput, CodeInputProps} from './CodeInput';
13
13
  import {jsonlint} from './impl/jsonlint';
14
14
 
@@ -6,12 +6,12 @@
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, HoistProps, StyleProps, LayoutProps, HSide} from '@xh/hoist/core';
9
+ import {hoistCmp, HoistProps, HSide, LayoutProps, StyleProps} from '@xh/hoist/core';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {fmtNumber, parseNumber} from '@xh/hoist/format';
12
12
  import {numericInput} from '@xh/hoist/kit/blueprint';
13
13
  import {wait} from '@xh/hoist/promise';
14
- import {apiRemoved, debounced, throwIf, withDefault} from '@xh/hoist/utils/js';
14
+ import {apiRemoved, debounced, TEST_ID, throwIf, withDefault} from '@xh/hoist/utils/js';
15
15
  import {getLayoutProps} from '@xh/hoist/utils/react';
16
16
  import {isNaN, isNil, isNumber, round} from 'lodash';
17
17
  import {ReactElement, ReactNode, Ref, useLayoutEffect} from 'react';
@@ -275,6 +275,7 @@ const cmp = hoistCmp.factory<NumberInputModel>(({model, className, ...props}, re
275
275
  flex: withDefault(flex, null),
276
276
  textAlign: withDefault(props.textAlign, 'right')
277
277
  },
278
+ [TEST_ID]: props.testId,
278
279
  onBlur: model.onBlur,
279
280
  onFocus: model.onFocus,
280
281
  onKeyDown: model.onKeyDown,