@xh/hoist 59.0.3 → 59.1.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 (64) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/admin/AdminUtils.ts +23 -0
  3. package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +2 -1
  4. package/admin/tabs/activity/feedback/FeedbackPanel.ts +3 -3
  5. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +2 -1
  6. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -2
  7. package/admin/tabs/general/config/ConfigPanelModel.ts +2 -2
  8. package/admin/tabs/general/users/UserModel.ts +2 -2
  9. package/admin/tabs/monitor/MonitorResultsModel.ts +48 -11
  10. package/admin/tabs/monitor/MonitorResultsPanel.ts +71 -8
  11. package/admin/tabs/server/connectionpool/ConnPoolMonitorModel.ts +2 -2
  12. package/admin/tabs/server/ehcache/EhCacheModel.ts +2 -2
  13. package/admin/tabs/server/environment/ServerEnvModel.ts +3 -3
  14. package/admin/tabs/server/logLevel/LogLevelPanel.ts +3 -3
  15. package/admin/tabs/server/logViewer/LogViewerModel.ts +2 -2
  16. package/admin/tabs/server/memory/MemoryMonitorModel.ts +2 -2
  17. package/admin/tabs/server/services/ServiceModel.ts +3 -3
  18. package/admin/tabs/server/websocket/WebSocketModel.ts +3 -2
  19. package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +2 -2
  20. package/admin/tabs/userData/prefs/PreferenceModel.ts +2 -2
  21. package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -3
  22. package/appcontainer/ExceptionDialogModel.ts +6 -0
  23. package/cmp/error/ErrorBoundary.ts +68 -0
  24. package/cmp/error/ErrorBoundaryModel.ts +77 -0
  25. package/cmp/markdown/Markdown.ts +32 -0
  26. package/cmp/markdown/index.ts +1 -0
  27. package/core/XH.ts +5 -9
  28. package/core/exception/ExceptionHandler.ts +15 -0
  29. package/data/cube/View.ts +14 -2
  30. package/desktop/appcontainer/AppContainer.ts +17 -3
  31. package/desktop/appcontainer/Banner.scss +25 -0
  32. package/desktop/appcontainer/Banner.ts +2 -1
  33. package/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +4 -1
  34. package/desktop/cmp/dash/container/impl/DashContainerView.ts +2 -1
  35. package/desktop/cmp/dock/impl/DockView.ts +2 -1
  36. package/desktop/cmp/error/ErrorMessage.scss +1 -0
  37. package/desktop/cmp/error/ErrorMessage.ts +61 -23
  38. package/desktop/cmp/input/Checkbox.scss +13 -0
  39. package/desktop/cmp/input/Checkbox.ts +2 -0
  40. package/desktop/cmp/modalsupport/ModalSupport.scss +2 -0
  41. package/desktop/cmp/panel/Panel.ts +37 -14
  42. package/desktop/cmp/panel/PanelModel.ts +35 -7
  43. package/desktop/cmp/panel/impl/PanelHeader.scss +5 -0
  44. package/desktop/cmp/panel/impl/PanelHeader.ts +53 -38
  45. package/desktop/cmp/tab/impl/Tab.ts +2 -1
  46. package/dynamics/desktop.ts +15 -17
  47. package/dynamics/mobile.ts +10 -8
  48. package/inspector/Inspector.scss +5 -10
  49. package/inspector/InspectorPanel.ts +2 -0
  50. package/kit/react-markdown/index.ts +11 -0
  51. package/mobile/appcontainer/AppContainer.ts +17 -3
  52. package/mobile/appcontainer/Banner.scss +25 -0
  53. package/mobile/appcontainer/Banner.ts +2 -1
  54. package/mobile/cmp/error/ErrorMessage.ts +58 -18
  55. package/mobile/cmp/navigator/PageModel.ts +1 -0
  56. package/mobile/cmp/navigator/impl/Page.ts +2 -1
  57. package/mobile/cmp/panel/Panel.ts +5 -1
  58. package/mobile/cmp/tab/impl/Tab.ts +2 -1
  59. package/package.json +5 -3
  60. package/styles/vars.scss +2 -0
  61. package/svc/AlertBannerService.ts +2 -2
  62. package/svc/GridExportService.ts +1 -1
  63. package/admin/tabs/monitor/MonitorResultsToolbar.ts +0 -66
  64. package/appcontainer/ErrorBoundary.ts +0 -36
