@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
package/core/XH.ts CHANGED
@@ -5,27 +5,7 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {RouterModel} from '@xh/hoist/appcontainer/RouterModel';
8
- import {Router, State} from 'router5';
9
- import {
10
- HoistService,
11
- AppSpec,
12
- AppState,
13
- Exception,
14
- ExceptionHandlerOptions,
15
- ExceptionHandler,
16
- TrackOptions,
17
- SizingMode,
18
- HoistServiceClass,
19
- Theme,
20
- PlainObject,
21
- HoistException,
22
- PageState,
23
- AppSuspendData,
24
- FetchResponse
25
- } from './';
26
8
  import {Store} from '@xh/hoist/data';
27
- import {instanceManager} from './impl/InstanceManager';
28
- import {installServicesAsync} from './impl/InstallServices';
29
9
  import {Icon} from '@xh/hoist/icon';
30
10
  import {action} from '@xh/hoist/mobx';
31
11
  import {never} from '@xh/hoist/promise';
@@ -35,6 +15,7 @@ import {
35
15
  ChangelogService,
36
16
  ConfigService,
37
17
  EnvironmentService,
18
+ FetchOptions,
38
19
  FetchService,
39
20
  GridAutosizeService,
40
21
  GridExportService,
@@ -45,17 +26,41 @@ import {
45
26
  LocalStorageService,
46
27
  PrefService,
47
28
  TrackService,
48
- WebSocketService,
49
- FetchOptions
29
+ WebSocketService
50
30
  } from '@xh/hoist/svc';
51
31
  import {camelCase, flatten, isString, uniqueId} from 'lodash';
32
+ import {Router, State} from 'router5';
33
+ import {CancelFn} from 'router5/types/types/base';
52
34
  import {AppContainerModel} from '../appcontainer/AppContainerModel';
53
- import {ToastModel} from '../appcontainer/ToastModel';
54
35
  import {BannerModel} from '../appcontainer/BannerModel';
36
+ import {ToastModel} from '../appcontainer/ToastModel';
55
37
  import '../styles/XH.scss';
56
- import {ModelSelector, HoistModel, RefreshContextModel} from './model';
57
- import {HoistAppModel, BannerSpec, ToastSpec, MessageSpec, HoistUser, TaskObserver} from './';
58
- import {CancelFn} from 'router5/types/types/base';
38
+ import {
39
+ AppSpec,
40
+ AppState,
41
+ AppSuspendData,
42
+ BannerSpec,
43
+ Exception,
44
+ ExceptionHandler,
45
+ ExceptionHandlerOptions,
46
+ FetchResponse,
47
+ HoistAppModel,
48
+ HoistException,
49
+ HoistService,
50
+ HoistServiceClass,
51
+ HoistUser,
52
+ MessageSpec,
53
+ PageState,
54
+ PlainObject,
55
+ SizingMode,
56
+ TaskObserver,
57
+ Theme,
58
+ ToastSpec,
59
+ TrackOptions
60
+ } from './';
61
+ import {installServicesAsync} from './impl/InstallServices';
62
+ import {instanceManager} from './impl/InstanceManager';
63
+ import {HoistModel, ModelSelector, RefreshContextModel} from './model';
59
64
  import {apiDeprecated} from '@xh/hoist/utils/js';
60
65
 
61
66
  export const MIN_HOIST_CORE_VERSION = '16.0';
@@ -659,7 +664,7 @@ export class XHApi {
659
664
  * Get a collection of Models currently 'active' in the app, returned in creation-time order.
660
665
  * This will include all models that have not yet had `destroy()` called on them.
661
666
  */
662
- getActiveModels(selector: ModelSelector = '*'): HoistModel[] {
667
+ getModels<T extends HoistModel>(selector: ModelSelector = '*'): T[] {
663
668
  const ret = [];
664
669
  instanceManager.models.forEach(m => {
665
670
  if (m.matchesSelector(selector, true)) ret.push(m);
@@ -667,6 +672,23 @@ export class XHApi {
667
672
  return ret;
668
673
  }
669
674
 
675
+ /** Get the first active model that matches the given selector, or null if none found. */
676
+ getModel<T extends HoistModel>(selector: ModelSelector = '*'): T {
677
+ instanceManager.models.forEach(m => {
678
+ if (m.matchesSelector(selector, true)) return m;
679
+ });
680
+ return null;
681
+ }
682
+
683
+ /**
684
+ * Get the first active model that has been assigned the given testId, or null if none found.
685
+ * Note that a small subset of models are automatically assigned the testId of their component.
686
+ * @see InstanceManager.testSupportedModels
687
+ */
688
+ getModelByTestId<T extends HoistModel>(testId: string): T {
689
+ return instanceManager.getModelByTestId(testId) as T;
690
+ }
691
+
670
692
  /** All services registered with this application. */
671
693
  getServices(): HoistService[] {
672
694
  return Array.from(instanceManager.services);
package/core/elem.ts CHANGED
@@ -4,14 +4,15 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {TEST_ID} from '@xh/hoist/utils/js';
7
8
  import {castArray, isFunction, isNil, isPlainObject} from 'lodash';
8
9
  import {
9
10
  createElement as reactCreateElement,
11
+ ForwardedRef,
10
12
  isValidElement,
11
- ReactNode,
13
+ Key,
12
14
  ReactElement,
13
- ForwardedRef,
14
- Key
15
+ ReactNode
15
16
  } from 'react';
16
17
  import {PlainObject, Some, Thunkable} from './types/Types';
17
18
 
@@ -60,6 +61,13 @@ export type ElementSpec<P extends PlainObject> = P & {
60
61
  /** React key for this component. */
61
62
  key?: Key;
62
63
 
64
+ /**
65
+ * Supports passing a "data-testid" prop to built-in tags (e.g. `div`), to be rendered as an
66
+ * HTML attribute. See {@link TestSupportProps} for the higher-level `testId` prop that most
67
+ * Hoist components accept and should use.
68
+ */
69
+ [TEST_ID]?: string;
70
+
63
71
  //----------------------------
64
72
  // Technical -- Escape support
65
73
  //----------------------------
@@ -5,7 +5,8 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {HoistService, HoistModel} from './..';
8
+ import {HoistService, HoistModel} from '../';
9
+ import {isNil} from 'lodash';
9
10
  import {Store} from '@xh/hoist/data';
10
11
  import {observable, makeObservable} from '@xh/hoist/mobx';
11
12
  import {wait} from '@xh/hoist/promise';
@@ -24,6 +25,9 @@ class InstanceManager {
24
25
  @observable.shallow
25
26
  stores: Set<Store> = new Set();
26
27
 
28
+ private modelsByTestId: Map<string, HoistModel> = new Map();
29
+ private testSupportedModels = new Set(['GridModel', 'DataViewModel', 'FormModel', 'TabModel']);
30
+
27
31
  registerModel(m: HoistModel) {
28
32
  wait().thenAction(() => this.models.add(m));
29
33
  }
@@ -44,6 +48,25 @@ class InstanceManager {
44
48
  wait().thenAction(() => this.stores.delete(s));
45
49
  }
46
50
 
51
+ registerModelWithTestId(testId: string, m: HoistModel) {
52
+ if (
53
+ isNil(testId) ||
54
+ !m.isHoistModel ||
55
+ !this.testSupportedModels.has(m.constructor.name) ||
56
+ this.modelsByTestId.has(testId)
57
+ )
58
+ return;
59
+ this.modelsByTestId.set(testId, m);
60
+ }
61
+
62
+ unregisterModelWithTestId(testId: string) {
63
+ this.modelsByTestId.delete(testId);
64
+ }
65
+
66
+ getModelByTestId(testId: string): HoistModel {
67
+ return this.modelsByTestId.get(testId);
68
+ }
69
+
47
70
  constructor() {
48
71
  makeObservable(this);
49
72
  }
@@ -4,13 +4,13 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {forOwn, has, isFunction} from 'lodash';
7
+ import {action, makeObservable, observable} from '@xh/hoist/mobx';
8
8
  import {warnIf} from '@xh/hoist/utils/js';
9
+ import {forOwn, has, isFunction} from 'lodash';
9
10
  import {DefaultHoistProps, HoistBase, managed, PlainObject} from '../';
10
- import {ModelSelector} from './';
11
- import {LoadSupport, LoadSpec, Loadable} from '../load';
12
- import {observable, action, makeObservable} from '@xh/hoist/mobx';
13
11
  import {instanceManager} from '../impl/InstanceManager';
12
+ import {Loadable, LoadSpec, LoadSupport} from '../load';
13
+ import {ModelSelector} from './';
14
14
 
15
15
  /**
16
16
  * Core superclass for stateful Models in Hoist. Models are used throughout the toolkit and
@@ -7,11 +7,11 @@
7
7
 
8
8
  import {isBoolean, isEmpty, isNil, isNumber, isString} from 'lodash';
9
9
  import {ReactElement} from 'react';
10
- import {Intent, PlainObject} from '../core';
10
+ import {Intent, PlainObject, TestSupportProps} from '../core';
11
11
  import {StoreRecord} from './StoreRecord';
12
- import {GridModel, Column} from '../cmp/grid';
12
+ import {Column, GridModel} from '../cmp/grid';
13
13
 
14
- export interface RecordActionSpec {
14
+ export interface RecordActionSpec extends TestSupportProps {
15
15
  /** Label to be displayed. */
16
16
  text?: string;
17
17
 
@@ -112,6 +112,7 @@ export class RecordAction {
112
112
  disabled: boolean;
113
113
  hidden: boolean;
114
114
  recordsRequired: boolean | number;
115
+ testId: string;
115
116
 
116
117
  constructor({
117
118
  text,
@@ -125,7 +126,8 @@ export class RecordAction {
125
126
  disabled = false,
126
127
  hidden = false,
127
128
  displayFn = null,
128
- recordsRequired = false
129
+ recordsRequired = false,
130
+ testId = null
129
131
  }: RecordActionSpec) {
130
132
  this.text = text;
131
133
  this.secondaryText = secondaryText;
@@ -138,6 +140,7 @@ export class RecordAction {
138
140
  this.hidden = hidden;
139
141
  this.displayFn = displayFn;
140
142
  this.recordsRequired = recordsRequired;
143
+ this.testId = testId;
141
144
 
142
145
  this.items = items?.map(it => {
143
146
  if (isString(it)) return it;
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import {PlainObject} from '@xh/hoist/core';
8
8
  import {throwIf} from '@xh/hoist/utils/js';
9
- import {isNil, flatMap} from 'lodash';
9
+ import {isNil, flatMap, isMatch} from 'lodash';
10
10
  import {Store} from './Store';
11
11
  import {ValidationState} from './validation/ValidationState';
12
12
  import {RecordValidator} from './impl/RecordValidator';
@@ -243,6 +243,13 @@ export class StoreRecord {
243
243
  this.store.getAncestorsById(this.id, fromFiltered).forEach(fn);
244
244
  }
245
245
 
246
+ /**
247
+ * Tests to see if this Record's data matches the given partial data object.
248
+ */
249
+ matchesData(partialData: PlainObject): boolean {
250
+ return isMatch(this.data, partialData);
251
+ }
252
+
246
253
  // --------------------------
247
254
  // Protected methods
248
255
  // --------------------------
@@ -13,6 +13,7 @@ import {suspendPanel} from '@xh/hoist/desktop/appcontainer/SuspendPanel';
13
13
  import {dockContainerImpl} from '@xh/hoist/desktop/cmp/dock/impl/DockContainer';
14
14
  import {colChooserDialog as colChooser} from '@xh/hoist/desktop/cmp/grid/impl/colchooser/ColChooserDialog';
15
15
  import {ColChooserModel} from '@xh/hoist/desktop/cmp/grid/impl/colchooser/ColChooserModel';
16
+ import {zoneMapperDialog as zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapperDialog';
16
17
  import {columnHeaderFilter} from '@xh/hoist/desktop/cmp/grid/impl/filter/ColumnHeaderFilter';
17
18
  import {ColumnHeaderFilterModel} from '@xh/hoist/desktop/cmp/grid/impl/filter/ColumnHeaderFilterModel';
18
19
  import {gridFilterDialog} from '@xh/hoist/desktop/cmp/grid/impl/filter/GridFilterDialog';
@@ -48,6 +49,7 @@ installDesktopImpls({
48
49
  storeFilterFieldImpl,
49
50
  pinPadImpl,
50
51
  colChooser,
52
+ zoneMapper,
51
53
  columnHeaderFilter,
52
54
  gridFilterDialog,
53
55
  ColChooserModel,
@@ -6,18 +6,18 @@
6
6
  */
7
7
 
8
8
  import {span} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, HoistProps, HSide, XH} from '@xh/hoist/core';
9
+ import {hoistCmp, HoistProps, HSide, TestSupportProps, XH} from '@xh/hoist/core';
10
10
  import {appBarSeparator} from '@xh/hoist/desktop/cmp/appbar';
11
11
  import {appMenuButton, AppMenuButtonProps, refreshButton} from '@xh/hoist/desktop/cmp/button';
12
12
  import {whatsNewButton} from '@xh/hoist/desktop/cmp/button/WhatsNewButton';
13
13
  import '@xh/hoist/desktop/register';
14
14
  import {navbar, navbarGroup} from '@xh/hoist/kit/blueprint';
15
- import {withDefault} from '@xh/hoist/utils/js';
15
+ import {TEST_ID, withDefault} from '@xh/hoist/utils/js';
16
16
  import {isEmpty} from 'lodash';
17
- import {ReactNode, ReactElement} from 'react';
17
+ import {ReactElement, ReactNode} from 'react';
18
18
  import './AppBar.scss';
19
19
 
20
- export interface AppBarProps extends HoistProps {
20
+ export interface AppBarProps extends HoistProps, TestSupportProps {
21
21
  /** Position of the AppMenuButton. */
22
22
  appMenuButtonPosition?: HSide;
23
23
 
@@ -69,7 +69,8 @@ export const [AppBar, appBar] = hoistCmp.withFactory<AppBarProps>({
69
69
  hideAppMenuButton,
70
70
  className,
71
71
  appMenuButtonProps = {},
72
- appMenuButtonPosition = 'right'
72
+ appMenuButtonPosition = 'right',
73
+ testId
73
74
  } = props;
74
75
 
75
76
  const title = withDefault(props.title, XH.clientAppName);
@@ -102,7 +103,8 @@ export const [AppBar, appBar] = hoistCmp.withFactory<AppBarProps>({
102
103
  })
103
104
  ]
104
105
  })
105
- ]
106
+ ],
107
+ [TEST_ID]: testId
106
108
  });
107
109
  }
108
110
  });
@@ -6,19 +6,28 @@
6
6
  */
7
7
  import {ButtonProps as BpButtonProps} from '@blueprintjs/core';
8
8
  import composeRefs from '@seznam/compose-react-refs';
9
- import {hoistCmp, HoistModel, HoistProps, LayoutProps, StyleProps, Intent} from '@xh/hoist/core';
9
+ import {
10
+ hoistCmp,
11
+ HoistModel,
12
+ HoistProps,
13
+ Intent,
14
+ LayoutProps,
15
+ StyleProps,
16
+ TestSupportProps
17
+ } from '@xh/hoist/core';
10
18
  import '@xh/hoist/desktop/register';
11
19
  import {button as bpButton} from '@xh/hoist/kit/blueprint';
12
- import {withDefault} from '@xh/hoist/utils/js';
20
+ import {TEST_ID, withDefault} from '@xh/hoist/utils/js';
13
21
  import {splitLayoutProps} from '@xh/hoist/utils/react';
14
22
  import classNames from 'classnames';
15
- import {ReactNode, ReactElement} from 'react';
23
+ import {ReactElement, ReactNode} from 'react';
16
24
  import './Button.scss';
17
25
 
18
26
  export interface ButtonProps<M extends HoistModel = null>
19
27
  extends HoistProps<M>,
20
28
  StyleProps,
21
29
  LayoutProps,
30
+ TestSupportProps,
22
31
  BpButtonProps {
23
32
  active?: boolean;
24
33
  autoFocus?: boolean;
@@ -69,6 +78,7 @@ export const [Button, button] = hoistCmp.withFactory<ButtonProps>({
69
78
  tooltip,
70
79
  active,
71
80
  elementRef,
81
+ testId,
72
82
  ...rest
73
83
  } = nonLayoutProps;
74
84
 
@@ -96,6 +106,7 @@ export const [Button, button] = hoistCmp.withFactory<ButtonProps>({
96
106
  autoFocus,
97
107
  className: classNames(className, classes),
98
108
  elementRef: composeRefs(ref, elementRef),
109
+ [TEST_ID]: testId,
99
110
  disabled,
100
111
  icon,
101
112
  intent,
@@ -4,17 +4,26 @@
4
4
  *
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
- import {SetOptional} from 'type-fest';
8
7
  import {ButtonGroupProps as BpButtonGroupProps} from '@blueprintjs/core';
9
- import {hoistCmp, HoistModel, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core';
8
+ import {
9
+ hoistCmp,
10
+ HoistModel,
11
+ HoistProps,
12
+ LayoutProps,
13
+ StyleProps,
14
+ TestSupportProps
15
+ } from '@xh/hoist/core';
10
16
  import '@xh/hoist/desktop/register';
11
17
  import {buttonGroup as bpButtonGroup} from '@xh/hoist/kit/blueprint';
18
+ import {TEST_ID} from '@xh/hoist/utils/js';
12
19
  import {splitLayoutProps} from '@xh/hoist/utils/react';
20
+ import {SetOptional} from 'type-fest';
13
21
 
14
22
  export interface ButtonGroupProps<M extends HoistModel = null>
15
23
  extends HoistProps<M>,
16
24
  LayoutProps,
17
25
  StyleProps,
26
+ TestSupportProps,
18
27
  SetOptional<BpButtonGroupProps, 'children'> {
19
28
  /** True to have all buttons fill available width equally. */
20
29
  fill?: boolean;
@@ -35,11 +44,13 @@ export const [ButtonGroup, buttonGroup] = hoistCmp.withFactory<ButtonGroupProps>
35
44
  className: 'xh-button-group',
36
45
 
37
46
  render(props, ref) {
38
- const [layoutProps, {fill, minimal, vertical, style, ...rest}] = splitLayoutProps(props);
47
+ const [layoutProps, {fill, minimal, vertical, style, testId, ...rest}] =
48
+ splitLayoutProps(props);
39
49
  return bpButtonGroup({
40
50
  fill,
41
51
  minimal,
42
52
  vertical,
53
+ [TEST_ID]: testId,
43
54
  style: {
44
55
  ...style,
45
56
  ...layoutProps
@@ -0,0 +1,82 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2023 Extremely Heavy Industries Inc.
6
+ */
7
+ import '@xh/hoist/desktop/register';
8
+ import {hoistCmp, useContextModel} from '@xh/hoist/core';
9
+ import {div, vbox} from '@xh/hoist/cmp/layout';
10
+ import {ZoneGridModel} from '@xh/hoist/cmp/zoneGrid';
11
+ import {ZoneMapperModel} from '@xh/hoist/cmp/zoneGrid/impl/ZoneMapperModel';
12
+ import {zoneMapper} from '@xh/hoist/desktop/cmp/zoneGrid/impl/ZoneMapper';
13
+ import {Icon} from '@xh/hoist/icon';
14
+ import {popover, Position} from '@xh/hoist/kit/blueprint';
15
+ import {stopPropagation, withDefault} from '@xh/hoist/utils/js';
16
+ import {button, ButtonProps} from './Button';
17
+
18
+ export interface ZoneMapperButtonProps extends ButtonProps {
19
+ /** ZoneGridModel of the grid for which this button should show a chooser. */
20
+ zoneGridModel?: ZoneGridModel;
21
+
22
+ /** Position for chooser popover, as per Blueprint docs. */
23
+ popoverPosition?: Position;
24
+ }
25
+
26
+ /**
27
+ * A convenience button to trigger the display of a ZoneMapper UI for ZoneGrid configuration.
28
+ *
29
+ * Requires a `ZoneGridModel.zoneMapperModel` config option, set to true for default implementation.
30
+ */
31
+ export const [ZoneMapperButton, zoneMapperButton] = hoistCmp.withFactory<ZoneMapperButtonProps>({
32
+ displayName: 'ZoneMapperButton',
33
+ model: false,
34
+ render({icon, title, zoneGridModel, popoverPosition, disabled, ...rest}, ref) {
35
+ zoneGridModel = withDefault(zoneGridModel, useContextModel(ZoneGridModel));
36
+
37
+ const mapperModel = zoneGridModel?.mapperModel as ZoneMapperModel;
38
+
39
+ if (!zoneGridModel) {
40
+ console.error(
41
+ "No ZoneGridModel available to ZoneMapperButton. Provide via a 'zoneGridModel' prop, or context."
42
+ );
43
+ disabled = true;
44
+ }
45
+
46
+ if (!mapperModel) {
47
+ console.error(
48
+ 'No ZoneMapperModel available on bound ZoneGridModel - enable via ZoneGridModel.zoneMapperModel config.'
49
+ );
50
+ disabled = true;
51
+ }
52
+
53
+ const isOpen = mapperModel?.isPopoverOpen;
54
+ return popover({
55
+ isOpen,
56
+ popoverClassName: 'xh-zone-mapper-popover xh-popup--framed',
57
+ position: withDefault(popoverPosition, 'auto'),
58
+ target: button({
59
+ icon: withDefault(icon, Icon.gridLarge()),
60
+ title: withDefault(title, 'Customize fields...'),
61
+ disabled,
62
+ ...rest
63
+ }),
64
+ disabled,
65
+ content: vbox({
66
+ onClick: stopPropagation,
67
+ onDoubleClick: stopPropagation,
68
+ items: [
69
+ div({ref, className: 'xh-popup__title', item: 'Customize Fields'}),
70
+ zoneMapper({model: mapperModel})
71
+ ]
72
+ }),
73
+ onInteraction: (nextOpenState, e) => {
74
+ if (nextOpenState) {
75
+ mapperModel.openPopover();
76
+ } else {
77
+ mapperModel.close();
78
+ }
79
+ }
80
+ });
81
+ }
82
+ });
@@ -14,3 +14,4 @@ export * from './OptionsButton';
14
14
  export * from './RefreshButton';
15
15
  export * from './RestoreDefaultsButton';
16
16
  export * from './ThemeToggleButton';
17
+ export * from './ZoneMapperButton';
@@ -7,10 +7,19 @@
7
7
  import {ContextMenu} from '@blueprintjs/core';
8
8
  import composeRefs from '@seznam/compose-react-refs';
9
9
  import {div, vbox, vspacer} from '@xh/hoist/cmp/layout';
10
- import {elementFactory, hoistCmp, HoistProps, refreshContextView, uses, XH} from '@xh/hoist/core';
10
+ import {
11
+ elementFactory,
12
+ hoistCmp,
13
+ HoistProps,
14
+ refreshContextView,
15
+ TestSupportProps,
16
+ uses,
17
+ XH
18
+ } from '@xh/hoist/core';
11
19
  import {dashCanvasAddViewButton} from '@xh/hoist/desktop/cmp/button/DashCanvasAddViewButton';
12
20
  import '@xh/hoist/desktop/register';
13
21
  import {Classes, overlay} from '@xh/hoist/kit/blueprint';
22
+ import {TEST_ID} from '@xh/hoist/utils/js';
14
23
  import {useOnVisibleChange} from '@xh/hoist/utils/react';
15
24
  import classNames from 'classnames';
16
25
  import ReactGridLayout, {WidthProvider} from 'react-grid-layout';
@@ -20,7 +29,7 @@ import {DashCanvasModel} from './DashCanvasModel';
20
29
  import {dashCanvasContextMenu} from './impl/DashCanvasContextMenu';
21
30
  import {dashCanvasView} from './impl/DashCanvasView';
22
31
 
23
- export type DashCanvasProps = HoistProps<DashCanvasModel>;
32
+ export type DashCanvasProps = HoistProps<DashCanvasModel> & TestSupportProps;
24
33
 
25
34
  /**
26
35
  * Dashboard-style container that allows users to drag-and-drop child widgets into flexible layouts.
@@ -38,7 +47,7 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
38
47
  className: 'xh-dash-canvas',
39
48
  model: uses(DashCanvasModel),
40
49
 
41
- render({className, model}, ref) {
50
+ render({className, model, testId}, ref) {
42
51
  const isDraggable = !model.layoutLocked,
43
52
  isResizable = !model.layoutLocked;
44
53
 
@@ -85,7 +94,8 @@ export const [DashCanvas, dashCanvas] = hoistCmp.withFactory<DashCanvasProps>({
85
94
  )
86
95
  }),
87
96
  emptyContainerOverlay()
88
- ]
97
+ ],
98
+ [TEST_ID]: testId
89
99
  })
90
100
  });
91
101
  }
@@ -6,7 +6,14 @@
6
6
  */
7
7
  import composeRefs from '@seznam/compose-react-refs';
8
8
  import {div, frame, vbox, vspacer} from '@xh/hoist/cmp/layout';
9
- import {hoistCmp, uses, ModelLookupContext, HoistProps, refreshContextView} from '@xh/hoist/core';
9
+ import {
10
+ hoistCmp,
11
+ HoistProps,
12
+ ModelLookupContext,
13
+ refreshContextView,
14
+ TestSupportProps,
15
+ uses
16
+ } from '@xh/hoist/core';
10
17
  import {mask} from '@xh/hoist/desktop/cmp/mask';
11
18
  import {Classes, overlay} from '@xh/hoist/kit/blueprint';
12
19
  import {useOnMount, useOnResize} from '@xh/hoist/utils/react';
@@ -15,7 +22,7 @@ import './DashContainer.scss';
15
22
  import {DashContainerModel} from './DashContainerModel';
16
23
  import {dashContainerAddViewButton} from './impl/DashContainerContextMenu';
17
24
 
18
- export type DashContainerProps = HoistProps<DashContainerModel>;
25
+ export type DashContainerProps = HoistProps<DashContainerModel> & TestSupportProps;
19
26
 
20
27
  /**
21
28
  * Display a set of child components in accordance with a DashContainerModel.
@@ -25,7 +32,7 @@ export const [DashContainer, dashContainer] = hoistCmp.withFactory<DashContainer
25
32
  model: uses(DashContainerModel),
26
33
  className: 'xh-dash-container',
27
34
 
28
- render({model, className}, ref) {
35
+ render({model, className, testId}, ref) {
29
36
  // Store current ModelLookupContext in model, to be applied in views later
30
37
  const context = useContext(ModelLookupContext);
31
38
  useOnMount(() => (model.modelLookupContext = context));
@@ -39,7 +46,7 @@ export const [DashContainer, dashContainer] = hoistCmp.withFactory<DashContainer
39
46
  return refreshContextView({
40
47
  model: model.refreshContextModel,
41
48
  item: frame(
42
- frame({className, ref}),
49
+ frame({className, ref, testId}),
43
50
  mask({spinner: true, bind: model.loadingStateTask}),
44
51
  emptyContainerOverlay()
45
52
  )
@@ -5,16 +5,15 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {div, filler, frame, hbox, p} from '@xh/hoist/cmp/layout';
8
- import {hoistCmp, HoistProps} from '@xh/hoist/core';
8
+ import {BoxProps, hoistCmp, HoistProps} from '@xh/hoist/core';
9
9
  import {button, ButtonProps} from '@xh/hoist/desktop/cmp/button';
10
10
  import '@xh/hoist/desktop/register';
11
11
  import {isNil, isString} from 'lodash';
12
12
  import {isValidElement, ReactNode} from 'react';
13
-
14
13
  import './ErrorMessage.scss';
15
14
  import {Icon} from '@xh/hoist/icon';
16
15
 
17
- export interface ErrorMessageProps extends HoistProps {
16
+ export interface ErrorMessageProps extends HoistProps, Omit<BoxProps, 'title'> {
18
17
  /**
19
18
  * If provided, will render a "Retry" button that calls this function.
20
19
  * Use `actionButtonProps` for further control over this button.
@@ -69,7 +68,8 @@ export const [ErrorMessage, errorMessage] = hoistCmp.withFactory<ErrorMessagePro
69
68
  actionFn,
70
69
  actionButtonProps,
71
70
  detailsFn,
72
- detailsButtonProps
71
+ detailsButtonProps,
72
+ ...rest
73
73
  } = props;
74
74
 
75
75
  if (isNil(error)) return null;
@@ -77,17 +77,17 @@ export const [ErrorMessage, errorMessage] = hoistCmp.withFactory<ErrorMessagePro
77
77
  if (!message) {
78
78
  if (isString(error)) {
79
79
  message = error;
80
- } else if (error.message) {
81
- message = error.message;
80
+ } else {
81
+ message = error.message || error.name || 'Unknown Error';
82
82
  }
83
83
  }
84
84
 
85
85
  if (actionFn) {
86
- actionButtonProps = {...actionButtonProps, onClick: error => actionFn(error)};
86
+ actionButtonProps = {...actionButtonProps, onClick: () => actionFn(error)};
87
87
  }
88
88
 
89
89
  if (detailsFn) {
90
- detailsButtonProps = {...detailsButtonProps, onClick: error => detailsFn(error)};
90
+ detailsButtonProps = {...detailsButtonProps, onClick: () => detailsFn(error)};
91
91
  }
92
92
 
93
93
  let buttons = [],
@@ -103,6 +103,7 @@ export const [ErrorMessage, errorMessage] = hoistCmp.withFactory<ErrorMessagePro
103
103
  return frame({
104
104
  ref,
105
105
  className,
106
+ ...rest,
106
107
  item: div({
107
108
  className: 'xh-error-message__inner',
108
109
  items: [titleCmp({title}), messageCmp({message, error}), buttonBar]