@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
@@ -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
+ }
@@ -0,0 +1,232 @@
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, HoistModel, lookup, managed, useLocalModel, uses} from '@xh/hoist/core';
9
+ import {div, filler, hbox, hframe, span, vbox} from '@xh/hoist/cmp/layout';
10
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
11
+ import {grid, GridModel} from '@xh/hoist/cmp/grid';
12
+ import {checkbox} from '@xh/hoist/desktop/cmp/input';
13
+ import {button} from '@xh/hoist/desktop/cmp/button';
14
+ import {select} from '@xh/hoist/desktop/cmp/input';
15
+ import {Icon} from '@xh/hoist/icon';
16
+ import {intersperse} from '@xh/hoist/utils/js';
17
+ import {isEmpty} from 'lodash';
18
+ import classNames from 'classnames';
19
+ import './ZoneMapper.scss';
20
+ import {ZoneMapperModel} from '@xh/hoist/cmp/zoneGrid/impl/ZoneMapperModel';
21
+
22
+ /**
23
+ * Hoist UI for user selection and discovery of available ZoneGrid columns, enabled via the
24
+ * `ZoneGridModel.zoneMapperModel` config option.
25
+ *
26
+ * This component displays an example of each of the four zones, with the available columns for
27
+ * the currently selected zone displayed in a list below. Users can toggle column visibility
28
+ * and labels for each zone to construct a custom layout for the grid rows.
29
+ *
30
+ * It is not necessary to manually create instances of this component within an application.
31
+ *
32
+ * @internal
33
+ */
34
+ export const [ZoneMapper, zoneMapper] = hoistCmp.withFactory<ZoneMapperModel>({
35
+ displayName: 'ZoneMapper',
36
+ model: uses(ZoneMapperModel),
37
+ className: 'xh-zone-mapper',
38
+ render({model, className}) {
39
+ const {showRestoreDefaults, isDirty} = model,
40
+ impl = useLocalModel(ZoneMapperLocalModel);
41
+
42
+ return panel({
43
+ className,
44
+ items: [zonePicker(), grid({model: impl.gridModel}), sortPicker()],
45
+ bbar: [
46
+ button({
47
+ omit: !showRestoreDefaults,
48
+ text: 'Restore Defaults',
49
+ icon: Icon.undo({className: 'xh-red'}),
50
+ onClick: () => model.restoreDefaultsAsync()
51
+ }),
52
+ filler(),
53
+ button({
54
+ text: 'Cancel',
55
+ onClick: () => model.close()
56
+ }),
57
+ button({
58
+ text: 'Save',
59
+ icon: Icon.check(),
60
+ intent: 'success',
61
+ disabled: !isDirty,
62
+ onClick: () => {
63
+ model.commit();
64
+ model.close();
65
+ }
66
+ })
67
+ ]
68
+ });
69
+ }
70
+ });
71
+
72
+ const zonePicker = hoistCmp.factory<ZoneMapperModel>({
73
+ render({model}) {
74
+ const {leftFlex, rightFlex} = model,
75
+ className = 'xh-zone-mapper__zone-picker';
76
+
77
+ return vbox({
78
+ className,
79
+ items: [
80
+ hbox({
81
+ className: `${className}__top`,
82
+ items: [
83
+ zoneCell({zone: 'tl', flex: leftFlex}),
84
+ zoneCell({zone: 'tr', flex: rightFlex})
85
+ ]
86
+ }),
87
+ hbox({
88
+ className: `${className}__bottom`,
89
+ items: [
90
+ zoneCell({zone: 'bl', flex: leftFlex}),
91
+ zoneCell({zone: 'br', flex: rightFlex})
92
+ ]
93
+ })
94
+ ]
95
+ });
96
+ }
97
+ });
98
+
99
+ const zoneCell = hoistCmp.factory<ZoneMapperModel>({
100
+ render({model, zone, flex}) {
101
+ const {selectedZone, delimiter} = model,
102
+ className = 'xh-zone-mapper__zone-picker__zone-cell',
103
+ samples = model.getSamplesForZone(zone);
104
+
105
+ return div({
106
+ className: classNames(
107
+ className,
108
+ zone,
109
+ selectedZone === zone ? `${className}--selected` : null
110
+ ),
111
+ style: {flex},
112
+ onClick: () => (model.selectedZone = zone),
113
+ items: intersperse(samples, span(delimiter))
114
+ });
115
+ }
116
+ });
117
+
118
+ const sortPicker = hoistCmp.factory<ZoneMapperModel>({
119
+ render({model}) {
120
+ return panel({
121
+ className: 'xh-zone-mapper__sort-picker',
122
+ title: 'Sorting',
123
+ icon: Icon.list(),
124
+ compactHeader: true,
125
+ items: hframe(
126
+ select({
127
+ bind: 'sortByColId',
128
+ enableFilter: true,
129
+ flex: 1,
130
+ options: model.sortByOptions
131
+ }),
132
+ button({
133
+ text: model.getSortLabel(),
134
+ icon: model.getSortIcon(),
135
+ width: 80,
136
+ minimal: false,
137
+ onClick: () => model.setNextSortBy()
138
+ })
139
+ )
140
+ });
141
+ }
142
+ });
143
+
144
+ class ZoneMapperLocalModel extends HoistModel {
145
+ override xhImpl = true;
146
+ @lookup(ZoneMapperModel) model: ZoneMapperModel;
147
+
148
+ @managed
149
+ gridModel: GridModel;
150
+
151
+ override onLinked() {
152
+ super.onLinked();
153
+
154
+ this.gridModel = this.createGridModel();
155
+
156
+ this.addReaction({
157
+ track: () => [
158
+ this.model.isOpen,
159
+ this.model.isPopoverOpen,
160
+ this.model.mappings,
161
+ this.model.selectedZone
162
+ ],
163
+ run: () => this.syncGrid(),
164
+ fireImmediately: true
165
+ });
166
+ }
167
+
168
+ private createGridModel(): GridModel {
169
+ const {model} = this,
170
+ {groupColumns, fields} = model,
171
+ hasGrouping = groupColumns && fields.some(it => it.chooserGroup);
172
+
173
+ return new GridModel({
174
+ store: {idSpec: 'field'},
175
+ groupBy: hasGrouping ? 'chooserGroup' : null,
176
+ colDefaults: {movable: false, resizable: false, sortable: false},
177
+ selModel: 'disabled',
178
+ columns: [
179
+ {
180
+ field: 'displayName',
181
+ headerName: 'Field',
182
+ flex: 1
183
+ },
184
+ {
185
+ field: 'show',
186
+ align: 'center',
187
+ renderer: (value, {record}) => {
188
+ const {field} = record.data;
189
+ return checkbox({value, onChange: () => model.toggleShown(field)});
190
+ }
191
+ },
192
+ {
193
+ field: 'showLabel',
194
+ headerName: 'Label',
195
+ align: 'center',
196
+ renderer: (value, {record}) => {
197
+ const {label, field} = record.data;
198
+ if (!label) return null;
199
+ return checkbox({value, onChange: () => model.toggleShowLabel(field)});
200
+ }
201
+ },
202
+ // Hidden
203
+ {field: 'field', hidden: true},
204
+ {field: 'label', hidden: true},
205
+ {field: 'chooserGroup', hidden: true}
206
+ ]
207
+ });
208
+ }
209
+
210
+ private syncGrid() {
211
+ const {fields, mappings, limits, selectedZone} = this.model,
212
+ mapping = mappings[selectedZone],
213
+ limit = limits?.[selectedZone],
214
+ data = [];
215
+
216
+ // 1) Determine which fields are shown and labeled for the zone
217
+ const allowedFields = !isEmpty(limit?.only)
218
+ ? fields.filter(it => limit.only.includes(it.field))
219
+ : fields;
220
+
221
+ allowedFields.forEach(f => {
222
+ const fieldMapping = mapping.find(it => f.field === it.field),
223
+ show = !!fieldMapping,
224
+ showLabel = fieldMapping?.showLabel ?? false;
225
+
226
+ data.push({...f, show, showLabel});
227
+ });
228
+
229
+ // 2) Load into display grid
230
+ this.gridModel.loadData(data);
231
+ }
232
+ }
@@ -0,0 +1,35 @@
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 {hoistCmp, uses} from '@xh/hoist/core';
8
+ import {Icon} from '@xh/hoist/icon';
9
+ import {dialog} from '@xh/hoist/kit/blueprint';
10
+ import {ZoneMapperModel} from '@xh/hoist/cmp/zoneGrid/impl/ZoneMapperModel';
11
+ import {zoneMapper} from './ZoneMapper';
12
+
13
+ export const zoneMapperDialog = hoistCmp.factory({
14
+ model: uses(ZoneMapperModel),
15
+ className: 'xh-zone-mapper-dialog',
16
+
17
+ render({model, className}) {
18
+ const {isOpen} = model;
19
+ if (!isOpen) return null;
20
+
21
+ return dialog({
22
+ className,
23
+ icon: Icon.gridLarge(),
24
+ title: 'Customize Fields',
25
+ isOpen: true,
26
+ onClose: () => model.close(),
27
+ item: zoneMapper({model}),
28
+ // Size determined by inner component
29
+ style: {
30
+ width: 'unset',
31
+ height: 'unset'
32
+ }
33
+ });
34
+ }
35
+ });
@@ -19,6 +19,7 @@ export let ColChooserModel = null;
19
19
  export let ColumnHeaderFilterModel = null;
