@xh/hoist 44.1.0 → 44.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/.nvmrc +1 -0
- package/CHANGELOG.md +25 -0
- package/admin/tabs/server/logViewer/LogDisplayModel.js +5 -3
- package/appcontainer/ViewportSizeModel.js +19 -1
- package/cmp/ag-grid/AgGrid.scss +54 -52
- package/cmp/form/field/BaseFieldModel.js +1 -1
- package/cmp/grid/Grid.js +39 -8
- package/cmp/grid/GridAutosizeMode.js +9 -9
- package/cmp/grid/GridModel.js +37 -5
- package/cmp/grid/impl/GridPersistenceModel.js +10 -9
- package/cmp/grid/index.js +1 -1
- package/core/XH.js +2 -2
- package/desktop/appcontainer/AppContainer.js +8 -1
- package/desktop/appcontainer/ExceptionDialog.js +1 -1
- package/desktop/appcontainer/IdlePanel.scss +2 -1
- package/desktop/cmp/contextmenu/StoreContextMenu.js +7 -8
- package/desktop/cmp/dash/DashContainer.scss +9 -0
- package/desktop/cmp/dash/DashContainerModel.js +12 -0
- package/desktop/cmp/dash/impl/DashContainerContextMenu.js +1 -1
- package/desktop/cmp/dash/impl/DashContainerMenuButton.js +34 -0
- package/desktop/cmp/grid/editors/SelectEditor.js +5 -0
- package/desktop/cmp/input/Select.scss +1 -0
- package/desktop/cmp/panel/impl/dragger/DraggerModel.js +14 -0
- package/mobile/appcontainer/ExceptionDialog.js +1 -1
- package/mobile/cmp/header/AppBar.js +2 -3
- package/package.json +1 -1
- package/svc/FetchService.js +31 -7
- package/svc/GridExportService.js +71 -40
- package/utils/js/Decorators.js +4 -4
- package/utils/js/LogUtils.js +119 -0
- package/utils/js/index.js +1 -1
- package/utils/js/WithDebug.js +0 -104
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lts/*
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v44.2.0 - 2021-12-07
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
|
|
7
|
+
* Desktop inline grid editor `Select` now commits the value immediately on selection.
|
|
8
|
+
* `DashContainerModel` now supports an observable `showMenuButton` config which will display a
|
|
9
|
+
button in the stack header for showing the context menu
|
|
10
|
+
* Added `GridAutosizeMode.MANAGED` to autosize Grid columns on data or `sizingMode` changes, unless
|
|
11
|
+
the user has manually modified their column widths.
|
|
12
|
+
* Copying from Grids to the clipboard will now use the value provided by the `exportValue`
|
|
13
|
+
property on the column.
|
|
14
|
+
* Refresh application hotkey is now built into hoist's global hotkeys (shift + r).
|
|
15
|
+
* Non-SSO applications will now automatically reload when a request fails due to session timeout.
|
|
16
|
+
* New utility methods `withInfo` and `logInfo` provide variants of the existing `withDebug` and
|
|
17
|
+
`logDebug` methods, but log at the more verbose `console.log` level.
|
|
18
|
+
|
|
19
|
+
### 🐞 Bug Fixes
|
|
20
|
+
|
|
21
|
+
* Desktop panel splitter can now be dragged over an `iframe` and reliably resize the panel.
|
|
22
|
+
* Ensure scrollbar does not appear on multi-select in toolbar when not needed.
|
|
23
|
+
* `XH.isPortrait` property fixed so that it no longer changes due to the appearance of the
|
|
24
|
+
mobile keyboard.
|
|
25
|
+
|
|
26
|
+
[Commit Log](https://github.com/xh/hoist-react/compare/v44.1.0...v44.2.0)
|
|
27
|
+
|
|
3
28
|
## v44.1.0 - 2021-11-08
|
|
4
29
|
|
|
5
30
|
### 🎁 New Features
|
|
@@ -43,10 +43,12 @@ export class LogDisplayModel extends HoistModel {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
syncTail() {
|
|
46
|
-
const {tail} = this.parent
|
|
47
|
-
rowElem = this[tail ? 'lastRowRef' : 'firstRowRef'].current;
|
|
46
|
+
const {tail} = this.parent;
|
|
48
47
|
|
|
49
|
-
if (
|
|
48
|
+
if (tail) {
|
|
49
|
+
const rowElem = this.lastRowRef.current;
|
|
50
|
+
if (rowElem) rowElem.scrollIntoView();
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async doLoadAsync(loadSpec) {
|
|
@@ -21,7 +21,14 @@ export class ViewportSizeModel extends HoistModel {
|
|
|
21
21
|
/** @returns {boolean} */
|
|
22
22
|
@computed
|
|
23
23
|
get isPortrait() {
|
|
24
|
-
|
|
24
|
+
const {size} = this; // Force triggering observation of size.
|
|
25
|
+
|
|
26
|
+
// Check Modern API and legacy API (for safari, BB Access)
|
|
27
|
+
let orientation = this.getOrientation() ?? this.getLegacyOrientation();
|
|
28
|
+
if (orientation !== null) return orientation === 0 || orientation === 180;
|
|
29
|
+
|
|
30
|
+
// Default to aspect ratio
|
|
31
|
+
return size.width < size.height;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/** @returns {boolean} */
|
|
@@ -46,4 +53,15 @@ export class ViewportSizeModel extends HoistModel {
|
|
|
46
53
|
height: window.innerHeight
|
|
47
54
|
};
|
|
48
55
|
}
|
|
56
|
+
|
|
57
|
+
getOrientation() {
|
|
58
|
+
const {orientation} = window.screen;
|
|
59
|
+
return orientation ? orientation.angle : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getLegacyOrientation() {
|
|
63
|
+
const {orientation} = window;
|
|
64
|
+
return isFinite(orientation) ? orientation: null;
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
}
|
package/cmp/ag-grid/AgGrid.scss
CHANGED
|
@@ -349,14 +349,24 @@
|
|
|
349
349
|
.ag-set-filter-select-all {
|
|
350
350
|
padding: var(--xh-pad-half-px) 0;
|
|
351
351
|
}
|
|
352
|
-
}
|
|
353
352
|
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
.ag-
|
|
358
|
-
|
|
359
|
-
|
|
353
|
+
//------------------------
|
|
354
|
+
// Overlay masks
|
|
355
|
+
//------------------------
|
|
356
|
+
.ag-overlay {
|
|
357
|
+
// Match loading overlay color to mask, suppress display of text in center.
|
|
358
|
+
.ag-overlay-loading-wrapper {
|
|
359
|
+
background-color: var(--xh-mask-bg);
|
|
360
|
+
|
|
361
|
+
.ag-overlay-loading-center {
|
|
362
|
+
display: none;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Reduce default contrast of emptyText.
|
|
367
|
+
.ag-overlay-no-rows-wrapper {
|
|
368
|
+
color: var(--xh-grid-empty-text-color);
|
|
369
|
+
}
|
|
360
370
|
}
|
|
361
371
|
}
|
|
362
372
|
|
|
@@ -371,58 +381,50 @@
|
|
|
371
381
|
}
|
|
372
382
|
}
|
|
373
383
|
|
|
374
|
-
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
.ag-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
.ag-overlay-loading-center {
|
|
383
|
-
display: none;
|
|
384
|
+
body.xh-app {
|
|
385
|
+
// Ensure ag-grid popups (context menus, tooltips, and column filter controls) with default z-index of 5
|
|
386
|
+
// appear over bp dialogs with z-index of 20.
|
|
387
|
+
.ag-theme-balham.ag-popup,
|
|
388
|
+
.ag-theme-balham-dark.ag-popup {
|
|
389
|
+
.ag-popup-child {
|
|
390
|
+
z-index: 30;
|
|
384
391
|
}
|
|
385
392
|
}
|
|
386
393
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
//------------------------
|
|
396
|
-
.ag-theme-balham .ag-menu,
|
|
397
|
-
.ag-theme-balham-dark .ag-menu {
|
|
398
|
-
font-family: var(--xh-font-family);
|
|
399
|
-
font-size: var(--xh-menu-item-font-size-px);
|
|
400
|
-
color: var(--xh-menu-item-text-color);
|
|
394
|
+
//------------------------
|
|
395
|
+
// Context Menu
|
|
396
|
+
//------------------------
|
|
397
|
+
.ag-theme-balham .ag-menu,
|
|
398
|
+
.ag-theme-balham-dark .ag-menu {
|
|
399
|
+
font-family: var(--xh-font-family);
|
|
400
|
+
font-size: var(--xh-menu-item-font-size-px);
|
|
401
|
+
color: var(--xh-menu-item-text-color);
|
|
401
402
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
// Minimal/high-contrast bg
|
|
404
|
+
background-color: var(--xh-menu-bg);
|
|
405
|
+
border: var(--xh-menu-border);
|
|
405
406
|
|
|
406
|
-
|
|
407
|
-
|
|
407
|
+
// Matching box-shadow of Blueprint context menu popover
|
|
408
|
+
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.2), 0 2px 4px rgba(16, 22, 26, 0.4), 0 8px 24px rgba(16, 22, 26, 0.4);
|
|
408
409
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
410
|
+
.ag-menu-option.ag-menu-option-active {
|
|
411
|
+
background-color: var(--xh-menu-item-highlight-bg);
|
|
412
|
+
}
|
|
412
413
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
.ag-menu-option-icon {
|
|
415
|
+
text-align: center;
|
|
416
|
+
padding-left: 8px;
|
|
417
|
+
padding-right: 8px;
|
|
418
|
+
}
|
|
418
419
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
420
|
+
// Mute the shortcut text to keep focus on the actual option text
|
|
421
|
+
.ag-menu-option-shortcut {
|
|
422
|
+
color: var(--xh-text-color-muted);
|
|
423
|
+
}
|
|
423
424
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
// Keep the submenu caret in the standard text color
|
|
426
|
+
.ag-menu-option-popup-pointer {
|
|
427
|
+
color: var(--xh-menu-item-text-color);
|
|
428
|
+
}
|
|
427
429
|
}
|
|
428
|
-
}
|
|
430
|
+
}
|
|
@@ -103,7 +103,7 @@ export class BaseFieldModel extends HoistModel {
|
|
|
103
103
|
super();
|
|
104
104
|
makeObservable(this);
|
|
105
105
|
this.name = name;
|
|
106
|
-
this.displayName =
|
|
106
|
+
this.displayName = displayName ?? genDisplayName(name);
|
|
107
107
|
this._origInitialValue = initialValue;
|
|
108
108
|
this.value = this.initialValue = executeIfFunction(initialValue);
|
|
109
109
|
this._disabled = disabled;
|
package/cmp/grid/Grid.js
CHANGED
|
@@ -530,12 +530,15 @@ class GridLocalModel extends HoistModel {
|
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
sizingModeReaction() {
|
|
533
|
-
const {model} = this
|
|
533
|
+
const {model} = this,
|
|
534
|
+
{mode} = model.autosizeOptions;
|
|
535
|
+
|
|
534
536
|
return {
|
|
535
537
|
track: () => model.sizingMode,
|
|
536
538
|
run: () => {
|
|
537
|
-
if (
|
|
538
|
-
|
|
539
|
+
if (mode === GridAutosizeMode.MANAGED || mode === GridAutosizeMode.ON_SIZING_MODE_CHANGE) {
|
|
540
|
+
model.autosizeAsync({showMask: true});
|
|
541
|
+
}
|
|
539
542
|
}
|
|
540
543
|
};
|
|
541
544
|
}
|
|
@@ -657,6 +660,20 @@ class GridLocalModel extends HoistModel {
|
|
|
657
660
|
wait().then(() => this.syncSelection());
|
|
658
661
|
}
|
|
659
662
|
|
|
663
|
+
if (model.autosizeOptions.mode === GridAutosizeMode.MANAGED) {
|
|
664
|
+
// If sizingMode different to autosizeState, autosize all columns...
|
|
665
|
+
if (model.autosizeState.sizingMode !== model.sizingMode) {
|
|
666
|
+
model.autosizeAsync();
|
|
667
|
+
} else {
|
|
668
|
+
// ...otherwise, only autosize columns that are not manually sized
|
|
669
|
+
const columns = model.getLeafColumnIds().filter(colId => {
|
|
670
|
+
const state = model.findColumn(model.columnState, colId);
|
|
671
|
+
return state && !state.manuallySized;
|
|
672
|
+
});
|
|
673
|
+
model.autosizeAsync({columns});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
660
677
|
model.noteAgExpandStateChange();
|
|
661
678
|
|
|
662
679
|
this._prevRs = newRs;
|
|
@@ -700,7 +717,10 @@ class GridLocalModel extends HoistModel {
|
|
|
700
717
|
|
|
701
718
|
// Catches column resizing on call to autoSize API.
|
|
702
719
|
onColumnResized = (ev) => {
|
|
703
|
-
if (isDisplayed(this.viewRef.current)
|
|
720
|
+
if (!isDisplayed(this.viewRef.current) || !ev.finished) return;
|
|
721
|
+
if (ev.source === 'uiColumnDragged') {
|
|
722
|
+
this.model.noteColumnsManuallySized(ev.column.colId);
|
|
723
|
+
} else if (ev.source === 'autosizeColumns') {
|
|
704
724
|
this.model.noteAgColumnStateChanged(ev.columnApi.getColumnState());
|
|
705
725
|
}
|
|
706
726
|
ev.api.resetRowHeights();
|
|
@@ -754,10 +774,21 @@ class GridLocalModel extends HoistModel {
|
|
|
754
774
|
this.model.agGridModel.agApi.setFilterModel(filterState);
|
|
755
775
|
}
|
|
756
776
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
777
|
+
processCellForClipboard = ({value, node, column}) => {
|
|
778
|
+
const {model} = this,
|
|
779
|
+
recId = node.id,
|
|
780
|
+
colId = column.colId,
|
|
781
|
+
record = isNil(recId) ? null : model.store.getById(recId, true),
|
|
782
|
+
xhColumn = isNil(colId) ? null : model.getColumn(colId);
|
|
783
|
+
|
|
784
|
+
if (!record || !xhColumn) return value;
|
|
785
|
+
|
|
786
|
+
return XH.gridExportService.getExportableValueForCell({
|
|
787
|
+
gridModel: model,
|
|
788
|
+
record,
|
|
789
|
+
column: xhColumn,
|
|
790
|
+
node
|
|
791
|
+
});
|
|
761
792
|
}
|
|
762
793
|
|
|
763
794
|
navigateToNextCell = (agParams) => {
|
|
@@ -21,16 +21,16 @@ export const GridAutosizeMode = Object.freeze({
|
|
|
21
21
|
ON_DEMAND: 'onDemand',
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
* the
|
|
24
|
+
* Grid will autosize columns when the GridModel's sizingMode changes.
|
|
25
|
+
* Also offers the affordances provided by ON_DEMAND.
|
|
26
26
|
*/
|
|
27
|
-
ON_SIZING_MODE_CHANGE: 'onSizingModeChange'
|
|
27
|
+
ON_SIZING_MODE_CHANGE: 'onSizingModeChange',
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Grid will autosize columns when the GridModel's sizingMode changes or data is loaded,
|
|
31
|
+
* unless the user has manually modified their column widths.
|
|
32
|
+
* Also offers the affordances provided by ON_DEMAND.
|
|
33
|
+
*/
|
|
34
|
+
MANAGED: 'managed'
|
|
35
35
|
|
|
36
36
|
});
|
package/cmp/grid/GridModel.js
CHANGED
|
@@ -71,6 +71,8 @@ export class GridModel extends HoistModel {
|
|
|
71
71
|
'OK to proceed?'
|
|
72
72
|
);
|
|
73
73
|
|
|
74
|
+
static DEFAULT_AUTOSIZE_MODE = GridAutosizeMode.ON_SIZING_MODE_CHANGE;
|
|
75
|
+
|
|
74
76
|
//------------------------
|
|
75
77
|
// Immutable public properties
|
|
76
78
|
//------------------------
|
|
@@ -129,6 +131,8 @@ export class GridModel extends HoistModel {
|
|
|
129
131
|
@observable.ref columnState = [];
|
|
130
132
|
/** @member {Object} */
|
|
131
133
|
@observable.ref expandState = {};
|
|
134
|
+
/** @member {AutosizeState} */
|
|
135
|
+
@observable.ref autosizeState = {};
|
|
132
136
|
/** @member {GridSorter[]} */
|
|
133
137
|
@observable.ref sortBy = [];
|
|
134
138
|
/** @member {string[]} */
|
|
@@ -367,7 +371,7 @@ export class GridModel extends HoistModel {
|
|
|
367
371
|
this.autosizeOptions = defaults(
|
|
368
372
|
{...autosizeOptions},
|
|
369
373
|
{
|
|
370
|
-
mode:
|
|
374
|
+
mode: GridModel.DEFAULT_AUTOSIZE_MODE,
|
|
371
375
|
includeCollapsedChildren: false,
|
|
372
376
|
showMask: false,
|
|
373
377
|
// Larger buffer on mobile (perhaps counterintuitively) to minimize clipping due to
|
|
@@ -768,7 +772,10 @@ export class GridModel extends HoistModel {
|
|
|
768
772
|
|
|
769
773
|
this.columns = columns;
|
|
770
774
|
this.columnState = this.getLeafColumns()
|
|
771
|
-
.map(
|
|
775
|
+
.map(it => {
|
|
776
|
+
const {colId, width, hidden, pinned} = it;
|
|
777
|
+
return {colId, width, hidden, pinned};
|
|
778
|
+
});
|
|
772
779
|
}
|
|
773
780
|
|
|
774
781
|
/** @param {ColumnState[]} colState */
|
|
@@ -824,6 +831,24 @@ export class GridModel extends HoistModel {
|
|
|
824
831
|
}
|
|
825
832
|
}
|
|
826
833
|
|
|
834
|
+
@action
|
|
835
|
+
setAutosizeState(autosizeState) {
|
|
836
|
+
if (!equal(this.autosizeState, autosizeState)) {
|
|
837
|
+
this.autosizeState = deepFreeze(autosizeState);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
noteColumnsManuallySized(colIds) {
|
|
842
|
+
const colStateChanges = castArray(colIds).map(colId => ({colId, manuallySized: true}));
|
|
843
|
+
this.applyColumnStateChanges(colStateChanges);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
noteColumnsAutosized(colIds) {
|
|
847
|
+
const colStateChanges = castArray(colIds).map(colId => ({colId, manuallySized: false}));
|
|
848
|
+
this.applyColumnStateChanges(colStateChanges);
|
|
849
|
+
this.setAutosizeState({sizingMode: this.sizingMode});
|
|
850
|
+
}
|
|
851
|
+
|
|
827
852
|
/**
|
|
828
853
|
* This method will update the current column definition if it has changed.
|
|
829
854
|
* Throws an exception if any of the columns provided in colStateChanges are not
|
|
@@ -852,6 +877,7 @@ export class GridModel extends HoistModel {
|
|
|
852
877
|
if (!isNil(change.width)) col.width = change.width;
|
|
853
878
|
if (!isNil(change.hidden)) col.hidden = change.hidden;
|
|
854
879
|
if (!isUndefined(change.pinned)) col.pinned = change.pinned;
|
|
880
|
+
if (!isNil(change.manuallySized)) col.manuallySized = change.manuallySized;
|
|
855
881
|
});
|
|
856
882
|
|
|
857
883
|
// 2) If the changes provided is a full list of leaf columns, synchronize the sort order
|
|
@@ -1038,7 +1064,6 @@ export class GridModel extends HoistModel {
|
|
|
1038
1064
|
.linkTo(this.autosizeTask);
|
|
1039
1065
|
}
|
|
1040
1066
|
|
|
1041
|
-
|
|
1042
1067
|
/**
|
|
1043
1068
|
* Begin an inline editing session.
|
|
1044
1069
|
* @param {RecordOrId} [recOrId] - Record/ID to edit. If unspecified, the first selected Record
|
|
@@ -1178,6 +1203,7 @@ export class GridModel extends HoistModel {
|
|
|
1178
1203
|
|
|
1179
1204
|
try {
|
|
1180
1205
|
await XH.gridAutosizeService.autosizeAsync(this, colIds, options);
|
|
1206
|
+
this.noteColumnsAutosized(colIds);
|
|
1181
1207
|
} finally {
|
|
1182
1208
|
if (showMask) {
|
|
1183
1209
|
await wait();
|
|
@@ -1307,10 +1333,10 @@ export class GridModel extends HoistModel {
|
|
|
1307
1333
|
// conflict with any code-level updates to their widths.
|
|
1308
1334
|
removeTransientWidths(columnState) {
|
|
1309
1335
|
const gridCols = this.getLeafColumns();
|
|
1310
|
-
|
|
1311
1336
|
return columnState.map(state => {
|
|
1312
1337
|
const col = this.findColumn(gridCols, state.colId);
|
|
1313
|
-
|
|
1338
|
+
if (!col.resizable) state = omit(state, 'width');
|
|
1339
|
+
return state;
|
|
1314
1340
|
});
|
|
1315
1341
|
}
|
|
1316
1342
|
|
|
@@ -1506,6 +1532,12 @@ export class GridModel extends HoistModel {
|
|
|
1506
1532
|
* @property {number} [width] - new width to set for the column
|
|
1507
1533
|
* @property {boolean} [hidden] - visibility of the column
|
|
1508
1534
|
* @property {string} [pinned] - 'left'|'right' if pinned, null if not
|
|
1535
|
+
* @property {boolean} [manuallySized] - has this column been resized manually?
|
|
1536
|
+
*/
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* @typedef {Object} AutosizeState
|
|
1540
|
+
* @property {SizingMode} sizingMode - sizing mode used last time the columns were autosized.
|
|
1509
1541
|
*/
|
|
1510
1542
|
|
|
1511
1543
|
/**
|
|
@@ -89,27 +89,28 @@ export class GridPersistenceModel extends HoistModel {
|
|
|
89
89
|
columnReaction() {
|
|
90
90
|
const {gridModel} = this;
|
|
91
91
|
return {
|
|
92
|
-
track: () => gridModel.columnState,
|
|
93
|
-
run: (columnState) => {
|
|
94
|
-
this.patchState({
|
|
92
|
+
track: () => [gridModel.columnState, gridModel.autosizeState],
|
|
93
|
+
run: ([columnState, autosizeState]) => {
|
|
94
|
+
this.patchState({
|
|
95
|
+
columns: gridModel.removeTransientWidths(columnState),
|
|
96
|
+
autosize: autosizeState
|
|
97
|
+
});
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
updateGridColumns() {
|
|
100
|
-
const {
|
|
101
|
-
if (!
|
|
102
|
-
|
|
103
|
-
gridModel.setColumnState(state.columns);
|
|
103
|
+
const {columns, autosize} = this.state;
|
|
104
|
+
if (!isUndefined(columns)) this.gridModel.setColumnState(columns);
|
|
105
|
+
if (!isUndefined(autosize)) this.gridModel.setAutosizeState(autosize);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
//--------------------------
|
|
107
109
|
// Sort
|
|
108
110
|
//--------------------------
|
|
109
111
|
sortReaction() {
|
|
110
|
-
const {gridModel} = this;
|
|
111
112
|
return {
|
|
112
|
-
track: () => gridModel.sortBy,
|
|
113
|
+
track: () => this.gridModel.sortBy,
|
|
113
114
|
run: (sortBy) => {
|
|
114
115
|
this.patchState({sortBy: sortBy.map(it => it.toString())});
|
|
115
116
|
}
|
package/cmp/grid/index.js
CHANGED
|
@@ -9,10 +9,10 @@ export * from './columns';
|
|
|
9
9
|
|
|
10
10
|
export * from './enums/TreeStyle';
|
|
11
11
|
|
|
12
|
+
export * from './GridAutosizeMode';
|
|
12
13
|
export * from './helpers/GridCountLabel';
|
|
13
14
|
|
|
14
15
|
export * from './renderers/MultiFieldRenderer';
|
|
15
16
|
|
|
16
|
-
export * from './GridAutosizeMode';
|
|
17
17
|
export * from './Grid';
|
|
18
18
|
export * from './GridModel';
|
package/core/XH.js
CHANGED
|
@@ -828,12 +828,12 @@ class XHClass extends HoistBase {
|
|
|
828
828
|
return await this.fetchService
|
|
829
829
|
.fetchJson({
|
|
830
830
|
url: 'xh/authStatus',
|
|
831
|
-
timeout: 3 * MINUTES //
|
|
831
|
+
timeout: 3 * MINUTES // Accommodate delay for user at a credentials prompt
|
|
832
832
|
})
|
|
833
833
|
.then(r => r.authenticated)
|
|
834
834
|
.catch(e => {
|
|
835
835
|
// 401s normal / expected for non-SSO apps when user not yet logged in.
|
|
836
|
-
if (e.httpStatus
|
|
836
|
+
if (e.httpStatus === 401) return false;
|
|
837
837
|
// Other exceptions indicate e.g. connectivity issue, server down - raise to user.
|
|
838
838
|
throw e;
|
|
839
839
|
});
|
|
@@ -150,7 +150,14 @@ const bannerList = hoistCmp.factory({
|
|
|
150
150
|
|
|
151
151
|
function globalHotKeys(model) {
|
|
152
152
|
const {impersonationBarModel, optionsDialogModel} = model,
|
|
153
|
-
ret = [
|
|
153
|
+
ret = [
|
|
154
|
+
{
|
|
155
|
+
global: true,
|
|
156
|
+
combo: 'shift + r',
|
|
157
|
+
label: 'Refresh application data',
|
|
158
|
+
onKeyDown: () => XH.refreshAppAsync()
|
|
159
|
+
}
|
|
160
|
+
];
|
|
154
161
|
|
|
155
162
|
if (XH.identityService.canAuthUserImpersonate) {
|
|
156
163
|
ret.push({
|
|
@@ -155,14 +155,13 @@ export class StoreContextMenu {
|
|
|
155
155
|
recordsRequired: true,
|
|
156
156
|
actionFn: ({record, column}) => {
|
|
157
157
|
if (record && column) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
const node = gridModel.agApi?.getRowNode(record.id),
|
|
159
|
+
value = XH.gridExportService.getExportableValueForCell({
|
|
160
|
+
gridModel,
|
|
161
|
+
record,
|
|
162
|
+
column,
|
|
163
|
+
node
|
|
164
|
+
});
|
|
166
165
|
copy(value);
|
|
167
166
|
}
|
|
168
167
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2021 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import ReactDOM from 'react-dom';
|
|
7
8
|
import {HoistModel, managed, RefreshMode, RenderMode, XH, PersistenceProvider, TaskObserver} from '@xh/hoist/core';
|
|
8
9
|
import {convertIconToHtml, deserializeIcon} from '@xh/hoist/icon';
|
|
9
10
|
import {ContextMenu} from '@xh/hoist/kit/blueprint';
|
|
@@ -15,6 +16,7 @@ import {createObservableRef} from '@xh/hoist/utils/react';
|
|
|
15
16
|
import {cloneDeep, defaultsDeep, find, isFinite, reject} from 'lodash';
|
|
16
17
|
import {DashViewModel} from './DashViewModel';
|
|
17
18
|
import {DashViewSpec} from './DashViewSpec';
|
|
19
|
+
import {dashContainerMenuButton} from './impl/DashContainerMenuButton';
|
|
18
20
|
import {dashContainerContextMenu} from './impl/DashContainerContextMenu';
|
|
19
21
|
import {convertGLToState, convertStateToGL, getViewModelId} from './impl/DashContainerUtils';
|
|
20
22
|
import {dashView} from './impl/DashView';
|
|
@@ -96,6 +98,8 @@ export class DashContainerModel extends HoistModel {
|
|
|
96
98
|
@bindable contentLocked;
|
|
97
99
|
/** @member {boolean} */
|
|
98
100
|
@bindable renameLocked;
|
|
101
|
+
/** @member {boolean} */
|
|
102
|
+
@bindable showMenuButton;
|
|
99
103
|
|
|
100
104
|
//------------------------
|
|
101
105
|
// Immutable public properties
|
|
@@ -131,6 +135,8 @@ export class DashContainerModel extends HoistModel {
|
|
|
131
135
|
* @param {boolean} [c.layoutLocked] - prevent re-arranging views by dragging and dropping.
|
|
132
136
|
* @param {boolean} [c.contentLocked] - prevent adding and removing views.
|
|
133
137
|
* @param {boolean} [c.renameLocked] - prevent renaming views.
|
|
138
|
+
* @param {boolean} [c.showMenuButton] - true to include a button in each stack header showing
|
|
139
|
+
* the dash context menu.
|
|
134
140
|
* @param {Object} [c.goldenLayoutSettings] - custom settings to be passed to the GoldenLayout instance.
|
|
135
141
|
* @see http://golden-layout.com/docs/Config.html
|
|
136
142
|
* @param {PersistOptions} [c.persistWith] - options governing persistence
|
|
@@ -149,6 +155,7 @@ export class DashContainerModel extends HoistModel {
|
|
|
149
155
|
layoutLocked = false,
|
|
150
156
|
contentLocked = false,
|
|
151
157
|
renameLocked = false,
|
|
158
|
+
showMenuButton = false,
|
|
152
159
|
goldenLayoutSettings,
|
|
153
160
|
persistWith = null,
|
|
154
161
|
emptyText = 'No views have been added to the container.',
|
|
@@ -169,6 +176,7 @@ export class DashContainerModel extends HoistModel {
|
|
|
169
176
|
this.layoutLocked = layoutLocked;
|
|
170
177
|
this.contentLocked = contentLocked;
|
|
171
178
|
this.renameLocked = renameLocked;
|
|
179
|
+
this.showMenuButton = showMenuButton;
|
|
172
180
|
this.goldenLayoutSettings = goldenLayoutSettings;
|
|
173
181
|
this.emptyText = emptyText;
|
|
174
182
|
this.addViewButtonText = addViewButtonText;
|
|
@@ -380,6 +388,10 @@ export class DashContainerModel extends HoistModel {
|
|
|
380
388
|
// Listen to active item change to support RenderMode
|
|
381
389
|
stack.on('activeContentItemChanged', () => this.onStackActiveItemChange(stack));
|
|
382
390
|
|
|
391
|
+
// Add menu button to stack header
|
|
392
|
+
const containerEl = stack.header.controlsContainer[0];
|
|
393
|
+
ReactDOM.render(dashContainerMenuButton({dashContainerModel: this, stack}), containerEl);
|
|
394
|
+
|
|
383
395
|
// Add context menu listener for adding components
|
|
384
396
|
const $el = stack.header.element;
|
|
385
397
|
$el.off('contextmenu').contextmenu(e => {
|
|
@@ -19,7 +19,7 @@ import {isEmpty} from 'lodash';
|
|
|
19
19
|
* @private
|
|
20
20
|
*/
|
|
21
21
|
export const dashContainerContextMenu = hoistCmp.factory({
|
|
22
|
-
model: null,
|
|
22
|
+
model: null, observer: null,
|
|
23
23
|
render(props) {
|
|
24
24
|
const menuItems = createMenuItems(props);
|
|
25
25
|
return contextMenu({menuItems});
|
|
@@ -0,0 +1,34 @@
|
|
|
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 © 2021 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {hoistCmp} from '@xh/hoist/core';
|
|
8
|
+
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
9
|
+
import {Icon} from '@xh/hoist/icon';
|
|
10
|
+
import {popover, Position} from '@xh/hoist/kit/blueprint';
|
|
11
|
+
import {dashContainerContextMenu} from './DashContainerContextMenu';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Button and popover for displaying the context menu. Apps can control whether this button is
|
|
15
|
+
* displayed via DashContainerModel's `showMenuButton` config.
|
|
16
|
+
*
|
|
17
|
+
* @see DashContainerModel
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
export const dashContainerMenuButton = hoistCmp.factory({
|
|
21
|
+
model: null,
|
|
22
|
+
render({stack, dashContainerModel}) {
|
|
23
|
+
if (dashContainerModel.contentLocked || !dashContainerModel.showMenuButton) return null;
|
|
24
|
+
|
|
25
|
+
return popover({
|
|
26
|
+
position: Position.BOTTOM,
|
|
27
|
+
target: button({
|
|
28
|
+
icon: Icon.ellipsisVertical(),
|
|
29
|
+
className: 'xh-dash-container-menu-btn'
|
|
30
|
+
}),
|
|
31
|
+
content: dashContainerContextMenu({stack, dashContainerModel})
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -23,6 +23,11 @@ export const [SelectEditor, selectEditor] = hoistCmp.withFactory({
|
|
|
23
23
|
hideDropdownIndicator: true,
|
|
24
24
|
hideSelectedOptionCheck: true,
|
|
25
25
|
selectOnFocus: false,
|
|
26
|
+
onCommit: () => {
|
|
27
|
+
// When not full-row editing we end editing after commit to avoid extra clicks
|
|
28
|
+
const {gridModel} = props;
|
|
29
|
+
if (!gridModel.fullRowEditing) gridModel.endEditAsync();
|
|
30
|
+
},
|
|
26
31
|
rsOptions: {
|
|
27
32
|
styles: {
|
|
28
33
|
menu: styles => ({
|
|
@@ -59,6 +59,8 @@ export class DraggerModel extends HoistModel {
|
|
|
59
59
|
'Resizable panel has no sibling panel against which to resize.'
|
|
60
60
|
);
|
|
61
61
|
|
|
62
|
+
if (XH.isDesktop) this.setIframePointerEvents('none');
|
|
63
|
+
|
|
62
64
|
e.stopPropagation();
|
|
63
65
|
|
|
64
66
|
const {clientX, clientY} = this.parseEventPositions(e);
|
|
@@ -113,6 +115,8 @@ export class DraggerModel extends HoistModel {
|
|
|
113
115
|
};
|
|
114
116
|
|
|
115
117
|
onDragEnd = () => {
|
|
118
|
+
if (XH.isDesktop) this.setIframePointerEvents('auto');
|
|
119
|
+
|
|
116
120
|
const {panelModel} = this;
|
|
117
121
|
if (!panelModel.isResizing) return;
|
|
118
122
|
|
|
@@ -216,4 +220,14 @@ export class DraggerModel extends HoistModel {
|
|
|
216
220
|
isValidTouchEvent(e) {
|
|
217
221
|
return e.touches && e.touches.length > 0;
|
|
218
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {('none'|'auto')} v - Workaround to allow dragging over iframe, which is its own
|
|
226
|
+
* separate document and cannot listen for events from main document.
|
|
227
|
+
*/
|
|
228
|
+
setIframePointerEvents(v) {
|
|
229
|
+
for (const el of document.getElementsByTagName('iframe')) {
|
|
230
|
+
el.style['pointer-events'] = v;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
219
233
|
}
|
|
@@ -66,9 +66,8 @@ export const [AppBar, appBar] = hoistCmp.withFactory({
|
|
|
66
66
|
icon: icon,
|
|
67
67
|
omit: !icon,
|
|
68
68
|
onClick: () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
69
|
+
// Navigate to root-level route
|
|
70
|
+
XH.navigate(XH.appModel.getRoutes()[0].name);
|
|
72
71
|
}
|
|
73
72
|
}),
|
|
74
73
|
div({
|
package/package.json
CHANGED
package/svc/FetchService.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2021 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
-
import {HoistService, XH} from '@xh/hoist/core';
|
|
7
|
+
import {HoistService, XH, AppState} from '@xh/hoist/core';
|
|
8
8
|
import {Exception} from '@xh/hoist/exception';
|
|
9
|
-
import {isLocalDate} from '@xh/hoist/utils/datetime';
|
|
9
|
+
import {isLocalDate, SECONDS, ONE_MINUTE, olderThan} from '@xh/hoist/utils/datetime';
|
|
10
10
|
import {throwIf} from '@xh/hoist/utils/js';
|
|
11
11
|
import {StatusCodes} from 'http-status-codes';
|
|
12
12
|
import {isDate, isFunction, isNil, omitBy} from 'lodash';
|
|
13
13
|
import {stringify} from 'qs';
|
|
14
|
-
import {
|
|
14
|
+
import {never} from '@xh/hoist/promise';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Service for making managed HTTP requests, both to the app's own Hoist server and to remote APIs.
|
|
@@ -27,6 +27,11 @@ import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
|
27
27
|
*
|
|
28
28
|
* Note that the convenience methods `fetchJson`, `postJson`, `putJson` all accept the same options
|
|
29
29
|
* as the main entry point `fetch`, as they delegate to fetch after setting additional defaults.
|
|
30
|
+
*
|
|
31
|
+
* Note: For non-SSO apps, FetchService will automatically trigger a reload of the app if a
|
|
32
|
+
* 401 is encountered from a local (relative) request. This default behavior is designed to allow
|
|
33
|
+
* more seamless re-establishment of timed out authentication sessions, but can be turned off
|
|
34
|
+
* via config if needed.
|
|
30
35
|
*/
|
|
31
36
|
export class FetchService extends HoistService {
|
|
32
37
|
|
|
@@ -164,7 +169,7 @@ export class FetchService extends HoistService {
|
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
if (e.isHoistException) throw e;
|
|
167
|
-
|
|
172
|
+
|
|
168
173
|
// Just two other cases where we expect this to throw -- Typically we get a failed response)
|
|
169
174
|
throw (e.name === 'AbortError') ? Exception.fetchAborted(opts, e) : Exception.serverUnavailable(opts, e);
|
|
170
175
|
} finally {
|
|
@@ -185,8 +190,8 @@ export class FetchService extends HoistService {
|
|
|
185
190
|
if (!method) {
|
|
186
191
|
method = (params ? 'POST' : 'GET');
|
|
187
192
|
}
|
|
188
|
-
|
|
189
|
-
if (
|
|
193
|
+
const isRelativeUrl = !url.startsWith('/') && !url.includes('//');
|
|
194
|
+
if (isRelativeUrl) {
|
|
190
195
|
url = XH.baseUrl + url;
|
|
191
196
|
}
|
|
192
197
|
|
|
@@ -232,12 +237,31 @@ export class FetchService extends HoistService {
|
|
|
232
237
|
|
|
233
238
|
if (!ret.ok) {
|
|
234
239
|
ret.responseText = await this.safeResponseTextAsync(ret);
|
|
235
|
-
|
|
240
|
+
const e = Exception.fetchError(opts, ret);
|
|
241
|
+
if (!XH.isSSO && isRelativeUrl && e.httpStatus === 401) {
|
|
242
|
+
await this.maybeReloadForAuthAsync();
|
|
243
|
+
}
|
|
244
|
+
throw e;
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
return ret;
|
|
239
248
|
}
|
|
240
249
|
|
|
250
|
+
async maybeReloadForAuthAsync() {
|
|
251
|
+
const {appState, configService, localStorageService} = XH;
|
|
252
|
+
|
|
253
|
+
// Don't interfere with initialization, avoid tight loops, and provide kill switch
|
|
254
|
+
if (
|
|
255
|
+
appState === AppState.RUNNING &&
|
|
256
|
+
configService.get('xhReloadOnFailedAuth', true) &&
|
|
257
|
+
olderThan(localStorageService.get('xhLastFailedAuthReload', null), ONE_MINUTE)
|
|
258
|
+
) {
|
|
259
|
+
localStorageService.set('xhLastFailedAuthReload', Date.now());
|
|
260
|
+
XH.reloadApp();
|
|
261
|
+
await never();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
241
265
|
async sendJsonInternalAsync(opts) {
|
|
242
266
|
return this.fetchJson({
|
|
243
267
|
...opts,
|
package/svc/GridExportService.js
CHANGED
|
@@ -12,7 +12,18 @@ import {SECONDS} from '@xh/hoist/utils/datetime';
|
|
|
12
12
|
import {throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
13
13
|
import download from 'downloadjs';
|
|
14
14
|
import {StatusCodes} from 'http-status-codes';
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
castArray,
|
|
17
|
+
isArray,
|
|
18
|
+
isEmpty,
|
|
19
|
+
isFunction,
|
|
20
|
+
isNil,
|
|
21
|
+
isString,
|
|
22
|
+
sortBy,
|
|
23
|
+
uniq,
|
|
24
|
+
compact,
|
|
25
|
+
findIndex
|
|
26
|
+
} from 'lodash';
|
|
16
27
|
import {span, a} from '@xh/hoist/cmp/layout';
|
|
17
28
|
import {wait} from '@xh/hoist/promise';
|
|
18
29
|
|
|
@@ -131,6 +142,61 @@ export class GridExportService extends HoistService {
|
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Get the exportable value for a given cell.
|
|
147
|
+
*
|
|
148
|
+
* This method is used internally by this service, but also made available
|
|
149
|
+
* publicly for use by grid clipboard functionality.
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} c
|
|
152
|
+
* @param {GridModel} c.gridModel
|
|
153
|
+
* @param {Record} c.record
|
|
154
|
+
* @param {Column} c.column
|
|
155
|
+
* @param {Object} [c.node] - rendered ag-Grid row, if available. Necessary for
|
|
156
|
+
* exporting agGrid aggregates.
|
|
157
|
+
* @param {boolean} [c.forServer] - for posting to server, default false.
|
|
158
|
+
* @return {String} - value suitable for export to excel, csv, or clipboard.
|
|
159
|
+
*/
|
|
160
|
+
getExportableValueForCell({gridModel, record, column, node, forServer = false}) {
|
|
161
|
+
const {field, exportValue, getValueFn} = column,
|
|
162
|
+
aggData = node && gridModel.treeMode && !isEmpty(record.children) ? node.aggData : null;
|
|
163
|
+
|
|
164
|
+
// 0) Main processing
|
|
165
|
+
let value = getValueFn({record, field, column, gridModel});
|
|
166
|
+
// Modify value using exportValue
|
|
167
|
+
if (isString(exportValue) && record.data[exportValue] !== null) {
|
|
168
|
+
// If exportValue points to a different field
|
|
169
|
+
value = record.data[exportValue];
|
|
170
|
+
} else if (isFunction(exportValue)) {
|
|
171
|
+
// If export value is a function that transforms the value
|
|
172
|
+
value = exportValue(value, {record, column, gridModel});
|
|
173
|
+
} else if (aggData && !isNil(aggData[field])) {
|
|
174
|
+
// If we found aggregate data calculated by agGrid
|
|
175
|
+
value = aggData[field];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (isNil(value)) return null;
|
|
179
|
+
|
|
180
|
+
// 1) Support per-cell exportFormat
|
|
181
|
+
let {exportFormat} = column,
|
|
182
|
+
cellHasExportFormat = isFunction(exportFormat);
|
|
183
|
+
|
|
184
|
+
if (cellHasExportFormat) {
|
|
185
|
+
exportFormat = exportFormat(value, {record, column, gridModel});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 2) Dates: Provide date data expected by server endpoint.
|
|
189
|
+
// Also, approximate formats for CSV and clipboard.
|
|
190
|
+
if (exportFormat === ExportFormat.DATE_FMT) value = fmtDate(value);
|
|
191
|
+
if (exportFormat === ExportFormat.DATETIME_FMT) value = fmtDate(value, 'YYYY-MM-DD HH:mm:ss');
|
|
192
|
+
|
|
193
|
+
value = value.toString();
|
|
194
|
+
|
|
195
|
+
return forServer && cellHasExportFormat ?
|
|
196
|
+
{value, format: exportFormat} :
|
|
197
|
+
value;
|
|
198
|
+
}
|
|
199
|
+
|
|
134
200
|
//-----------------------
|
|
135
201
|
// Implementation
|
|
136
202
|
//-----------------------
|
|
@@ -255,48 +321,13 @@ export class GridExportService extends HoistService {
|
|
|
255
321
|
}
|
|
256
322
|
|
|
257
323
|
getRecordRow(gridModel, record, columns, depth) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const data = columns.map(it => this.getCellData(gridModel, record, it, aggData));
|
|
324
|
+
const node = gridModel.agApi?.getRowNode(record.id),
|
|
325
|
+
data = columns.map(column => {
|
|
326
|
+
return this.getExportableValueForCell({gridModel, record, column, node, forServer: true});
|
|
327
|
+
});
|
|
263
328
|
return {data, depth};
|
|
264
329
|
}
|
|
265
330
|
|
|
266
|
-
getCellData(gridModel, record, column, aggData) {
|
|
267
|
-
const {field, exportValue, getValueFn} = column;
|
|
268
|
-
|
|
269
|
-
let value = getValueFn({record, field, column, gridModel});
|
|
270
|
-
// Modify value using exportValue
|
|
271
|
-
if (isString(exportValue) && record.data[exportValue] !== null) {
|
|
272
|
-
// If exportValue points to a different field
|
|
273
|
-
value = record.data[exportValue];
|
|
274
|
-
} else if (isFunction(exportValue)) {
|
|
275
|
-
// If export value is a function that transforms the value
|
|
276
|
-
value = exportValue(value, {record, column, gridModel});
|
|
277
|
-
} else if (aggData && !isNil(aggData[field])) {
|
|
278
|
-
// If we found aggregate data calculated by agGrid
|
|
279
|
-
value = aggData[field];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (isNil(value)) return null;
|
|
283
|
-
|
|
284
|
-
// Get cell-level format if function form provided
|
|
285
|
-
let {exportFormat} = column,
|
|
286
|
-
cellHasExportFormat = isFunction(exportFormat);
|
|
287
|
-
|
|
288
|
-
if (cellHasExportFormat) {
|
|
289
|
-
exportFormat = exportFormat(value, {record, column, gridModel});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Enforce date formats expected by server
|
|
293
|
-
if (exportFormat === ExportFormat.DATE_FMT) value = fmtDate(value);
|
|
294
|
-
if (exportFormat === ExportFormat.DATETIME_FMT) value = fmtDate(value, 'YYYY-MM-DD HH:mm:ss');
|
|
295
|
-
|
|
296
|
-
value = value.toString();
|
|
297
|
-
return cellHasExportFormat ? {value, format: exportFormat} : value;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
331
|
getContentType(type) {
|
|
301
332
|
switch (type) {
|
|
302
333
|
case 'excelTable':
|
package/utils/js/Decorators.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import {debounce, isFunction} from 'lodash';
|
|
8
8
|
import {throwIf, getOrCreate} from './LangUtils';
|
|
9
|
-
import {withDebug} from './
|
|
9
|
+
import {withDebug} from './LogUtils';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Decorates a class method so that it is debounced by the specified duration.
|
|
@@ -30,7 +30,7 @@ export function debounced(duration) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
* Modify a method or getter so that it will compute once lazily
|
|
33
|
+
* Modify a method or getter so that it will compute once lazily and then cache the results.
|
|
34
34
|
* Not appropriate for methods that take arguments. Typically useful on immutable objects.
|
|
35
35
|
*/
|
|
36
36
|
export function computeOnce(target, key, descriptor) {
|
|
@@ -51,8 +51,8 @@ export function computeOnce(target, key, descriptor) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* Modify a method so that
|
|
55
|
-
* @see
|
|
54
|
+
* Modify a method so that its execution is tracked and timed with a debug message.
|
|
55
|
+
* @see withDebug
|
|
56
56
|
*/
|
|
57
57
|
export function logWithDebug(target, key, descriptor) {
|
|
58
58
|
const {value} = descriptor;
|
|
@@ -0,0 +1,119 @@
|
|
|
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 © 2021 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
import {castArray, isString} from 'lodash';
|
|
8
|
+
import {apiDeprecated} from './LangUtils';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Track a function execution with console.log.
|
|
12
|
+
*
|
|
13
|
+
* This method will log the provided message(s) with timing information in a single message *after*
|
|
14
|
+
* the tracked function returns.
|
|
15
|
+
*
|
|
16
|
+
* If the function passed to this util returns a Promise, it will wait until the Promise resolves
|
|
17
|
+
* or completes to finish its logging. The actual object returned by the tracked function will
|
|
18
|
+
* always be returned directly to the caller.
|
|
19
|
+
*
|
|
20
|
+
* @param {(string[]|string)} msgs
|
|
21
|
+
* @param {function} fn
|
|
22
|
+
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
23
|
+
*/
|
|
24
|
+
export function withInfo(msgs, fn, source) {
|
|
25
|
+
return loggedDo(msgs, fn, source, 'info');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Track a function execution with console.debug.
|
|
30
|
+
* @see withInfo
|
|
31
|
+
*
|
|
32
|
+
* @param {(string[]|string)} msgs
|
|
33
|
+
* @param {function} fn
|
|
34
|
+
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
35
|
+
*/
|
|
36
|
+
export function withDebug(msgs, fn, source) {
|
|
37
|
+
return loggedDo(msgs, fn, source, 'debug');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Log a message with console.log.
|
|
42
|
+
*
|
|
43
|
+
* @param {(string[]|string)} msgs
|
|
44
|
+
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
45
|
+
*/
|
|
46
|
+
export function logInfo(msgs, source) {
|
|
47
|
+
return loggedDo(msgs, null, source, 'info');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Log a message with console.debug.
|
|
52
|
+
*
|
|
53
|
+
* @param {(string[]|string)} msgs
|
|
54
|
+
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
55
|
+
*/
|
|
56
|
+
export function logDebug(msgs, source) {
|
|
57
|
+
return loggedDo(msgs, null, source, 'debug');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @deprecated */
|
|
61
|
+
export function withShortDebug(msgs, fn, source) {
|
|
62
|
+
apiDeprecated('withShortDebug', {msg: 'Use withDebug() instead', v: 'v45'});
|
|
63
|
+
return withDebug(msgs, fn, source);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
//----------------------------------
|
|
68
|
+
// Implementation
|
|
69
|
+
//----------------------------------
|
|
70
|
+
function loggedDo(msgs, fn, source, level) {
|
|
71
|
+
source = parseSource(source);
|
|
72
|
+
msgs = castArray(msgs);
|
|
73
|
+
const msg = msgs.join(' | ');
|
|
74
|
+
|
|
75
|
+
// Support simple message only.
|
|
76
|
+
if (!fn) {
|
|
77
|
+
writeLog(msg, source, level);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Otherwise, wrap the call to the provided fn.
|
|
82
|
+
let start, ret;
|
|
83
|
+
const logCompletion = () => {
|
|
84
|
+
const elapsed = Date.now() - start;
|
|
85
|
+
writeLog(`${msg} | ${elapsed}ms`, source, level);
|
|
86
|
+
},
|
|
87
|
+
logException = (e) => {
|
|
88
|
+
const elapsed = Date.now() - start;
|
|
89
|
+
writeLog(`${msg} | failed - ${e.message ?? e.name ?? 'Unknown error'} | ${elapsed}ms`, source, level);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
start = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
ret = fn();
|
|
95
|
+
} catch (e) {
|
|
96
|
+
logException(e);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (ret instanceof Promise) {
|
|
101
|
+
ret.then(logCompletion, logException);
|
|
102
|
+
} else {
|
|
103
|
+
logCompletion();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return ret;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseSource(source) {
|
|
110
|
+
if (isString(source)) return source;
|
|
111
|
+
if (source?.displayName) return source.displayName;
|
|
112
|
+
if (source?.constructor) return source.constructor.name;
|
|
113
|
+
return '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeLog(msg, source, level) {
|
|
117
|
+
if (source) msg = `[${source}] ${msg}`;
|
|
118
|
+
level === 'info' ? console.log(msg) : console.debug(msg);
|
|
119
|
+
}
|
package/utils/js/index.js
CHANGED
package/utils/js/WithDebug.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
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 © 2021 Extremely Heavy Industries Inc.
|
|
6
|
-
*/
|
|
7
|
-
import {castArray, isString} from 'lodash';
|
|
8
|
-
import {apiDeprecated} from './LangUtils';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Track a function execution, logging the provided message(s) on debug with timing information in
|
|
12
|
-
* a single message after the tracked function returns.
|
|
13
|
-
*
|
|
14
|
-
* If the function passed to this util returns a Promise, it will wait until the Promise resolves
|
|
15
|
-
* or completes to finish its logging. The actual object returned by the tracked function will
|
|
16
|
-
* always be returned directly to the caller.
|
|
17
|
-
*
|
|
18
|
-
* @param {(string[]|string)} msgs
|
|
19
|
-
* @param {function} fn
|
|
20
|
-
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
21
|
-
*/
|
|
22
|
-
export function withDebug(msgs, fn, source) {
|
|
23
|
-
return loggedDo(msgs, fn, source);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Track a function execution, logging the provided message(s) on debug with timing information in
|
|
29
|
-
* a single message after the tracked function returns.
|
|
30
|
-
*
|
|
31
|
-
* @deprecated use withDebug instead.
|
|
32
|
-
*/
|
|
33
|
-
export function withShortDebug(msgs, fn, source) {
|
|
34
|
-
apiDeprecated('withShortDebug', {msg: 'Use withDebug() instead', v: 'v44'});
|
|
35
|
-
return withDebug(msgs, fn, source);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Log a message for debugging with standardized formatting.
|
|
40
|
-
*
|
|
41
|
-
* @param {(string[]|string)} msgs
|
|
42
|
-
* @param {(Object|string)} [source] - class, function or string to label the source of the message
|
|
43
|
-
*/
|
|
44
|
-
export function logDebug(msgs, source) {
|
|
45
|
-
return loggedDo(msgs, null, source);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
//----------------------------------
|
|
49
|
-
// Implementation
|
|
50
|
-
//----------------------------------
|
|
51
|
-
function loggedDo(msgs, fn, source) {
|
|
52
|
-
|
|
53
|
-
source = parseSource(source);
|
|
54
|
-
msgs = castArray(msgs);
|
|
55
|
-
const msg = msgs.join(' | ');
|
|
56
|
-
|
|
57
|
-
// Support simple message only
|
|
58
|
-
if (!fn) {
|
|
59
|
-
writeLog(msg, source);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ..otherwise a wrapped call..
|
|
64
|
-
const start = Date.now();
|
|
65
|
-
let ret;
|
|
66
|
-
try {
|
|
67
|
-
ret = fn();
|
|
68
|
-
} catch (e) {
|
|
69
|
-
logException(start, msg, source, e);
|
|
70
|
-
throw e;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (ret instanceof Promise) {
|
|
74
|
-
ret.then(
|
|
75
|
-
() => logCompletion(start, msg, source),
|
|
76
|
-
(e) => logException(start, msg, source, e)
|
|
77
|
-
);
|
|
78
|
-
} else {
|
|
79
|
-
logCompletion(start, msg, source);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return ret;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function parseSource(source) {
|
|
86
|
-
if (isString(source)) return source;
|
|
87
|
-
if (source?.displayName) return source.displayName;
|
|
88
|
-
if (source?.constructor) return source.constructor.name;
|
|
89
|
-
return '';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function writeLog(msg, source) {
|
|
93
|
-
console.debug(source ? `[${source}] ${msg}` : msg);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function logCompletion(start, msg, source) {
|
|
97
|
-
const elapsed = Date.now() - start;
|
|
98
|
-
writeLog(`${msg} | ${elapsed}ms`, source);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function logException(start, msg, source, e) {
|
|
102
|
-
const elapsed = Date.now() - start;
|
|
103
|
-
writeLog(`${msg} | failed - ${e.message || e.name || 'Unknown error'} | ${elapsed}ms`, source);
|
|
104
|
-
}
|