@xh/hoist 59.1.0 → 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 +25 -0
- package/admin/tabs/general/alertBanner/AlertBannerModel.ts +2 -2
- package/cmp/ag-grid/AgGrid.scss +27 -5
- package/cmp/grid/Grid.scss +10 -0
- package/cmp/grid/GridModel.ts +137 -33
- package/cmp/grid/columns/ColumnGroup.ts +11 -0
- package/data/RecordAction.ts +1 -1
- package/data/cube/Query.ts +37 -3
- package/data/cube/View.ts +2 -2
- package/data/cube/row/BaseRow.ts +2 -2
- package/desktop/appcontainer/Banner.ts +2 -2
- package/desktop/cmp/dash/container/DashContainerModel.ts +3 -2
- package/desktop/cmp/dash/container/impl/DashContainerUtils.ts +4 -4
- package/desktop/cmp/dock/DockViewModel.ts +10 -8
- package/desktop/cmp/dock/impl/DockContainer.ts +1 -0
- package/inspector/instances/InstancesModel.ts +1 -1
- package/mobile/appcontainer/Banner.ts +2 -2
- package/mobile/cmp/menu/impl/Menu.scss +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 59.2.0 - 2023-10-16
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
|
|
7
|
+
* New `DockViewConfig.onClose` hook invoked when a user attempts to remove a `DockContainer` view
|
|
8
|
+
* Add `GridModel` APIs to lookup and show / hide entire column groups
|
|
9
|
+
* Left / right borders are now rendered along `Grid` `ColumnGroup` edges by default. Control
|
|
10
|
+
with new boolean property `ColumnGroupSpec.borders`
|
|
11
|
+
* The Cube package has been enhanced to support `Query` specific post-processing functions. See
|
|
12
|
+
new properties `Query.omitFn`, `Query.bucketSpecFn` and `Query.lockFn`. These properties default
|
|
13
|
+
to their respective properties on `Cube`.
|
|
14
|
+
|
|
15
|
+
### 🐞 Bug Fixes
|
|
16
|
+
|
|
17
|
+
* `DashContainerModel` fixes:
|
|
18
|
+
* Fix bug where `addView` would throw when adding a view to a row or column
|
|
19
|
+
* Fix bug where `allowRemove` flag was dropped from state for containers
|
|
20
|
+
* Fix bug in `DockContainer` where adding / removing views would cause other views to be remounted
|
|
21
|
+
* Fix erroneous `GridModel` warning when using a tree column within a column group
|
|
22
|
+
* Fix regression to alert banners. Resume allowing elements as messages
|
|
23
|
+
|
|
24
|
+
### ⚙️ Typescript API Adjustments
|
|
25
|
+
|
|
26
|
+
* Add type for `ActionFnData.record`
|
|
27
|
+
|
|
3
28
|
## 59.1.0 - 2023-09-20
|
|
4
29
|
|
|
5
30
|
### 🎁 New Features
|
|
@@ -11,7 +11,7 @@ import {HoistModel, LoadSpec, managed, XH, Intent, PlainObject} from '@xh/hoist/
|
|
|
11
11
|
import {dateIs, required} from '@xh/hoist/data';
|
|
12
12
|
import {action, makeObservable, observable} from '@xh/hoist/mobx';
|
|
13
13
|
import {AppModel} from '@xh/hoist/admin/AppModel';
|
|
14
|
-
import
|
|
14
|
+
import {some, sortBy, without} from 'lodash';
|
|
15
15
|
import {computed} from 'mobx';
|
|
16
16
|
|
|
17
17
|
export class AlertBannerModel extends HoistModel {
|
|
@@ -151,7 +151,7 @@ export class AlertBannerModel extends HoistModel {
|
|
|
151
151
|
@computed
|
|
152
152
|
get currentValuesSavedAsPreset() {
|
|
153
153
|
const {message, intent, iconName, enableClose} = this.formModel.values;
|
|
154
|
-
return
|
|
154
|
+
return some(this.savedPresets, {message, intent, iconName, enableClose});
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
async loadPresetsAsync() {
|
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
|
}
|
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
|
}
|
package/data/RecordAction.ts
CHANGED
package/data/cube/Query.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Copyright © 2023 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {Filter, parseFilter, StoreRecord} from '@xh/hoist/data';
|
|
8
|
+
import {BucketSpecFn, Filter, LockFn, OmitFn, parseFilter, StoreRecord} from '@xh/hoist/data';
|
|
9
9
|
import {isEqual, find} from 'lodash';
|
|
10
10
|
import {FilterLike, FilterTestFn} from '../filter/Types';
|
|
11
11
|
import {CubeField} from './CubeField';
|
|
@@ -51,6 +51,25 @@ export interface QueryConfig {
|
|
|
51
51
|
|
|
52
52
|
/** True to include leaf nodes in return.*/
|
|
53
53
|
includeLeaves?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Optional function to be called for each aggregate node to determine if it should be "locked",
|
|
57
|
+
* preventing drill-down into its children. Defaults to Cube.lockFn.
|
|
58
|
+
*/
|
|
59
|
+
lockFn?: LockFn;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional function to be called for each dimension during row generation to determine if the
|
|
63
|
+
* children of that dimension should be bucketed into additional dynamic dimensions.
|
|
64
|
+
* Defaults to Cube.bucketSpecFn.
|
|
65
|
+
*/
|
|
66
|
+
bucketSpecFn?: BucketSpecFn;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Optional function to be called on all single child rows during view processing.
|
|
70
|
+
* Return true to omit the row. Defaults to Cube.omitFn.
|
|
71
|
+
*/
|
|
72
|
+
omitFn?: OmitFn;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
75
|
/** {@inheritDoc QueryConfig} */
|
|
@@ -61,6 +80,9 @@ export class Query {
|
|
|
61
80
|
readonly includeRoot: boolean;
|
|
62
81
|
readonly includeLeaves: boolean;
|
|
63
82
|
readonly cube: Cube;
|
|
83
|
+
readonly lockFn: LockFn;
|
|
84
|
+
readonly bucketSpecFn: BucketSpecFn;
|
|
85
|
+
readonly omitFn: OmitFn;
|
|
64
86
|
|
|
65
87
|
private readonly _testFn: FilterTestFn;
|
|
66
88
|
|
|
@@ -70,7 +92,10 @@ export class Query {
|
|
|
70
92
|
dimensions,
|
|
71
93
|
filter = null,
|
|
72
94
|
includeRoot = false,
|
|
73
|
-
includeLeaves = false
|
|
95
|
+
includeLeaves = false,
|
|
96
|
+
lockFn = cube.lockFn,
|
|
97
|
+
bucketSpecFn = cube.bucketSpecFn,
|
|
98
|
+
omitFn = cube.omitFn
|
|
74
99
|
}: QueryConfig) {
|
|
75
100
|
this.cube = cube;
|
|
76
101
|
this.fields = this.parseFields(fields);
|
|
@@ -78,6 +103,9 @@ export class Query {
|
|
|
78
103
|
this.includeRoot = includeRoot;
|
|
79
104
|
this.includeLeaves = includeLeaves;
|
|
80
105
|
this.filter = parseFilter(filter);
|
|
106
|
+
this.lockFn = lockFn;
|
|
107
|
+
this.bucketSpecFn = bucketSpecFn;
|
|
108
|
+
this.omitFn = omitFn;
|
|
81
109
|
|
|
82
110
|
this._testFn = this.filter?.getTestFn(this.cube.store) ?? null;
|
|
83
111
|
}
|
|
@@ -89,6 +117,9 @@ export class Query {
|
|
|
89
117
|
filter: this.filter,
|
|
90
118
|
includeRoot: this.includeRoot,
|
|
91
119
|
includeLeaves: this.includeLeaves,
|
|
120
|
+
lockFn: this.lockFn,
|
|
121
|
+
bucketSpecFn: this.bucketSpecFn,
|
|
122
|
+
omitFn: this.omitFn,
|
|
92
123
|
cube: this.cube,
|
|
93
124
|
...overrides
|
|
94
125
|
};
|
|
@@ -121,7 +152,10 @@ export class Query {
|
|
|
121
152
|
isEqual(this.dimensions, other.dimensions) &&
|
|
122
153
|
this.cube === other.cube &&
|
|
123
154
|
this.includeRoot === other.includeRoot &&
|
|
124
|
-
this.includeLeaves === other.includeLeaves
|
|
155
|
+
this.includeLeaves === other.includeLeaves &&
|
|
156
|
+
this.bucketSpecFn == other.bucketSpecFn &&
|
|
157
|
+
this.omitFn == other.omitFn &&
|
|
158
|
+
this.lockFn == other.lockFn
|
|
125
159
|
);
|
|
126
160
|
}
|
|
127
161
|
|
package/data/cube/View.ts
CHANGED
|
@@ -351,9 +351,9 @@ export class View extends HoistBase {
|
|
|
351
351
|
parentId: string,
|
|
352
352
|
appliedDimensions: PlainObject
|
|
353
353
|
): BaseRow[] {
|
|
354
|
-
if (!this.
|
|
354
|
+
if (!this.query.bucketSpecFn) return rows;
|
|
355
355
|
|
|
356
|
-
const bucketSpec = this.
|
|
356
|
+
const bucketSpec = this.query.bucketSpecFn(rows);
|
|
357
357
|
if (!bucketSpec) return rows;
|
|
358
358
|
|
|
359
359
|
if (!this.query.includeLeaves && rows[0]?.isLeaf) return rows;
|
package/data/cube/row/BaseRow.ts
CHANGED
|
@@ -59,7 +59,7 @@ export abstract class BaseRow {
|
|
|
59
59
|
let dataChildren = this.getVisibleChildrenDatas();
|
|
60
60
|
|
|
61
61
|
// 2) If omitting ourselves, we are done, return visible children.
|
|
62
|
-
if (!isLeaf && view.
|
|
62
|
+
if (!isLeaf && view.query.omitFn?.(this as any)) return dataChildren;
|
|
63
63
|
|
|
64
64
|
// 3) Otherwise, we can attach this data to the children data and return.
|
|
65
65
|
|
|
@@ -89,7 +89,7 @@ export abstract class BaseRow {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Skip all children in a locked node
|
|
92
|
-
if (view.
|
|
92
|
+
if (view.query.lockFn?.(this as any)) {
|
|
93
93
|
this.locked = true;
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
@@ -11,7 +11,7 @@ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
|
|
|
11
11
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
12
12
|
import {Icon} from '@xh/hoist/icon';
|
|
13
13
|
import {markdown} from '@xh/hoist/cmp/markdown';
|
|
14
|
-
import {isFunction, isEmpty} from 'lodash';
|
|
14
|
+
import {isFunction, isEmpty, isString} from 'lodash';
|
|
15
15
|
import classNames from 'classnames';
|
|
16
16
|
|
|
17
17
|
import './Banner.scss';
|
|
@@ -44,7 +44,7 @@ export const banner = hoistCmp.factory({
|
|
|
44
44
|
icon,
|
|
45
45
|
div({
|
|
46
46
|
className: 'xh-banner__message',
|
|
47
|
-
item: markdown({content: message}),
|
|
47
|
+
item: isString(message) ? markdown({content: message}) : message,
|
|
48
48
|
onClick
|
|
49
49
|
})
|
|
50
50
|
]
|
|
@@ -22,7 +22,7 @@ import {wait} from '@xh/hoist/promise';
|
|
|
22
22
|
import {isOmitted} from '@xh/hoist/utils/impl';
|
|
23
23
|
import {debounced, ensureUniqueBy, throwIf} from '@xh/hoist/utils/js';
|
|
24
24
|
import {createObservableRef} from '@xh/hoist/utils/react';
|
|
25
|
-
import {cloneDeep, defaultsDeep, find, isFinite, isNil, reject, startCase} from 'lodash';
|
|
25
|
+
import {cloneDeep, defaultsDeep, find, isFinite, isNil, last, reject, startCase} from 'lodash';
|
|
26
26
|
import {createRoot} from 'react-dom/client';
|
|
27
27
|
import {DashConfig, DashModel} from '../';
|
|
28
28
|
import {DashViewModel, DashViewState} from '../DashViewModel';
|
|
@@ -290,7 +290,8 @@ export class DashContainerModel extends DashModel<
|
|
|
290
290
|
|
|
291
291
|
if (!isFinite(index)) index = container.contentItems.length;
|
|
292
292
|
container.addChild(goldenLayoutConfig(viewSpec), index);
|
|
293
|
-
|
|
293
|
+
const stack = container.isStack ? container : last(container.contentItems);
|
|
294
|
+
wait(1).then(() => this.onStackActiveItemChange(stack));
|
|
294
295
|
}
|
|
295
296
|
|
|
296
297
|
/**
|
|
@@ -46,8 +46,8 @@ function convertGLToStateInner(configItems = [], contentItems = [], dashContaine
|
|
|
46
46
|
|
|
47
47
|
ret.push(view);
|
|
48
48
|
} else {
|
|
49
|
-
const {type, width, height, activeItemIndex, content} = configItem,
|
|
50
|
-
container = {type} as PlainObject;
|
|
49
|
+
const {type, width, height, activeItemIndex, content, isClosable} = configItem,
|
|
50
|
+
container = {type, allowRemove: isClosable} as PlainObject;
|
|
51
51
|
|
|
52
52
|
if (isFinite(width)) container.width = round(width, 2);
|
|
53
53
|
if (isFinite(height)) container.height = round(height, 2);
|
|
@@ -138,12 +138,12 @@ function convertStateToGLInner(items = [], viewSpecs = [], containerSize, contai
|
|
|
138
138
|
const content = convertStateToGLInner(item.content, viewSpecs, itemSize, item).filter(
|
|
139
139
|
it => !isNil(it)
|
|
140
140
|
);
|
|
141
|
-
if (!content.length) return null;
|
|
141
|
+
if (!content.length && item.allowRemove) return null;
|
|
142
142
|
|
|
143
143
|
// Below is a workaround for issue https://github.com/golden-layout/golden-layout/issues/418
|
|
144
144
|
// GoldenLayouts can sometimes export its state with an out-of-bounds `activeItemIndex`.
|
|
145
145
|
// If we encounter this, we overwrite `activeItemIndex` to point to the last item.
|
|
146
|
-
const ret = {...item, content};
|
|
146
|
+
const ret = {...item, content, isClosable: item.allowRemove};
|
|
147
147
|
if (
|
|
148
148
|
type === 'stack' &&
|
|
149
149
|
isFinite(ret.activeItemIndex) &&
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
RefreshContextModel,
|
|
14
14
|
RefreshMode,
|
|
15
15
|
RenderMode,
|
|
16
|
-
|
|
16
|
+
Awaitable
|
|
17
17
|
} from '@xh/hoist/core';
|
|
18
18
|
import {ModalSupportModel} from '@xh/hoist/desktop/cmp/modalsupport/ModalSupportModel';
|
|
19
19
|
import '@xh/hoist/desktop/register';
|
|
@@ -53,6 +53,8 @@ export interface DockViewConfig {
|
|
|
53
53
|
allowClose?: boolean;
|
|
54
54
|
/** true (default) to allow popping out of the dock and displaying in a modal Dialog. */
|
|
55
55
|
allowDialog?: boolean;
|
|
56
|
+
/** Awaitable callback invoked on close. Return false to prevent close. */
|
|
57
|
+
onClose?: () => Awaitable<boolean | void>;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
|
@@ -75,6 +77,7 @@ export class DockViewModel extends HoistModel {
|
|
|
75
77
|
collapsedWidth: number;
|
|
76
78
|
allowClose: boolean;
|
|
77
79
|
allowDialog: boolean;
|
|
80
|
+
onClose?: () => Awaitable<boolean | void>;
|
|
78
81
|
|
|
79
82
|
containerModel: DockContainerModel;
|
|
80
83
|
@managed refreshContextModel: RefreshContextModel;
|
|
@@ -109,7 +112,8 @@ export class DockViewModel extends HoistModel {
|
|
|
109
112
|
docked = true,
|
|
110
113
|
collapsed = false,
|
|
111
114
|
allowClose = true,
|
|
112
|
-
allowDialog = true
|
|
115
|
+
allowDialog = true,
|
|
116
|
+
onClose
|
|
113
117
|
}: DockViewConfig) {
|
|
114
118
|
super();
|
|
115
119
|
makeObservable(this);
|
|
@@ -128,6 +132,7 @@ export class DockViewModel extends HoistModel {
|
|
|
128
132
|
this.collapsed = collapsed;
|
|
129
133
|
this.allowClose = allowClose;
|
|
130
134
|
this.allowDialog = allowDialog;
|
|
135
|
+
this.onClose = onClose;
|
|
131
136
|
|
|
132
137
|
this._renderMode = renderMode;
|
|
133
138
|
this._refreshMode = refreshMode;
|
|
@@ -195,11 +200,8 @@ export class DockViewModel extends HoistModel {
|
|
|
195
200
|
// Actions
|
|
196
201
|
//-----------------------
|
|
197
202
|
close() {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
override destroy() {
|
|
202
|
-
XH.safeDestroy(this.content);
|
|
203
|
-
super.destroy();
|
|
203
|
+
Promise.resolve(this.onClose?.()).then(v => {
|
|
204
|
+
if (v !== false) this.containerModel.removeView(this.id);
|
|
205
|
+
});
|
|
204
206
|
}
|
|
205
207
|
}
|
|
@@ -218,7 +218,7 @@ export class InstancesModel extends HoistModel {
|
|
|
218
218
|
icon: Icon.refresh({intent: 'success'}),
|
|
219
219
|
tooltip: 'Call loadAsync()',
|
|
220
220
|
actionFn: ({record}) =>
|
|
221
|
-
(this.getInstance(record.id) as any)?.loadAsync(),
|
|
221
|
+
(this.getInstance(record.id as string) as any)?.loadAsync(),
|
|
222
222
|
displayFn: ({record}) => ({hidden: !record.data.hasLoadSupport})
|
|
223
223
|
}
|
|
224
224
|
]
|
|
@@ -10,7 +10,7 @@ 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
12
|
import {markdown} from '@xh/hoist/cmp/markdown';
|
|
13
|
-
import {isEmpty, isFunction} from 'lodash';
|
|
13
|
+
import {isEmpty, isFunction, isString} from 'lodash';
|
|
14
14
|
import classNames from 'classnames';
|
|
15
15
|
|
|
16
16
|
import './Banner.scss';
|
|
@@ -42,7 +42,7 @@ export const banner = hoistCmp.factory({
|
|
|
42
42
|
icon,
|
|
43
43
|
div({
|
|
44
44
|
className: 'xh-banner__message',
|
|
45
|
-
item: markdown({content: message}),
|
|
45
|
+
item: isString(message) ? markdown({content: message}) : message,
|
|
46
46
|
onClick
|
|
47
47
|
})
|
|
48
48
|
]
|
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
position: fixed;
|
|
3
3
|
margin: 0 !important;
|
|
4
4
|
z-index: 21;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Use `svh` units target the *smallest* viewport height, which is preferred to using `vh` on mobile.
|
|
7
|
+
* Including `px` units as a fallback for older browsers that may not support `vh`/`svh` units.
|
|
8
|
+
* https://css-tricks.com/the-large-small-and-dynamic-viewports/
|
|
9
|
+
*/
|
|
10
|
+
max-height: 500px;
|
|
11
|
+
max-height: 80svh;
|
|
6
12
|
|
|
7
13
|
&__title {
|
|
8
14
|
padding: var(--xh-pad-half-px);
|