@@ -13,7 +13,8 @@ import {
13
13
  TaskObserver,
14
14
  useContextModel,
15
15
  uses,
16
- hoistCmp
16
+ hoistCmp,
17
+ HoistModel
17
18
  } from '@xh/hoist/core';
18
19
  import {loadingIndicator} from '@xh/hoist/desktop/cmp/loadingindicator';
19
20
  import {mask} from '@xh/hoist/desktop/cmp/mask';
@@ -30,17 +31,24 @@ import './Panel.scss';
30
31
  import {PanelModel} from './PanelModel';
31
32
  import {HotkeyConfig} from '@xh/hoist/kit/blueprint';
32
33
  import {ContextMenuSpec} from '../contextmenu/ContextMenu';
34
+ import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
33
35
 
34
36
  export interface PanelProps extends HoistProps<PanelModel>, Omit<BoxProps, 'title'> {
35
37
  /** True to style panel header (if displayed) with reduced padding and font-size. */
36
38
  compactHeader?: boolean;
37
39
 
40
+ /** CSS class name specific to the panel's header. */
41
+ headerClassName?: string;
42
+
38
43
  /** Items to be added to the right-side of the panel's header. */
39
44
  headerItems?: ReactNode[];
40
45
 
41
46
  /** An icon placed at the left-side of the panel's header. */
42
47
  icon?: ReactElement;
43
48
 
49
+ /** Icon to be used when the panel is collapsed. Defaults to `icon`. */
50
+ collapsedIcon?: ReactElement;
51
+
44
52
  /** Context Menu to show on context clicking this panel. */
45
53
  contextMenu?: ContextMenuSpec;
46
54
 
@@ -118,6 +126,8 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
118
126
  icon,
119
127
  compactHeader,
120
128
  collapsedTitle,
129
+ collapsedIcon,
130
+ headerClassName,
121
131
  headerItems,
122
132
  mask: maskProp,
123
133
  loadingIndicator: loadingIndicatorProp,
@@ -145,22 +155,23 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
145
155
  const {
146
156
  resizable,
147
157
  collapsible,
148
- collapsed,
158
+ isRenderedCollapsed,
149
159
  renderMode,
150
160
  vertical,
151
161
  showSplitter,
152
162
  refreshContextModel,
153
- modalSupportModel
163
+ modalSupportModel,
164
+ errorBoundaryModel
154
165
  } = model;
155
166
 
156
- if (collapsed) {
167
+ if (isRenderedCollapsed) {
157
168
  delete layoutProps[`min${vertical ? 'Height' : 'Width'}`];
158
169
  delete layoutProps[vertical ? 'height' : 'width'];
159
170
  }
160
171
 
161
172
  let coreContents = null;
162
173
  if (
163
- !collapsed ||
174
+ !isRenderedCollapsed ||
164
175
  renderMode === 'always' ||
165
176
  (renderMode === 'lazy' && wasDisplayed.current)
166
177
  ) {
@@ -169,7 +180,7 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
169
180
  };
170
181
 
171
182
  coreContents = vframe({
172
- style: {display: collapsed ? 'none' : 'flex'},
183
+ style: {display: isRenderedCollapsed ? 'none' : 'flex'},
173
184
  items: Children.toArray([
174
185
  parseToolbar(tbar),
175
186
  ...castArray(children),
@@ -177,17 +188,30 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
177
188
  ])
178
189
  });
179
190
  }
180
- if (!collapsed) wasDisplayed.current = true;
191
+ if (!isRenderedCollapsed) wasDisplayed.current = true;
181
192
 
182
193
  // decorate with hooks (internally conditional, of course)
183
194
  coreContents = useContextMenu(coreContents, contextMenu);
184
195
  coreContents = useHotkeys(coreContents, hotkeys);
185
196
 
186
- // 3) Prepare combined layout with header above core. This is what layout props are trampolined to
197
+ // Apply error boundary to content *excluding* header and affordances.
198
+ if (errorBoundaryModel) {
199
+ coreContents = errorBoundary({model: errorBoundaryModel, item: coreContents});
200
+ }
201
+
202
+ // 3) Prepare core layout with header above core. This is what layout props are trampolined to
187
203
  let item = vbox({
188
204
  className: 'xh-panel__content',
189
205
  items: [
190
- panelHeader({title, icon, compact: compactHeader, collapsedTitle, headerItems}),
206
+ panelHeader({
207
+ title,
208
+ icon,
209
+ compact: compactHeader,
210
+ collapsedTitle,
211
+ collapsedIcon,
212
+ className: headerClassName,
213
+ headerItems
214
+ }),
191
215
  coreContents,
192
216
  parseLoadDecorator(maskProp, 'mask', contextModel),
193
217
  parseLoadDecorator(loadingIndicatorProp, 'loadingIndicator', contextModel)
@@ -195,23 +219,23 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
195
219
  ...rest
196
220
  });
197
221
 
222
+ // 4) Additional optional wrappers
198
223
  if (refreshContextModel) {
199
224
  item = refreshContextView({model: refreshContextModel, item});
200
225
  }
201
226
 
202
- // 3) Wrap in modal support if needed. Inner frame ensures className is still present in
203
- // DOM when rendered in Dialog
204
227
  if (modalSupportModel) {
205
228
  item = modalSupport({
206
229
  model: modalSupportModel,
207
230
  item: frame({
231
+ // Frame ensures className is still present when rendered in Dialog
208
232
  item,
209
233
  className: model.isModal ? className : undefined
210
234
  })
211
235
  });
212
236
  }
213
237
 
214
- // 4) Return wrapped in resizable affordances if needed, or equivalent layout box
238
+ // 5) Return wrapped in resizable affordances if needed, or equivalent layout box
215
239
  item =
216
240
  resizable || collapsible || showSplitter
217
241
  ? resizeContainer({ref, item, className})
@@ -221,9 +245,8 @@ export const [Panel, panel] = hoistCmp.withFactory<PanelProps>({
221
245
  }
222
246
  });
223
247
 
224
- function parseLoadDecorator(prop, name, contextModel) {
248
+ function parseLoadDecorator(prop: any, name: string, contextModel: HoistModel) {
225
249
  const cmp = (name === 'mask' ? mask : loadingIndicator) as any;
226
- if (!prop) return null;
227
250
  if (isValidElement(prop)) return prop;
228
251
  if (prop === true) return cmp({isDisplayed: true});
229
252
  if (prop === 'onLoad') {
@@ -25,6 +25,7 @@ import {throwIf} from '@xh/hoist/utils/js';
25
25
  import {isNil, isNumber, isString} from 'lodash';
26
26
  import {createRef} from 'react';
27
27
  import {ModalSupportConfig, ModalSupportModel} from '../modalsupport/';
28
+ import {ErrorBoundaryConfig, ErrorBoundaryModel} from '@xh/hoist/cmp/error/ErrorBoundaryModel';
28
29
 
29
30
  export interface PanelConfig {
30
31
  /** Can panel be resized? */
@@ -62,10 +63,16 @@ export interface PanelConfig {
62
63
 
63
64
  /**
64
65
  * Set to true to enable built-in support for showing panel contents in a modal, or provide a
65
- * config to further configure.
66
+ * config to further configure. Default false.
66
67
  */
67
68
  modalSupport?: boolean | ModalSupportConfig;
68
69
 
70
+ /**
71
+ * Set to true to place an ErrorBoundary around the panel, or provide a
72
+ * config to further configure. Default false.
73
+ */
74
+ errorBoundary?: boolean | ErrorBoundaryConfig;
75
+
69
76
  /** How should collapsed content be rendered? Ignored if collapsible is false. */
70
77
  renderMode?: RenderMode;
71
78
 
@@ -126,6 +133,7 @@ export class PanelModel extends HoistModel {
126
133
 
127
134
  @managed modalSupportModel: ModalSupportModel;
128
135
  @managed refreshContextModel: RefreshContextModel;
136
+ @managed errorBoundaryModel: ErrorBoundaryModel;
129
137
  @managed provider: PersistenceProvider;
130
138
 
131
139
  //----------------
@@ -136,11 +144,14 @@ export class PanelModel extends HoistModel {
136
144
  //---------------------
137
145
  // Observable State
138
146
  //---------------------
139
- /** Is the Panel rendering in a collapsed state? */
147
+ /**
148
+ * True when collapsed in its "home" location as per this model's state.
149
+ * See also {@link isRenderedCollapsed}, which takes modal state into account.
150
+ */
140
151
  @observable
141
152
  collapsed: boolean = false;
142
153
 
143
- /** Size in pixels or percents along sizing dimension. Used when object is *not* collapsed. */
154
+ /** Size in pixels or percents along sizing dimension. Used when object is *not* collapsed. */
144
155
  @bindable
145
156
  size: number | string = null;
146
157
 
@@ -157,8 +168,13 @@ export class PanelModel extends HoistModel {
157
168
  return !!this.modalSupportModel;
158
169
  }
159
170
 
171
+ /** True when both collapsed and not currently in a modal - i.e. *really* collapsed. */
172
+ get isRenderedCollapsed(): boolean {
173
+ return this.collapsed && !this.isModal;
174
+ }
175
+
160
176
  get isActive(): boolean {
161
- return !this.collapsed;
177
+ return !this.isRenderedCollapsed;
162
178
  }
163
179
 
164
180
  //-----------------
@@ -177,6 +193,7 @@ export class PanelModel extends HoistModel {
177
193
  defaultCollapsed = false,
178
194
  side,
179
195
  modalSupport = false,
196
+ errorBoundary = false,
180
197
  renderMode = 'lazy',
181
198
  refreshMode = 'onShowLazy',
182
199
  persistWith = null,
@@ -227,9 +244,10 @@ export class PanelModel extends HoistModel {
227
244
  this.refreshMode = refreshMode;
228
245
  this.showSplitter = showSplitter;
229
246
  this.showSplitterCollapseButton = showSplitterCollapseButton;
230
- this.showHeaderCollapseButton = showHeaderCollapseButton;
231
- this.showModalToggleButton = showModalToggleButton;
247
+ this.showHeaderCollapseButton = collapsible && showHeaderCollapseButton;
248
+ this.showModalToggleButton = modalSupport && showModalToggleButton;
232
249
 
250
+ // Set up various optional functionality;
233
251
  if (modalSupport) {
234
252
  this.modalSupportModel =
235
253
  modalSupport === true
@@ -237,7 +255,13 @@ export class PanelModel extends HoistModel {
237
255
  : new ModalSupportModel(modalSupport);
238
256
  }
239
257
 
240
- // Set up various optional functionality;
258
+ if (errorBoundary) {
259
+ this.errorBoundaryModel =
260
+ errorBoundary === true
261
+ ? new ErrorBoundaryModel()
262
+ : new ErrorBoundaryModel(errorBoundary);
263
+ }
264
+
241
265
  if (collapsible) {
242
266
  this.refreshContextModel = new ManagedRefreshContextModel(this);
243
267
  }
@@ -340,6 +364,10 @@ export class PanelModel extends HoistModel {
340
364
  return this.side === 'top' || this.side === 'left';
341
365
  }
342
366
 
367
+ get isCollapsedToLeftOrRight(): boolean {
368
+ return this.isRenderedCollapsed && !this.vertical;
369
+ }
370
+
343
371
  enforceSizeLimits() {
344
372
  if (this.collapsed) return;
345
373
 
@@ -38,6 +38,11 @@
38
38
  color: var(--xh-panel-title-text-color) !important;
39
39
  }
40
40
 
41
+ // Ensure text color correct for Blueprint controls, e.g. a switch in headerItems
42
+ .bp4-control {
43
+ color: var(--xh-panel-title-text-color);
44
+ }
45
+
41
46
  // Vertical mode - collapsed to left or right
42
47
  &.xh-vbox {
43
48
  width: var(--xh-title-height-px);
@@ -21,13 +21,39 @@ export const panelHeader = hoistCmp.factory({
21
21
 
22
22
  render({className, ...props}) {
23
23
  const panelModel = useContextModel(PanelModel),
24
- {collapsed, collapsible, isModal, vertical, side} = panelModel,
25
- {title, icon, compact} = props,
26
- collapsedTitle = withDefault(props.collapsedTitle, title),
27
- displayedTitle = collapsed ? collapsedTitle : title,
28
- headerItems = props.headerItems ?? [];
24
+ {
25
+ isRenderedCollapsed,
26
+ isCollapsedToLeftOrRight,
27
+ collapsible,
28
+ isModal,
29
+ side,
30
+ showModalToggleButton,
31
+ showHeaderCollapseButton
32
+ } = panelModel,
33
+ {title, icon, compact} = props;
29
34
 
30
- if (isNil(title) && isNil(icon) && isEmpty(headerItems)) return null;
35
+ // Title and icon can vary based on collapsed state.
36
+ const collapsedTitle = withDefault(props.collapsedTitle, title),
37
+ displayedTitle = (isRenderedCollapsed ? collapsedTitle : title) ?? null,
38
+ collapsedIcon = withDefault(props.collapsedIcon, icon),
39
+ displayedIcon = (isRenderedCollapsed ? collapsedIcon : icon) ?? null;
40
+
41
+ // As can headerItems, which include app-specified controls (never shown when collapsed)
42
+ // as well as (maybe) built-in modal/collapse toggle buttons.
43
+ const headerItems = props.headerItems ?? [],
44
+ displayedHeaderItems = isRenderedCollapsed ? [] : [...headerItems];
45
+
46
+ // Return null if nothing to display.
47
+ if (isNil(displayedTitle) && isNil(displayedIcon) && isEmpty(displayedHeaderItems)) {
48
+ return null;
49
+ }
50
+
51
+ if (showModalToggleButton && !isRenderedCollapsed) {
52
+ displayedHeaderItems.push(modalToggleButton());
53
+ }
54
+ if (showHeaderCollapseButton && !isModal) {
55
+ displayedHeaderItems.push(collapseToggleButton({panelModel}));
56
+ }
31
57
 
32
58
  const onDoubleClick = () => {
33
59
  if (isModal) {
@@ -41,12 +67,13 @@ export const panelHeader = hoistCmp.factory({
41
67
  sideCls = `xh-panel-header--${side}`,
42
68
  compactCls = compact ? 'xh-panel-header--compact' : null;
43
69
 
44
- // 1) Classic "top" title bar
45
- if (!collapsed || vertical || isModal) {
46
- return hbox({
47
- className: classNames(className, compactCls),
70
+ // Return a vertically oriented header if collapsed to left or right side.
71
+ if (isCollapsedToLeftOrRight) {
72
+ return vbox({
73
+ className: classNames(className, sideCls, compactCls),
74
+ flex: 1,
48
75
  items: [
49
- icon || null,
76
+ displayedIcon,
50
77
  displayedTitle
51
78
  ? box({
52
79
  className: titleCls,
@@ -54,44 +81,38 @@ export const panelHeader = hoistCmp.factory({
54
81
  item: span({className: `${titleCls}__inner`, item: displayedTitle})
55
82
  })
56
83
  : filler(),
57
- hbox({
58
- className: 'xh-panel-header__items',
59
- items: [
60
- ...(!collapsed || isModal ? headerItems : []),
61
- modalButton({panelModel}),
62
- collapseButton({panelModel})
63
- ],
64
- onDoubleClick: e => e.stopPropagation()
65
- })
84
+ ...displayedHeaderItems
66
85
  ],
67
86
  onDoubleClick
68
87
  });
69
88
  }
70
89
 
71
- // 2) ...otherwise its a narrow, sidebar
72
- return vbox({
73
- className: classNames(className, sideCls, compactCls),
74
- flex: 1,
90
+ // Return a standard horizontal header otherwise.
91
+ // Panel is expanded, modal, and/or collapsed to the top or bottom side.
92
+ return hbox({
93
+ className: classNames(className, compactCls),
75
94
  items: [
76
- collapseButton({panelModel}),
77
- icon || null,
95
+ displayedIcon,
78
96
  displayedTitle
79
97
  ? box({
80
98
  className: titleCls,
99
+ flex: 1,
81
100
  item: span({className: `${titleCls}__inner`, item: displayedTitle})
82
101
  })
83
- : null
102
+ : filler(),
103
+ hbox({
104
+ className: 'xh-panel-header__items',
105
+ items: displayedHeaderItems,
106
+ onDoubleClick: e => e.stopPropagation()
107
+ })
84
108
  ],
85
109
  onDoubleClick
86
110
  });
87
111
  }
88
112
  });
89
113
 
90
- const collapseButton = hoistCmp.factory(({panelModel}) => {
91
- const {showHeaderCollapseButton, collapsible, isModal} = panelModel as PanelModel;
92
- if (!showHeaderCollapseButton || !collapsible || isModal) return null;
93
-
94
- const {vertical, collapsed, contentFirst} = panelModel,
114
+ const collapseToggleButton = hoistCmp.factory(({panelModel}) => {
115
+ const {vertical, collapsed, contentFirst} = panelModel as PanelModel,
95
116
  directions = vertical ? ['chevronUp', 'chevronDown'] : ['chevronLeft', 'chevronRight'],
96
117
  idx = contentFirst !== collapsed ? 0 : 1,
97
118
  chevron = directions[idx];
@@ -102,9 +123,3 @@ const collapseButton = hoistCmp.factory(({panelModel}) => {
102
123
  minimal: true
103
124
  });
104
125
  });
105
-
106
- const modalButton = hoistCmp.factory(({panelModel}) => {
107
- const {showModalToggleButton, hasModalSupport} = panelModel as PanelModel;
108
- if (!showModalToggleButton || !hasModalSupport) return null;
109
- return modalToggleButton();
110
- });
@@ -9,6 +9,7 @@ import {TabModel} from '@xh/hoist/cmp/tab';
9
9
  import {hoistCmp, refreshContextView, uses} from '@xh/hoist/core';
10
10
  import {elementFromContent} from '@xh/hoist/utils/react';
11
11
  import {useRef} from 'react';
12
+ import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
12
13
 
13
14
  /**
14
15
  * Wrapper for contents to be shown within a TabContainer. This Component is used by TabContainer's
@@ -43,7 +44,7 @@ export const tab = hoistCmp.factory({
43
44
  className,
44
45
  item: refreshContextView({
45
46
  model: refreshContextModel,
46
- item: elementFromContent(content, {flex: 1})
47
+ item: errorBoundary(elementFromContent(content, {flex: 1}))
47
48
  })
48
49
  });
49
50
  }
@@ -15,37 +15,35 @@
15
15
  *
16
16
  * See the platform specific AppContainer where these implementations are actually provided.
17
17
  */
18
- export let tabContainerImpl = null;
19
- export let dockContainerImpl = null;
18
+ export let ColChooserModel = null;
19
+ export let ColumnHeaderFilterModel = null;
20
+ export let ModalSupportModel = null;
20
21
  export let colChooser = null;
21
22
  export let columnHeaderFilter = null;
23
+ export let dockContainerImpl = null;
24
+ export let errorMessage = null;
22
25
  export let gridFilterDialog = null;
23
- export let storeFilterFieldImpl = null;
24
26
  export let pinPadImpl = null;
25
-
26
- export let ColChooserModel = null;
27
- export let ColumnHeaderFilterModel = null;
27
+ export let storeFilterFieldImpl = null;
28
+ export let tabContainerImpl = null;
28
29
  export let useContextMenu = null;
29
30
 
30
- export let ModalSupportModel = null;
31
-
32
31
  /**
33
32
  * Provide implementations of functions and classes exported in this file.
34
33
  *
35
34
  * Not for Application use.
36
35
  */
37
36
  export function installDesktopImpls(impls) {
38
- tabContainerImpl = impls.tabContainerImpl;
39
- dockContainerImpl = impls.dockContainerImpl;
40
- storeFilterFieldImpl = impls.storeFilterFieldImpl;
41
- pinPadImpl = impls.pinPadImpl;
37
+ ColChooserModel = impls.ColChooserModel;
38
+ ColumnHeaderFilterModel = impls.ColumnHeaderFilterModel;
39
+ ModalSupportModel = impls.ModalSupportModel;
42
40
  colChooser = impls.colChooser;
43
41
  columnHeaderFilter = impls.columnHeaderFilter;
42
+ dockContainerImpl = impls.dockContainerImpl;
43
+ errorMessage = impls.errorMessage;
44
44
  gridFilterDialog = impls.gridFilterDialog;
45
-
46
- ColChooserModel = impls.ColChooserModel;
47
- ColumnHeaderFilterModel = impls.ColumnHeaderFilterModel;
45
+ pinPadImpl = impls.pinPadImpl;
46
+ storeFilterFieldImpl = impls.storeFilterFieldImpl;
47
+ tabContainerImpl = impls.tabContainerImpl;
48
48
  useContextMenu = impls.useContextMenu;
49
-
50
- ModalSupportModel = impls.ModalSupportModel;
51
49
  }
@@ -15,11 +15,12 @@
15
15
  *
16
16
  * See the platform specific AppContainer where these implementations are actually provided.
17
17
  */
18
- export let tabContainerImpl = null;
19
- export let storeFilterFieldImpl = null;
20
- export let pinPadImpl = null;
21
- export let colChooser = null;
22
18
  export let ColChooserModel = null;
19
+ export let colChooser = null;
20
+ export let errorMessage = null;
21
+ export let pinPadImpl = null;
22
+ export let storeFilterFieldImpl = null;
23
+ export let tabContainerImpl = null;
23
24
 
24
25
  /**
25
26
  * Provide implementations of functions and classes exported in this file.
@@ -27,9 +28,10 @@ export let ColChooserModel = null;
27
28
  * Not for Application use.
28
29
  */
29
30
  export function installMobileImpls(impls) {
30
- tabContainerImpl = impls.tabContainerImpl;
31
- storeFilterFieldImpl = impls.storeFilterFieldImpl;
32
- pinPadImpl = impls.pinPadImpl;
33
- colChooser = impls.colChooser;
34
31
  ColChooserModel = impls.ColChooserModel;
32
+ colChooser = impls.colChooser;
33
+ errorMessage = impls.errorMessage;
34
+ pinPadImpl = impls.pinPadImpl;
35
+ storeFilterFieldImpl = impls.storeFilterFieldImpl;
36
+ tabContainerImpl = impls.tabContainerImpl;
35
37
  }
@@ -1,14 +1,4 @@
1
1
  .xh-inspector {
2
- & > .xh-panel__content > .xh-panel-header {
3
- background-color: hsl(33, 93%, 40%);
4
- color: white;
5
- }
6
-
7
- .xh-modal-support__host > div > .xh-panel__content > .xh-panel-header {
8
- background-color: hsl(33, 93%, 40%);
9
- color: white;
10
- }
11
-
12
2
  .xh-font-family-mono {
13
3
  font-size: 0.9em;
14
4
  }
@@ -21,3 +11,8 @@
21
11
  background-color: var(--xh-bg);
22
12
  }
23
13
  }
14
+
15
+ .xh-inspector-panel-header {
16
+ background-color: hsl(33, 93%, 40%);
17
+ color: white;
18
+ }
@@ -26,11 +26,13 @@ export const inspectorPanel = hoistCmp.factory({
26
26
  title: `Inspector - Hoist v${XH.environmentService.get('hoistReactVersion')}`,
27
27
  icon: Icon.search(),
28
28
  className: 'xh-inspector',
29
+ headerClassName: 'xh-inspector-panel-header',
29
30
  modelConfig: {
30
31
  defaultSize: 400,
31
32
  side: 'bottom',
32
33
  persistWith: XH.inspectorService.persistWith,
33
34
  modalSupport: true,
35
+ errorBoundary: true,
34
36
  showModalToggleButton: true,
35
37
  showHeaderCollapseButton: false,
36
38
  xhImpl: true
@@ -0,0 +1,11 @@
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 {elementFactory} from '@xh/hoist/core';
8
+ import ReactMarkdown from 'react-markdown';
9
+
10
+ export {ReactMarkdown};
11
+ export const reactMarkdown = elementFactory(ReactMarkdown);
@@ -5,7 +5,7 @@
5
5
  * Copyright © 2023 Extremely Heavy Industries Inc.
6
6
  */
7
7
  import {AppContainerModel} from '@xh/hoist/appcontainer/AppContainerModel';
8
- import {errorBoundary} from '@xh/hoist/appcontainer/ErrorBoundary';
8
+ import {errorBoundary} from '@xh/hoist/cmp/error/ErrorBoundary';
9
9
  import {fragment, frame, vframe, viewport} from '@xh/hoist/cmp/layout';
10
10
  import {createElement, hoistCmp, refreshContextView, uses, XH} from '@xh/hoist/core';
11
11
  import {installMobileImpls} from '@xh/hoist/dynamics/mobile';
@@ -30,13 +30,15 @@ import {messageSource} from './MessageSource';
30
30
  import {optionsDialog} from './OptionsDialog';
31
31
  import {toastSource} from './ToastSource';
32
32
  import {versionBar} from './VersionBar';
33
+ import {errorMessage} from '../cmp/error/ErrorMessage';
33
34
 
34
35
  installMobileImpls({
35
36
  tabContainerImpl,
36
37
  storeFilterFieldImpl,
37
38
  pinPadImpl,
38
39
  colChooser,
39
- ColChooserModel
40
+ ColChooserModel,
41
+ errorMessage
40
42
  });
41
43
 
42
44
  /**
@@ -57,7 +59,19 @@ export const AppContainer = hoistCmp({
57
59
  useOnMount(() => model.initAsync());
58
60
 
59
61
  return fragment(
60
- errorBoundary(viewForState()),
62
+ errorBoundary({
63
+ modelConfig: {
64
+ errorHandler: {
65
+ title: 'Critical Error',
66
+ message:
67
+ XH.clientAppName +
68
+ ' encountered a critical error and cannot be displayed.',
69
+ requireReload: true
70
+ },
71
+ errorRenderer: () => null
72
+ },
73
+ item: viewForState()
74
+ }),
61
75
  // Modal component helpers rendered here at top-level to support display of messages
62
76
  // and exceptions at any point during the app lifecycle.
63
77
  exceptionDialog(),
@@ -20,6 +20,31 @@ body.xh-app .xh-banner {
20
20
  overflow: hidden;
21
21
  white-space: nowrap;
22
22
  text-overflow: ellipsis;
23
+
24
+ // Disallow most markdown styling except **bold**, *italics* and (links)
25
+ h1,
26
+ h2,
27
+ h3,
28
+ h4,
29
+ p,
30
+ ul,
31
+ li,
32
+ a,
33
+ code {
34
+ display: inline;
35
+ margin: unset;
36
+ padding: unset;
37
+ color: unset;
38
+ font-weight: unset;
39
+ font-size: unset;
40
+ font-family: unset;
41
+ list-style: none;
42
+ }
43
+
44
+ // Render links with an underline
45
+ a {
46
+ text-decoration: underline;
47
+ }
23
48
  }
24
49
 
25
50
  &__action-button {
@@ -9,6 +9,7 @@ import {XH, uses, hoistCmp} from '@xh/hoist/core';
9
9
  import {hframe, div} from '@xh/hoist/cmp/layout';
10
10
  import {button} from '@xh/hoist/mobile/cmp/button';
11
11
  import {Icon} from '@xh/hoist/icon';
12
+ import {markdown} from '@xh/hoist/cmp/markdown';
12
13
  import {isEmpty, isFunction} from 'lodash';
13
14
  import classNames from 'classnames';
14
15
 
@@ -41,7 +42,7 @@ export const banner = hoistCmp.factory({
41
42
  icon,
42
43
  div({
43
44
  className: 'xh-banner__message',
44
- item: message,
45
+ item: markdown({content: message}),
45
46
  onClick
46
47
  })
47
48
  ]