20
20
  export let ModalSupportModel = null;
21
21
  export let colChooser = null;
22
+ export let zoneMapper = null;
22
23
  export let columnHeaderFilter = null;
23
24
  export let dockContainerImpl = null;
24
25
  export let errorMessage = null;
@@ -38,6 +39,7 @@ export function installDesktopImpls(impls) {
38
39
  ColumnHeaderFilterModel = impls.ColumnHeaderFilterModel;
39
40
  ModalSupportModel = impls.ModalSupportModel;
40
41
  colChooser = impls.colChooser;
42
+ zoneMapper = impls.zoneMapper;
41
43
  columnHeaderFilter = impls.columnHeaderFilter;
42
44
  dockContainerImpl = impls.dockContainerImpl;
43
45
  errorMessage = impls.errorMessage;
@@ -17,6 +17,7 @@
17
17
  */
18
18
  export let ColChooserModel = null;
19
19
  export let colChooser = null;
20
+ export let zoneMapper = null;
20
21
  export let errorMessage = null;
21
22
  export let pinPadImpl = null;
22
23
  export let storeFilterFieldImpl = null;
@@ -30,6 +31,7 @@ export let tabContainerImpl = null;
30
31
  export function installMobileImpls(impls) {
31
32
  ColChooserModel = impls.ColChooserModel;
32
33
  colChooser = impls.colChooser;
34
+ zoneMapper = impls.zoneMapper;
33
35
  errorMessage = impls.errorMessage;
34
36
  pinPadImpl = impls.pinPadImpl;
35
37
  storeFilterFieldImpl = impls.storeFilterFieldImpl;
@@ -32,7 +32,7 @@ export class InstancesModel extends HoistModel {
32
32
  instancesPanelModel: PanelModel;
33
33
 
34
34
  get statsModel(): StatsModel {
35
- return XH.getActiveModels(StatsModel)[0] as StatsModel;
35
+ return XH.getModels(StatsModel)[0] as StatsModel;
36
36
  }
37
37
 
38
38
  get selectedSyncRun() {
@@ -170,7 +170,7 @@ export class InstancesModel extends HoistModel {
170
170
  getInstance(xhId: string): HoistBase {
171
171
  if (!xhId) return null;
172
172
  return (
173
- head(XH.getActiveModels(it => it.xhId === xhId)) ??
173
+ head(XH.getModels(it => it.xhId === xhId)) ??
174
174
  XH.getServices().find(it => it.xhId === xhId) ??
175
175
  XH.getStores().find(it => it.xhId === xhId)
176
176
  );
@@ -11,6 +11,7 @@ import {createElement, hoistCmp, refreshContextView, uses, XH} from '@xh/hoist/c
11
11
  import {installMobileImpls} from '@xh/hoist/dynamics/mobile';
12
12
  import {colChooser} from '@xh/hoist/mobile/cmp/grid/impl/ColChooser';
13
13
  import {ColChooserModel} from '@xh/hoist/mobile/cmp/grid/impl/ColChooserModel';
14
+ import {zoneMapper} from '@xh/hoist/mobile/cmp/zoneGrid/impl/ZoneMapper';
14
15
  import {mask} from '@xh/hoist/mobile/cmp/mask';
15
16
  import {storeFilterFieldImpl} from '@xh/hoist/mobile/cmp/store/impl/StoreFilterField';
16
17
  import {tabContainerImpl} from '@xh/hoist/mobile/cmp/tab/impl/TabContainer';
@@ -38,6 +39,7 @@ installMobileImpls({
38
39
  pinPadImpl,
39
40
  colChooser,
40
41
  ColChooserModel,
42
+ zoneMapper,
41
43
  errorMessage
42
44
  });
43
45
 
@@ -0,0 +1,41 @@
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 {hoistCmp, useContextModel} from '@xh/hoist/core';
8
+ import {ZoneGridModel} from '../../../cmp/zoneGrid';
9
+ import {button, ButtonProps} from '@xh/hoist/mobile/cmp/button';
10
+ import {Icon} from '@xh/hoist/icon';
11
+ import {withDefault} from '@xh/hoist/utils/js';
12
+ import '@xh/hoist/mobile/register';
13
+
14
+ export interface ZoneMapperButtonProps extends ButtonProps {
15
+ /** ZoneGridModel of the grid for which this button should show a chooser. */
16
+ zoneGridModel?: ZoneGridModel;
17
+ }
18
+
19
+ /**
20
+ * A convenience button to trigger the display of a ZoneMapper UI for ZoneGrid configuration.
21
+ *
22
+ * Requires a `ZoneGridModel.zoneMapperModel` config option, set to true for default implementation.
23
+ */
24
+ export const [ZoneMapperButton, zoneMapperButton] = hoistCmp.withFactory<ZoneMapperButtonProps>({
25
+ displayName: 'ZoneMapperButton',
26
+ model: false,
27
+ render({zoneGridModel, icon = Icon.gridLarge(), onClick, ...props}) {
28
+ zoneGridModel = withDefault(zoneGridModel, useContextModel(ZoneGridModel));
29
+
30
+ if (!zoneGridModel) {
31
+ console.error(
32
+ "No ZoneGridModel available to ZoneMapperButton. Provide via a 'zoneGridModel' prop, or context."
33
+ );
34
+ return button({icon, disabled: true, ...props});
35
+ }
36
+
37
+ onClick = onClick ?? (() => zoneGridModel.showMapper());
38
+
39
+ return button({icon, onClick, ...props});
40
+ }
41
+ });
@@ -15,3 +15,4 @@ export * from './LogoutButton';
15
15
  export * from './RefreshButton';
16
16
  export * from './RestoreDefaultsButton';
17
17
  export * from './NavigatorBackButton';
18
+ export * from './ZoneMapperButton';
@@ -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 = [],
@@ -107,6 +107,7 @@
107
107
 
108
108
  &__fullscreen-wrapper {
109
109
  position: absolute !important;
110
+ z-index: inherit;
110
111
  top: 0;
111
112
  left: 0;
112
113
  right: 0;
@@ -57,6 +57,12 @@ export interface SelectProps extends HoistProps, HoistInputProps, LayoutProps {
57
57
  */
58
58
  enableFullscreen?: boolean;
59
59
 
60
+ /**
61
+ * Optional override for fullscreen z-index. Useful for enabling fullscreen from
62
+ * within components that have a higher z-index.
63
+ */
64
+ fullScreenZIndex?: number;
65
+
60
66
  /**
61
67
  * Function called to filter available options for a given query string input.
62
68
  * Used for filtering of options provided by `options` prop when `enableFilter` is true.
@@ -531,6 +537,7 @@ class SelectInputModel extends HoistInputModel {
531
537
  portal.id = FULLSCREEN_PORTAL_ID;
532
538
  document.body.appendChild(portal);
533
539
  }
540
+ portal.style.zIndex = withDefault(this.componentProps.fullScreenZIndex, null);
534
541
  return portal;
535
542
  }
536
543