@xh/hoist 59.0.3 → 59.2.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.
- package/CHANGELOG.md +59 -2
- package/admin/AdminUtils.ts +23 -0
- package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +2 -1
- package/admin/tabs/activity/feedback/FeedbackPanel.ts +3 -3
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +2 -1
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +3 -2
- package/admin/tabs/general/alertBanner/AlertBannerModel.ts +2 -2
- package/admin/tabs/general/config/ConfigPanelModel.ts +2 -2
- package/admin/tabs/general/users/UserModel.ts +2 -2
- package/admin/tabs/monitor/MonitorResultsModel.ts +48 -11
- package/admin/tabs/monitor/MonitorResultsPanel.ts +71 -8
- package/admin/tabs/server/connectionpool/ConnPoolMonitorModel.ts +2 -2
- package/admin/tabs/server/ehcache/EhCacheModel.ts +2 -2
- package/admin/tabs/server/environment/ServerEnvModel.ts +3 -3
- package/admin/tabs/server/logLevel/LogLevelPanel.ts +3 -3
- package/admin/tabs/server/logViewer/LogViewerModel.ts +2 -2
- package/admin/tabs/server/memory/MemoryMonitorModel.ts +2 -2
- package/admin/tabs/server/services/ServiceModel.ts +3 -3
- package/admin/tabs/server/websocket/WebSocketModel.ts +3 -2
- package/admin/tabs/userData/jsonblob/JsonBlobModel.ts +2 -2
- package/admin/tabs/userData/prefs/PreferenceModel.ts +2 -2
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +3 -3
- package/appcontainer/ExceptionDialogModel.ts +6 -0
- package/cmp/ag-grid/AgGrid.scss +27 -5
- package/cmp/error/ErrorBoundary.ts +68 -0
- package/cmp/error/ErrorBoundaryModel.ts +77 -0
- package/cmp/grid/Grid.scss +10 -0
- package/cmp/grid/GridModel.ts +137 -33
- package/cmp/grid/columns/ColumnGroup.ts +11 -0
- package/cmp/markdown/Markdown.ts +32 -0
- package/cmp/markdown/index.ts +1 -0
- package/core/XH.ts +5 -9
- package/core/exception/ExceptionHandler.ts +15 -0
- package/data/RecordAction.ts +1 -1
- package/data/cube/Query.ts +37 -3
- package/data/cube/View.ts +16 -4
- package/data/cube/row/BaseRow.ts +2 -2
- package/desktop/appcontainer/AppContainer.ts +17 -3
- package/desktop/appcontainer/Banner.scss +25 -0
- package/desktop/appcontainer/Banner.ts +3 -2
- package/desktop/cmp/dash/canvas/impl/DashCanvasView.ts +4 -1
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -2
- package/desktop/cmp/dash/container/impl/DashContainerUtils.ts +4 -4
- package/desktop/cmp/dash/container/impl/DashContainerView.ts +2 -1
- package/desktop/cmp/dock/DockViewModel.ts +10 -8
- package/desktop/cmp/dock/impl/DockContainer.ts +1 -0
- package/desktop/cmp/dock/impl/DockView.ts +2 -1
- package/desktop/cmp/error/ErrorMessage.scss +1 -0
- package/desktop/cmp/error/ErrorMessage.ts +61 -23
- package/desktop/cmp/input/Checkbox.scss +13 -0
- package/desktop/cmp/input/Checkbox.ts +2 -0
- package/desktop/cmp/modalsupport/ModalSupport.scss +2 -0
- package/desktop/cmp/panel/Panel.ts +37 -14
- package/desktop/cmp/panel/PanelModel.ts +35 -7
- package/desktop/cmp/panel/impl/PanelHeader.scss +5 -0
- package/desktop/cmp/panel/impl/PanelHeader.ts +53 -38
- package/desktop/cmp/tab/impl/Tab.ts +2 -1
- package/dynamics/desktop.ts +15 -17
- package/dynamics/mobile.ts +10 -8
- package/inspector/Inspector.scss +5 -10
- package/inspector/InspectorPanel.ts +2 -0
- package/inspector/instances/InstancesModel.ts +1 -1
- package/kit/react-markdown/index.ts +11 -0
- package/mobile/appcontainer/AppContainer.ts +17 -3
- package/mobile/appcontainer/Banner.scss +25 -0
- package/mobile/appcontainer/Banner.ts +3 -2
- package/mobile/cmp/error/ErrorMessage.ts +58 -18
- package/mobile/cmp/menu/impl/Menu.scss +7 -1
- package/mobile/cmp/navigator/PageModel.ts +1 -0
- package/mobile/cmp/navigator/impl/Page.ts +2 -1
- package/mobile/cmp/panel/Panel.ts +5 -1
- package/mobile/cmp/tab/impl/Tab.ts +2 -1
- package/package.json +5 -3
- package/styles/vars.scss +2 -0
- package/svc/AlertBannerService.ts +2 -2
- package/svc/GridExportService.ts +1 -1
- package/admin/tabs/monitor/MonitorResultsToolbar.ts +0 -66
- package/appcontainer/ErrorBoundary.ts +0 -36
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
7
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
8
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
9
10
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
} from '@xh/hoist/desktop/cmp/rest';
|
|
19
20
|
import {fmtDateTime} from '@xh/hoist/format';
|
|
20
21
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
21
|
-
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
22
22
|
import {isDate} from 'lodash';
|
|
23
23
|
import {DifferModel} from '../../../differ/DifferModel';
|
|
24
24
|
import * as JBCol from './JsonBlobColumns';
|
|
@@ -46,7 +46,7 @@ export class JsonBlobModel extends HoistModel {
|
|
|
46
46
|
persistWith: this.persistWith,
|
|
47
47
|
colChooserModel: true,
|
|
48
48
|
enableExport: true,
|
|
49
|
-
exportOptions: {filename:
|
|
49
|
+
exportOptions: {filename: exportFilenameWithDate('json-blobs')},
|
|
50
50
|
selModel: 'multiple',
|
|
51
51
|
store: {
|
|
52
52
|
url: 'rest/jsonBlobAdmin',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
7
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
8
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
9
10
|
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
@@ -11,7 +12,6 @@ import {FieldSpec} from '@xh/hoist/data';
|
|
|
11
12
|
import {textArea} from '@xh/hoist/desktop/cmp/input';
|
|
12
13
|
import {addAction, deleteAction, editAction, RestGridModel} from '@xh/hoist/desktop/cmp/rest';
|
|
13
14
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
14
|
-
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
15
15
|
import {DifferModel} from '../../../differ/DifferModel';
|
|
16
16
|
import {RegroupDialogModel} from '../../../regroup/RegroupDialogModel';
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@ export class PreferenceModel extends HoistModel {
|
|
|
41
41
|
persistWith: this.persistWith,
|
|
42
42
|
colChooserModel: true,
|
|
43
43
|
enableExport: true,
|
|
44
|
-
exportOptions: {filename:
|
|
44
|
+
exportOptions: {filename: exportFilenameWithDate('prefs')},
|
|
45
45
|
selModel: 'multiple',
|
|
46
46
|
store: {
|
|
47
47
|
url: 'rest/preferenceAdmin',
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import {exportFilenameWithDate} from '@xh/hoist/admin/AdminUtils';
|
|
7
8
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
8
9
|
import * as Col from '@xh/hoist/admin/columns';
|
|
9
|
-
import {hoistCmp
|
|
10
|
+
import {hoistCmp} from '@xh/hoist/core';
|
|
10
11
|
import {FieldSpec} from '@xh/hoist/data';
|
|
11
12
|
import {restGrid, RestGridConfig} from '@xh/hoist/desktop/cmp/rest';
|
|
12
|
-
import {LocalDate} from '@xh/hoist/utils/datetime';
|
|
13
13
|
|
|
14
14
|
export const userPreferencePanel = hoistCmp.factory(() =>
|
|
15
15
|
restGrid({modelConfig: {...modelSpec, readonly: AppModel.readonly}})
|
|
@@ -22,7 +22,7 @@ const modelSpec: RestGridConfig = {
|
|
|
22
22
|
persistWith: {localStorageKey: 'xhAdminUserPreferenceState'},
|
|
23
23
|
colChooserModel: true,
|
|
24
24
|
enableExport: true,
|
|
25
|
-
exportOptions: {filename:
|
|
25
|
+
exportOptions: {filename: exportFilenameWithDate('user-prefs')},
|
|
26
26
|
selModel: 'multiple',
|
|
27
27
|
store: {
|
|
28
28
|
url: 'rest/userPreferenceAdmin',
|
|
@@ -49,6 +49,12 @@ export class ExceptionDialogModel extends HoistModel {
|
|
|
49
49
|
this.displayData = {exception, options};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
@action
|
|
53
|
+
showDetails(exception: HoistException, options: ExceptionHandlerOptions) {
|
|
54
|
+
this.show(exception, options);
|
|
55
|
+
this.openDetails();
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
@action
|
|
53
59
|
close() {
|
|
54
60
|
this.displayData = null;
|
package/cmp/ag-grid/AgGrid.scss
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
@mixin group-border($side) {
|
|
9
|
+
border-#{$side}: var(--xh-border-solid);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@mixin pinned-border {
|
|
13
|
+
border-right: 1px solid var(--xh-grid-pinned-column-border-color);
|
|
14
|
+
}
|
|
15
|
+
|
|
8
16
|
// Ag-grid installs an outer div around itself.
|
|
9
17
|
.xh-ag-grid > div {
|
|
10
18
|
width: 100%;
|
|
@@ -121,12 +129,14 @@
|
|
|
121
129
|
|
|
122
130
|
&--no-row-borders {
|
|
123
131
|
.ag-row {
|
|
124
|
-
border-
|
|
132
|
+
border-bottom: none;
|
|
133
|
+
border-top: none;
|
|
125
134
|
|
|
126
135
|
// Deliberately keep border on full-width grouped rows even when we aren't adding row borders.
|
|
127
136
|
// Without this collapsed groups (which don't stripe) blend together in a solid block.
|
|
128
137
|
&.ag-row-group.ag-full-width-row {
|
|
129
|
-
border-
|
|
138
|
+
border-bottom: 1px solid var(--xh-grid-group-border-color);
|
|
139
|
+
border-top: 1px solid var(--xh-grid-group-border-color);
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
142
|
}
|
|
@@ -188,7 +198,7 @@
|
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
.ag-cell.ag-cell-last-left-pinned:not(.ag-cell-focus) {
|
|
191
|
-
|
|
201
|
+
@include pinned-border;
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
// We use flexbox to consistently vertically center cell contents across
|
|
@@ -227,7 +237,7 @@
|
|
|
227
237
|
&--no-cell-borders {
|
|
228
238
|
.ag-cell,
|
|
229
239
|
.ag-context-menu-open .ag-cell-focus:not(.ag-cell-range-selected) {
|
|
230
|
-
border
|
|
240
|
+
border: none;
|
|
231
241
|
}
|
|
232
242
|
}
|
|
233
243
|
|
|
@@ -235,7 +245,19 @@
|
|
|
235
245
|
&--no-cell-focus {
|
|
236
246
|
.ag-has-focus {
|
|
237
247
|
.ag-cell-focus:not(.ag-cell-range-selected) {
|
|
238
|
-
border
|
|
248
|
+
border: none;
|
|
249
|
+
|
|
250
|
+
&.ag-cell.ag-cell-last-left-pinned {
|
|
251
|
+
@include pinned-border;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
&.ag-cell.xh-cell--group-border-left {
|
|
255
|
+
@include group-border(left);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
&.ag-cell.xh-cell--group-border-right {
|
|
259
|
+
@include group-border(right);
|
|
260
|
+
}
|
|
239
261
|
}
|
|
240
262
|
}
|
|
241
263
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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, hoistCmp, uses, XH} from '@xh/hoist/core';
|
|
8
|
+
import {errorMessage as desktopErrorMessage} from '@xh/hoist/dynamics/desktop';
|
|
9
|
+
import {errorMessage as mobileErrorMessage} from '@xh/hoist/dynamics/mobile';
|
|
10
|
+
import {Component, ReactNode} from 'react';
|
|
11
|
+
import {ErrorBoundaryModel} from './ErrorBoundaryModel';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A wrapper component that will catch an otherwise unhandled React lifecycle error from any child
|
|
15
|
+
* component, preventing such an error from bringing down the entire app. Upon catching an error,
|
|
16
|
+
* this comp will swap out its children with an `ErrorMessage` component (or other configured
|
|
17
|
+
* renderer), giving the user the chance to report the exception and optionally try again.
|
|
18
|
+
*
|
|
19
|
+
* This wrapper will automatically only catch and handle exceptions that occur during the React
|
|
20
|
+
* lifecycle, but applications that wish to use this component to display other caught exceptions
|
|
21
|
+
* may explicitly use it to handle those exceptions.
|
|
22
|
+
*/
|
|
23
|
+
export const [ErrorBoundary, errorBoundary] = hoistCmp.withFactory<ErrorBoundaryModel>({
|
|
24
|
+
displayName: 'ErrorBoundary',
|
|
25
|
+
model: uses(ErrorBoundaryModel, {
|
|
26
|
+
createDefault: true,
|
|
27
|
+
fromContext: false,
|
|
28
|
+
publishMode: 'limited'
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
render({model, ...props}) {
|
|
32
|
+
let {error, errorRenderer} = model;
|
|
33
|
+
|
|
34
|
+
if (!error) return reactErrorBoundary({model, ...props});
|
|
35
|
+
if (errorRenderer) return errorRenderer(error);
|
|
36
|
+
|
|
37
|
+
const cmp = XH.isMobileApp ? mobileErrorMessage : desktopErrorMessage;
|
|
38
|
+
return cmp({
|
|
39
|
+
error,
|
|
40
|
+
title: 'Unexpected error while rendering this component',
|
|
41
|
+
actionFn: () => model.clear(),
|
|
42
|
+
detailsFn: () => XH.exceptionHandler.showExceptionDetails(error)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
//------------------------------------------------------------------
|
|
48
|
+
// Standard recipe from React Docs, requires class based component
|
|
49
|
+
// See https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
|
50
|
+
//------------------------------------------------------------------
|
|
51
|
+
class ReactErrorBoundary extends Component<
|
|
52
|
+
{children: ReactNode; model: ErrorBoundaryModel},
|
|
53
|
+
{error: unknown}
|
|
54
|
+
> {
|
|
55
|
+
override state = {error: null};
|
|
56
|
+
override render() {
|
|
57
|
+
return !this.state.error ? this.props.children : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static getDerivedStateFromError(error: unknown) {
|
|
61
|
+
return {error};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override componentDidCatch(e: unknown) {
|
|
65
|
+
this.props.model.handleError(e);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const reactErrorBoundary = elementFactory(ReactErrorBoundary);
|
|
@@ -0,0 +1,77 @@
|
|
|
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 {ExceptionHandlerOptions, HoistModel, XH} from '@xh/hoist/core';
|
|
8
|
+
import {isFunction} from 'lodash';
|
|
9
|
+
import {action, makeObservable, observable} from 'mobx';
|
|
10
|
+
import {ReactNode} from 'react';
|
|
11
|
+
|
|
12
|
+
export interface ErrorBoundaryConfig {
|
|
13
|
+
/**
|
|
14
|
+
* Config for {@link XH.handleException}, or a custom function to handle any error caught by
|
|
15
|
+
* the boundary. Defaults to `{showAlert: false}`.
|
|
16
|
+
*/
|
|
17
|
+
errorHandler?: ExceptionHandlerOptions | ((e: unknown) => void);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Function to render any error caught by the boundary - return will be rendered in lieu of the
|
|
21
|
+
* component's normal children. Defaults to a platform-appropriate {@link ErrorMessage}.
|
|
22
|
+
*/
|
|
23
|
+
errorRenderer?: (e: unknown) => ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ErrorBoundaryModel extends HoistModel {
|
|
27
|
+
errorHandler: ExceptionHandlerOptions | ((e: unknown) => void);
|
|
28
|
+
errorRenderer: (e: unknown) => ReactNode;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Caught error being displayed instead of the content.
|
|
32
|
+
* Null if content rendering normally.
|
|
33
|
+
*/
|
|
34
|
+
@observable.ref error: unknown;
|
|
35
|
+
|
|
36
|
+
constructor(config?: ErrorBoundaryConfig) {
|
|
37
|
+
super();
|
|
38
|
+
makeObservable(this);
|
|
39
|
+
this.errorHandler = config?.errorHandler ?? {showAlert: false};
|
|
40
|
+
this.errorRenderer = config?.errorRenderer;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handle the exception and replace the contents of the component with a rendered error.
|
|
45
|
+
*
|
|
46
|
+
* This method does not need to be called for React Lifecycle events that occur within its
|
|
47
|
+
* rendered content - that is handled automatically by the component. It is publicly available
|
|
48
|
+
* for apps that wish to use this component to handle and display other caught exceptions.
|
|
49
|
+
*
|
|
50
|
+
* For exceptions that have already been handled, call {@link showError} instead.
|
|
51
|
+
*/
|
|
52
|
+
@action
|
|
53
|
+
handleError(e: unknown) {
|
|
54
|
+
let handler = this.errorHandler;
|
|
55
|
+
if (handler) {
|
|
56
|
+
isFunction(handler) ? handler(e) : XH.handleException(e, handler as any);
|
|
57
|
+
}
|
|
58
|
+
this.error = e;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Replace the contents of the component with a rendered error.
|
|
63
|
+
*
|
|
64
|
+
* Note that unlike {@link handleError} this method will *not* report or take any other action
|
|
65
|
+
* on the error. It is intended for use with exceptions that have already been handled.
|
|
66
|
+
*/
|
|
67
|
+
@action
|
|
68
|
+
showError(e: unknown) {
|
|
69
|
+
this.error = e;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Reset this component to clear the current error and attempt to re-render its contents. */
|
|
73
|
+
@action
|
|
74
|
+
clear() {
|
|
75
|
+
this.error = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/cmp/grid/Grid.scss
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
@use 'sass:math';
|
|
9
|
+
@use '../ag-grid/AgGrid';
|
|
9
10
|
|
|
10
11
|
.xh-grid:not(.xh-data-view) {
|
|
11
12
|
//------------------------
|
|
@@ -124,6 +125,15 @@
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
// Render left / right group borders
|
|
129
|
+
.ag-cell.xh-cell--group-border-left {
|
|
130
|
+
@include AgGrid.group-border(left);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ag-cell.xh-cell--group-border-right {
|
|
134
|
+
@include AgGrid.group-border(right);
|
|
135
|
+
}
|
|
136
|
+
|
|
127
137
|
.xh-ag-grid {
|
|
128
138
|
&--tiny {
|
|
129
139
|
.ag-cell.xh-cell--invalid::before {
|
package/cmp/grid/GridModel.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
CellClickedEvent,
|
|
9
9
|
CellContextMenuEvent,
|
|
10
10
|
CellDoubleClickedEvent,
|
|
11
|
+
ColumnEvent,
|
|
11
12
|
RowClickedEvent,
|
|
12
13
|
RowDoubleClickedEvent
|
|
13
14
|
} from '@ag-grid-community/core';
|
|
@@ -20,7 +21,8 @@ import {
|
|
|
20
21
|
GridAutosizeMode,
|
|
21
22
|
GridFilterModelConfig,
|
|
22
23
|
GridGroupSortFn,
|
|
23
|
-
TreeStyle
|
|
24
|
+
TreeStyle,
|
|
25
|
+
ColumnCellClassRuleFn
|
|
24
26
|
} from '@xh/hoist/cmp/grid';
|
|
25
27
|
import {GridFilterModel} from '@xh/hoist/cmp/grid/filter/GridFilterModel';
|
|
26
28
|
import {br, fragment} from '@xh/hoist/cmp/layout';
|
|
@@ -63,7 +65,7 @@ import {
|
|
|
63
65
|
withDefault
|
|
64
66
|
} from '@xh/hoist/utils/js';
|
|
65
67
|
import equal from 'fast-deep-equal';
|
|
66
|
-
import
|
|
68
|
+
import {
|
|
67
69
|
castArray,
|
|
68
70
|
clone,
|
|
69
71
|
cloneDeep,
|
|
@@ -72,6 +74,7 @@ import _, {
|
|
|
72
74
|
defaultsDeep,
|
|
73
75
|
every,
|
|
74
76
|
find,
|
|
77
|
+
first,
|
|
75
78
|
forEach,
|
|
76
79
|
isArray,
|
|
77
80
|
isEmpty,
|
|
@@ -81,6 +84,7 @@ import _, {
|
|
|
81
84
|
isString,
|
|
82
85
|
isUndefined,
|
|
83
86
|
keysIn,
|
|
87
|
+
last,
|
|
84
88
|
max,
|
|
85
89
|
min,
|
|
86
90
|
omit,
|
|
@@ -1179,6 +1183,10 @@ export class GridModel extends HoistModel {
|
|
|
1179
1183
|
return this.findColumn(this.columns, colId);
|
|
1180
1184
|
}
|
|
1181
1185
|
|
|
1186
|
+
getColumnGroup(groupId: string): ColumnGroup {
|
|
1187
|
+
return this.findColumnGroup(this.columns, groupId);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1182
1190
|
/** Return all leaf-level columns - i.e. excluding column groups. */
|
|
1183
1191
|
getLeafColumns(): Column[] {
|
|
1184
1192
|
return this.gatherLeaves(this.columns);
|
|
@@ -1217,6 +1225,22 @@ export class GridModel extends HoistModel {
|
|
|
1217
1225
|
this.setColumnVisible(colId, false);
|
|
1218
1226
|
}
|
|
1219
1227
|
|
|
1228
|
+
setColumnGroupVisible(groupId: string, visible: boolean) {
|
|
1229
|
+
this.applyColumnStateChanges(
|
|
1230
|
+
this.getColumnGroup(groupId)
|
|
1231
|
+
.getLeafColumns()
|
|
1232
|
+
.map(({colId}) => ({colId, hidden: !visible}))
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
showColumnGroup(groupId: string) {
|
|
1237
|
+
this.setColumnGroupVisible(groupId, true);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
hideColumnGroup(groupId: string) {
|
|
1241
|
+
this.setColumnGroupVisible(groupId, false);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1220
1244
|
/**
|
|
1221
1245
|
* Determine if a leaf-level column is currently pinned.
|
|
1222
1246
|
*
|
|
@@ -1228,7 +1252,7 @@ export class GridModel extends HoistModel {
|
|
|
1228
1252
|
return state ? state.pinned : null;
|
|
1229
1253
|
}
|
|
1230
1254
|
|
|
1231
|
-
/** Return matching leaf-level Column object from the provided collection
|
|
1255
|
+
/** Return matching leaf-level Column object from the provided collection. */
|
|
1232
1256
|
findColumn(cols: Array<Column | ColumnGroup>, colId: string): Column {
|
|
1233
1257
|
for (let col of cols) {
|
|
1234
1258
|
if (col instanceof ColumnGroup) {
|
|
@@ -1241,6 +1265,18 @@ export class GridModel extends HoistModel {
|
|
|
1241
1265
|
return null;
|
|
1242
1266
|
}
|
|
1243
1267
|
|
|
1268
|
+
/** Return matching ColumnGroup from the provided collection. */
|
|
1269
|
+
findColumnGroup(cols: Array<Column | ColumnGroup>, groupId: string): ColumnGroup {
|
|
1270
|
+
for (let col of cols) {
|
|
1271
|
+
if (col instanceof ColumnGroup) {
|
|
1272
|
+
if (col.groupId === groupId) return col;
|
|
1273
|
+
const ret = this.findColumnGroup(col.children, groupId);
|
|
1274
|
+
if (ret) return ret;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return null;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1244
1280
|
/**
|
|
1245
1281
|
* Return the current state object representation for the given colId.
|
|
1246
1282
|
*
|
|
@@ -1252,30 +1288,6 @@ export class GridModel extends HoistModel {
|
|
|
1252
1288
|
return find(this.columnState, {colId});
|
|
1253
1289
|
}
|
|
1254
1290
|
|
|
1255
|
-
buildColumn(config: ColumnGroupSpec | ColumnSpec) {
|
|
1256
|
-
// Merge leaf config with defaults.
|
|
1257
|
-
// Ensure *any* tooltip setting on column itself always wins.
|
|
1258
|
-
if (this.colDefaults && !this.isGroupSpec(config)) {
|
|
1259
|
-
let colDefaults = {...this.colDefaults};
|
|
1260
|
-
if (config.tooltip) colDefaults.tooltip = null;
|
|
1261
|
-
config = defaultsDeep({}, config, colDefaults);
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
const omit = isFunction(config.omit) ? config.omit() : config.omit;
|
|
1265
|
-
if (omit) return null;
|
|
1266
|
-
|
|
1267
|
-
if (this.isGroupSpec(config)) {
|
|
1268
|
-
const children = compact(config.children.map(c => this.buildColumn(c))) as Array<
|
|
1269
|
-
ColumnGroup | Column
|
|
1270
|
-
>;
|
|
1271
|
-
return !isEmpty(children)
|
|
1272
|
-
? new ColumnGroup(config as ColumnGroupSpec, this, children)
|
|
1273
|
-
: null;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return new Column(config, this);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
1291
|
/**
|
|
1280
1292
|
* Autosize columns to fit their contents.
|
|
1281
1293
|
*
|
|
@@ -1436,6 +1448,35 @@ export class GridModel extends HoistModel {
|
|
|
1436
1448
|
//-----------------------
|
|
1437
1449
|
// Implementation
|
|
1438
1450
|
//-----------------------
|
|
1451
|
+
private buildColumn(config: ColumnGroupSpec | ColumnSpec, borderedGroup?: ColumnGroupSpec) {
|
|
1452
|
+
// Merge leaf config with defaults.
|
|
1453
|
+
// Ensure *any* tooltip setting on column itself always wins.
|
|
1454
|
+
if (this.colDefaults && !this.isGroupSpec(config)) {
|
|
1455
|
+
let colDefaults = {...this.colDefaults};
|
|
1456
|
+
if (config.tooltip) colDefaults.tooltip = null;
|
|
1457
|
+
config = defaultsDeep({}, config, colDefaults);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const omit = isFunction(config.omit) ? config.omit() : config.omit;
|
|
1461
|
+
if (omit) return null;
|
|
1462
|
+
|
|
1463
|
+
if (this.isGroupSpec(config)) {
|
|
1464
|
+
if (config.borders) borderedGroup = config;
|
|
1465
|
+
const children = compact(
|
|
1466
|
+
config.children.map(c => this.buildColumn(c, borderedGroup))
|
|
1467
|
+
) as Array<ColumnGroup | Column>;
|
|
1468
|
+
return !isEmpty(children)
|
|
1469
|
+
? new ColumnGroup(config as ColumnGroupSpec, this, children)
|
|
1470
|
+
: null;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
if (borderedGroup) {
|
|
1474
|
+
config = this.enhanceConfigWithGroupBorders(config, borderedGroup);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return new Column(config, this);
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1439
1480
|
private async autosizeColsInternalAsync(colIds, options) {
|
|
1440
1481
|
await this.whenReadyAsync();
|
|
1441
1482
|
if (!this.isReady) return;
|
|
@@ -1543,18 +1584,16 @@ export class GridModel extends HoistModel {
|
|
|
1543
1584
|
if (isEmpty(cols)) return;
|
|
1544
1585
|
|
|
1545
1586
|
const ids = this.collectIds(cols);
|
|
1546
|
-
const nonUnique =
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
.keys();
|
|
1550
|
-
if (!nonUnique.isEmpty()) {
|
|
1587
|
+
const nonUnique = ids.filter((item, index) => ids.indexOf(item) !== index);
|
|
1588
|
+
|
|
1589
|
+
if (!isEmpty(nonUnique)) {
|
|
1551
1590
|
const msg =
|
|
1552
1591
|
`Non-unique ids: [${nonUnique}] ` +
|
|
1553
1592
|
"Use 'ColumnSpec'/'ColumnGroupSpec' configs to resolve a unique ID for each column/group.";
|
|
1554
1593
|
throw XH.exception(msg);
|
|
1555
1594
|
}
|
|
1556
1595
|
|
|
1557
|
-
const treeCols = cols.filter(it => it.isTreeColumn);
|
|
1596
|
+
const treeCols = this.gatherLeaves(cols).filter(it => it.isTreeColumn);
|
|
1558
1597
|
warnIf(
|
|
1559
1598
|
this.treeMode && treeCols.length != 1,
|
|
1560
1599
|
'Grids in treeMode should include exactly one column with isTreeColumn:true.'
|
|
@@ -1752,4 +1791,69 @@ export class GridModel extends HoistModel {
|
|
|
1752
1791
|
defaultGroupSortFn = (a, b) => {
|
|
1753
1792
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
1754
1793
|
};
|
|
1794
|
+
|
|
1795
|
+
private readonly LEFT_BORDER_CLASS = 'xh-cell--group-border-left';
|
|
1796
|
+
private readonly RIGHT_BORDER_CLASS = 'xh-cell--group-border-right';
|
|
1797
|
+
|
|
1798
|
+
private enhanceConfigWithGroupBorders(config: ColumnSpec, group: ColumnGroupSpec): ColumnSpec {
|
|
1799
|
+
return {
|
|
1800
|
+
...config,
|
|
1801
|
+
cellClassRules: {
|
|
1802
|
+
...config.cellClassRules,
|
|
1803
|
+
[this.LEFT_BORDER_CLASS]: this.createGroupBorderFn('left', group),
|
|
1804
|
+
[this.RIGHT_BORDER_CLASS]: this.createGroupBorderFn('right', group)
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
private createGroupBorderFn(
|
|
1810
|
+
side: 'left' | 'right',
|
|
1811
|
+
group: ColumnGroupSpec
|
|
1812
|
+
): ColumnCellClassRuleFn {
|
|
1813
|
+
return ({api, column, columnApi, ...ctx}) => {
|
|
1814
|
+
if (!api || !column || !columnApi) return false;
|
|
1815
|
+
|
|
1816
|
+
// Re-evaluate cell class rules when column is re-ordered
|
|
1817
|
+
// See https://www.ag-grid.com/javascript-data-grid/column-object/#reference-events
|
|
1818
|
+
if (!column['xhAppliedGroupBorderListener']) {
|
|
1819
|
+
column['xhAppliedGroupBorderListener'] = true;
|
|
1820
|
+
column.addEventListener('leftChanged', ({api, columns, source}: ColumnEvent) => {
|
|
1821
|
+
if (source === 'uiColumnMoved') api.refreshCells({columns});
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// Don't render a left-border if col is first or if prev col already has right-border
|
|
1826
|
+
if (side === 'left') {
|
|
1827
|
+
const prevCol = columnApi.getDisplayedColBefore(column);
|
|
1828
|
+
|
|
1829
|
+
if (!prevCol) return false;
|
|
1830
|
+
|
|
1831
|
+
const prevColDef = prevCol.getColDef(),
|
|
1832
|
+
prevRule = prevColDef.cellClassRules[this.RIGHT_BORDER_CLASS];
|
|
1833
|
+
if (
|
|
1834
|
+
isFunction(prevRule) &&
|
|
1835
|
+
prevRule({
|
|
1836
|
+
...ctx,
|
|
1837
|
+
api,
|
|
1838
|
+
colDef: prevColDef,
|
|
1839
|
+
column: prevCol,
|
|
1840
|
+
columnApi
|
|
1841
|
+
})
|
|
1842
|
+
) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Walk up parent groups to find "bordered" group. Return true if on relevant edge.
|
|
1848
|
+
const getter = side === 'left' ? first : last;
|
|
1849
|
+
for (let parent = column?.getParent(); parent; parent = parent.getParent()) {
|
|
1850
|
+
if (
|
|
1851
|
+
group.groupId === parent.getGroupId() &&
|
|
1852
|
+
getter(parent.getDisplayedLeafColumns()) === column
|
|
1853
|
+
) {
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1755
1859
|
}
|
|
@@ -26,6 +26,8 @@ export interface ColumnGroupSpec {
|
|
|
26
26
|
headerClass?: Some<string> | ColumnHeaderClassFn;
|
|
27
27
|
/** Horizontal alignment of header contents. */
|
|
28
28
|
headerAlign?: HAlign;
|
|
29
|
+
/** True to render borders on column group edges. */
|
|
30
|
+
borders?: boolean;
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
33
|
* "Escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note
|
|
@@ -50,6 +52,7 @@ export class ColumnGroup {
|
|
|
50
52
|
readonly headerName: ReactNode | ColumnHeaderNameFn;
|
|
51
53
|
readonly headerClass: Some<string> | ColumnHeaderClassFn;
|
|
52
54
|
readonly headerAlign: HAlign;
|
|
55
|
+
readonly borders: boolean;
|
|
53
56
|
|
|
54
57
|
/**
|
|
55
58
|
* "Escape hatch" object to pass directly to Ag-Grid for desktop implementations. Note
|
|
@@ -79,6 +82,7 @@ export class ColumnGroup {
|
|
|
79
82
|
headerClass,
|
|
80
83
|
headerAlign,
|
|
81
84
|
agOptions,
|
|
85
|
+
borders,
|
|
82
86
|
...rest
|
|
83
87
|
} = config;
|
|
84
88
|
|
|
@@ -94,6 +98,7 @@ export class ColumnGroup {
|
|
|
94
98
|
this.headerName = withDefault(headerName, genDisplayName(this.groupId));
|
|
95
99
|
this.headerClass = headerClass;
|
|
96
100
|
this.headerAlign = headerAlign;
|
|
101
|
+
this.borders = withDefault(borders, true);
|
|
97
102
|
this.children = children;
|
|
98
103
|
this.gridModel = gridModel;
|
|
99
104
|
this.agOptions = agOptions ? clone(agOptions) : {};
|
|
@@ -119,4 +124,10 @@ export class ColumnGroup {
|
|
|
119
124
|
...this.agOptions
|
|
120
125
|
};
|
|
121
126
|
}
|
|
127
|
+
|
|
128
|
+
getLeafColumns(): Column[] {
|
|
129
|
+
return this.children.flatMap(child =>
|
|
130
|
+
child instanceof Column ? child : child.getLeafColumns()
|
|
131
|
+
);
|
|
132
|
+
}
|
|
122
133
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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, HoistProps} from '@xh/hoist/core';
|
|
8
|
+
import {reactMarkdown} from '@xh/hoist/kit/react-markdown';
|
|
9
|
+
import remarkBreaks from 'remark-breaks';
|
|
10
|
+
|
|
11
|
+
interface MarkdownProps extends HoistProps {
|
|
12
|
+
/**
|
|
13
|
+
* Markdown formatted string to render.
|
|
14
|
+
*/
|
|
15
|
+
content: string;
|
|
16
|
+
|
|
17
|
+
/** True (default) to render new lines with <br/> tags. */
|
|
18
|
+
lineBreaks?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render Markdown formatted strings as HTML (e.g. **foo** becomes <strong>foo</strong>).
|
|
23
|
+
*/
|
|
24
|
+
export const [Markdown, markdown] = hoistCmp.withFactory<MarkdownProps>({
|
|
25
|
+
displayName: 'Markdown',
|
|
26
|
+
render({content, lineBreaks = true}) {
|
|
27
|
+
return reactMarkdown({
|
|
28
|
+
item: content,
|
|
29
|
+
remarkPlugins: lineBreaks ? [remarkBreaks] : null
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Markdown';
|