@xh/hoist 59.2.0 → 59.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -14
- package/admin/AppComponent.ts +1 -1
- package/admin/tabs/activity/ActivityTab.ts +1 -1
- package/admin/tabs/general/GeneralTab.ts +2 -2
- package/admin/tabs/general/config/ConfigPanel.ts +1 -0
- package/admin/tabs/monitor/MonitorTab.ts +1 -1
- package/admin/tabs/server/ServerTab.ts +1 -1
- package/admin/tabs/userData/UserDataTab.ts +1 -1
- package/cmp/ag-grid/AgGrid.scss +51 -25
- package/cmp/ag-grid/AgGrid.ts +8 -2
- package/cmp/badge/Badge.ts +18 -5
- package/cmp/chart/Chart.ts +13 -11
- package/cmp/clock/Clock.ts +6 -5
- package/cmp/dataview/DataView.ts +5 -3
- package/cmp/form/Form.ts +25 -6
- package/cmp/grid/Grid.ts +41 -25
- package/cmp/grid/GridModel.ts +2 -2
- package/cmp/grid/Types.ts +1 -1
- package/cmp/grid/columns/Column.ts +45 -2
- package/cmp/grid/impl/GridHScrollbar.ts +140 -0
- package/cmp/grid/renderers/MultiFieldRenderer.ts +1 -1
- package/cmp/input/HoistInputModel.ts +4 -4
- package/cmp/input/HoistInputProps.ts +3 -1
- package/cmp/layout/Box.ts +4 -2
- package/cmp/relativetimestamp/RelativeTimestamp.ts +106 -40
- package/cmp/store/StoreFilterField.ts +2 -2
- package/cmp/tab/TabContainer.ts +1 -1
- package/cmp/zoneGrid/Types.ts +47 -0
- package/cmp/zoneGrid/ZoneGrid.ts +62 -0
- package/cmp/zoneGrid/ZoneGridModel.ts +666 -0
- package/cmp/zoneGrid/impl/ZoneGridPersistenceModel.ts +143 -0
- package/cmp/zoneGrid/impl/ZoneMapperModel.ts +335 -0
- package/cmp/zoneGrid/index.ts +3 -0
- package/core/HoistComponent.ts +23 -10
- package/core/HoistProps.ts +25 -6
- package/core/XH.ts +49 -27
- package/core/elem.ts +11 -3
- package/core/impl/InstanceManager.ts +24 -1
- package/core/model/HoistModel.ts +4 -4
- package/data/RecordAction.ts +7 -4
- package/data/StoreRecord.ts +8 -1
- package/desktop/appcontainer/AppContainer.ts +2 -0
- package/desktop/cmp/appbar/AppBar.ts +8 -6
- package/desktop/cmp/button/Button.ts +14 -3
- package/desktop/cmp/button/ButtonGroup.ts +14 -3
- package/desktop/cmp/button/ZoneMapperButton.ts +82 -0
- package/desktop/cmp/button/index.ts +1 -0
- package/desktop/cmp/dash/canvas/DashCanvas.ts +14 -4
- package/desktop/cmp/dash/container/DashContainer.ts +11 -4
- package/desktop/cmp/error/ErrorMessage.ts +9 -8
- package/desktop/cmp/form/FormField.ts +34 -10
- package/desktop/cmp/grid/columns/Actions.ts +2 -1
- package/desktop/cmp/grid/impl/colchooser/ColChooser.ts +3 -2
- package/desktop/cmp/grouping/GroupingChooser.ts +29 -29
- package/desktop/cmp/input/ButtonGroupInput.ts +1 -1
- package/desktop/cmp/input/Checkbox.ts +3 -3
- package/desktop/cmp/input/CodeInput.ts +2 -1
- package/desktop/cmp/input/DateInput.ts +128 -123
- package/desktop/cmp/input/JsonInput.ts +1 -1
- package/desktop/cmp/input/NumberInput.ts +3 -2
- package/desktop/cmp/input/RadioInput.ts +3 -1
- package/desktop/cmp/input/Select.ts +31 -4
- package/desktop/cmp/input/SwitchInput.ts +2 -1
- package/desktop/cmp/input/TextArea.ts +3 -3
- package/desktop/cmp/input/TextInput.ts +51 -47
- package/desktop/cmp/panel/Panel.ts +21 -19
- package/desktop/cmp/panel/impl/ResizeContainer.ts +3 -2
- package/desktop/cmp/pinpad/impl/PinPad.ts +4 -3
- package/desktop/cmp/record/RecordActionBar.ts +12 -3
- package/desktop/cmp/record/impl/RecordActionButton.ts +1 -0
- package/desktop/cmp/rest/Actions.ts +10 -5
- package/desktop/cmp/rest/RestGrid.ts +20 -6
- package/desktop/cmp/rest/impl/RestForm.ts +5 -4
- package/desktop/cmp/rest/impl/RestGridToolbar.ts +3 -2
- package/desktop/cmp/tab/TabSwitcher.ts +8 -3
- package/desktop/cmp/tab/impl/Tab.ts +2 -1
- package/desktop/cmp/tab/impl/TabContainer.ts +18 -15
- package/desktop/cmp/toolbar/Toolbar.ts +3 -1
- package/desktop/cmp/treemap/SplitTreeMap.ts +2 -1
- package/desktop/cmp/treemap/TreeMap.ts +5 -3
- package/desktop/cmp/zoneGrid/impl/ZoneMapper.scss +71 -0
- package/desktop/cmp/zoneGrid/impl/ZoneMapper.ts +232 -0
- package/desktop/cmp/zoneGrid/impl/ZoneMapperDialog.ts +35 -0
- package/dynamics/desktop.ts +2 -0
- package/dynamics/mobile.ts +2 -0
- package/inspector/instances/InstancesModel.ts +2 -2
- package/mobile/appcontainer/AppContainer.ts +2 -0
- package/mobile/cmp/button/ZoneMapperButton.ts +41 -0
- package/mobile/cmp/button/index.ts +1 -0
- package/mobile/cmp/error/ErrorMessage.ts +4 -4
- package/mobile/cmp/input/Select.scss +1 -0
- package/mobile/cmp/input/Select.ts +7 -0
- package/mobile/cmp/input/TextInput.ts +1 -0
- package/mobile/cmp/panel/DialogPanel.scss +18 -6
- package/mobile/cmp/panel/DialogPanel.ts +3 -1
- package/mobile/cmp/zoneGrid/impl/ZoneMapper.scss +67 -0
- package/mobile/cmp/zoneGrid/impl/ZoneMapper.ts +236 -0
- package/package.json +4 -3
- package/styles/vars.scss +3 -3
- package/svc/InspectorService.ts +1 -1
- package/utils/js/DomUtils.ts +10 -0
- package/utils/js/LangUtils.ts +10 -0
- package/utils/js/TestUtils.ts +9 -0
- package/utils/js/index.ts +1 -0
|
@@ -0,0 +1,666 @@
|
|
|
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 {
|
|
8
|
+
HoistModel,
|
|
9
|
+
LoadSpec,
|
|
10
|
+
PlainObject,
|
|
11
|
+
Some,
|
|
12
|
+
managed,
|
|
13
|
+
XH,
|
|
14
|
+
Awaitable,
|
|
15
|
+
VSide
|
|
16
|
+
} from '@xh/hoist/core';
|
|
17
|
+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
|
|
18
|
+
import {
|
|
19
|
+
RecordAction,
|
|
20
|
+
Store,
|
|
21
|
+
StoreConfig,
|
|
22
|
+
StoreRecordOrId,
|
|
23
|
+
StoreSelectionConfig,
|
|
24
|
+
StoreSelectionModel,
|
|
25
|
+
StoreTransaction
|
|
26
|
+
} from '@xh/hoist/data';
|
|
27
|
+
import {
|
|
28
|
+
Column,
|
|
29
|
+
ColumnSpec,
|
|
30
|
+
Grid,
|
|
31
|
+
GridConfig,
|
|
32
|
+
GridContextMenuSpec,
|
|
33
|
+
GridGroupSortFn,
|
|
34
|
+
GridModel,
|
|
35
|
+
GridSorter,
|
|
36
|
+
GridSorterLike,
|
|
37
|
+
GroupRowRenderer,
|
|
38
|
+
RowClassFn,
|
|
39
|
+
RowClassRuleFn,
|
|
40
|
+
TreeStyle,
|
|
41
|
+
multiFieldRenderer
|
|
42
|
+
} from '@xh/hoist/cmp/grid';
|
|
43
|
+
import {
|
|
44
|
+
CellClickedEvent,
|
|
45
|
+
CellContextMenuEvent,
|
|
46
|
+
CellDoubleClickedEvent,
|
|
47
|
+
RowClickedEvent,
|
|
48
|
+
RowDoubleClickedEvent
|
|
49
|
+
} from '@ag-grid-community/core';
|
|
50
|
+
import {Icon} from '@xh/hoist/icon';
|
|
51
|
+
import {throwIf, withDefault} from '@xh/hoist/utils/js';
|
|
52
|
+
import {castArray, forOwn, isEmpty, isFinite, isPlainObject, isString} from 'lodash';
|
|
53
|
+
import {ReactNode} from 'react';
|
|
54
|
+
import {ZoneMapperConfig, ZoneMapperModel} from './impl/ZoneMapperModel';
|
|
55
|
+
import {ZoneGridPersistenceModel} from './impl/ZoneGridPersistenceModel';
|
|
56
|
+
import {ZoneGridModelPersistOptions, Zone, ZoneLimit, ZoneMapping} from './Types';
|
|
57
|
+
|
|
58
|
+
export interface ZoneGridConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Available columns for this grid. Note that the actual display of
|
|
61
|
+
* the zone columns is managed via `mappings` below.
|
|
62
|
+
*/
|
|
63
|
+
columns: Array<ColumnSpec>;
|
|
64
|
+
|
|
65
|
+
/** Mappings of columns to zones. */
|
|
66
|
+
mappings: Record<Zone, Some<string | ZoneMapping>>;
|
|
67
|
+
|
|
68
|
+
/** Optional configurations for zone constraints. */
|
|
69
|
+
limits?: Partial<Record<Zone, ZoneLimit>>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Optional configs to apply to left column. Intended for use as an `escape hatch`, and should be used with care.
|
|
73
|
+
* Settings made here may interfere with the implementation of this component.
|
|
74
|
+
*/
|
|
75
|
+
leftColumnSpec?: Partial<ColumnSpec>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Optional configs to apply to right column. Intended for use as an `escape hatch`, and should be used with care.
|
|
79
|
+
* Settings made here may interfere with the implementation of this component.
|
|
80
|
+
*/
|
|
81
|
+
rightColumnSpec?: Partial<ColumnSpec>;
|
|
82
|
+
|
|
83
|
+
/** String rendered between consecutive SubFields. */
|
|
84
|
+
delimiter?: string;
|
|
85
|
+
|
|
86
|
+
/** Config with which to create a ZoneMapperModel, or boolean `true` to enable default. */
|
|
87
|
+
zoneMapperModel?: ZoneMapperConfig | boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A Store instance, or a config with which to create a Store. If not supplied,
|
|
91
|
+
* store fields will be inferred from columns config.
|
|
92
|
+
*/
|
|
93
|
+
store?: Store | StoreConfig;
|
|
94
|
+
|
|
95
|
+
/** True if grid is a tree grid (default false). */
|
|
96
|
+
treeMode?: boolean;
|
|
97
|
+
|
|
98
|
+
/** Location for a docked summary row. Requires `store.SummaryRecord` to be populated. */
|
|
99
|
+
showSummary?: boolean | VSide;
|
|
100
|
+
|
|
101
|
+
/** Specification of selection behavior. Defaults to 'single' (desktop) and 'disabled' (mobile) */
|
|
102
|
+
selModel?: StoreSelectionModel | StoreSelectionConfig | 'single' | 'multiple' | 'disabled';
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Function to be called when the user triggers ZoneGridModel.restoreDefaultsAsync().
|
|
106
|
+
* This function will be called after the built-in defaults have been restored, and can be
|
|
107
|
+
* used to restore application specific defaults.
|
|
108
|
+
*/
|
|
109
|
+
restoreDefaultsFn?: () => Awaitable<boolean>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Confirmation warning to be presented to user before restoring default state. Set to
|
|
113
|
+
* null to skip user confirmation.
|
|
114
|
+
*/
|
|
115
|
+
restoreDefaultsWarning?: ReactNode;
|
|
116
|
+
|
|
117
|
+
/** Options governing persistence. */
|
|
118
|
+
persistWith?: ZoneGridModelPersistOptions;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Text/element to display if grid has no records. Defaults to null, in which case no empty
|
|
122
|
+
* text will be shown.
|
|
123
|
+
*/
|
|
124
|
+
emptyText?: ReactNode;
|
|
125
|
+
|
|
126
|
+
/** True (default) to hide empty text until after the Store has been loaded at least once. */
|
|
127
|
+
hideEmptyTextBeforeLoad?: boolean;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Initial sort to apply to grid data.
|
|
131
|
+
* Note that unlike GridModel, multi-sort is not supported.
|
|
132
|
+
*/
|
|
133
|
+
sortBy?: GridSorterLike;
|
|
134
|
+
|
|
135
|
+
/** Column ID(s) by which to do full-width grouping. */
|
|
136
|
+
groupBy?: Some<string>;
|
|
137
|
+
|
|
138
|
+
/** True (default) to show a count of group member rows within each full-width group row. */
|
|
139
|
+
showGroupRowCounts?: boolean;
|
|
140
|
+
|
|
141
|
+
/** True to highlight the currently hovered row. */
|
|
142
|
+
showHover?: boolean;
|
|
143
|
+
|
|
144
|
+
/** True to render row borders. */
|
|
145
|
+
rowBorders?: boolean;
|
|
146
|
+
|
|
147
|
+
/** Specify treeMode-specific styling. */
|
|
148
|
+
treeStyle?: TreeStyle;
|
|
149
|
+
|
|
150
|
+
/** True to use alternating backgrounds for rows. */
|
|
151
|
+
stripeRows?: boolean;
|
|
152
|
+
|
|
153
|
+
/** True to render cell borders. */
|
|
154
|
+
cellBorders?: boolean;
|
|
155
|
+
|
|
156
|
+
/** True to highlight the focused cell with a border. */
|
|
157
|
+
showCellFocus?: boolean;
|
|
158
|
+
|
|
159
|
+
/** True to suppress display of the grid's header row. */
|
|
160
|
+
hideHeaders?: boolean;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Closure to generate CSS class names for a row.
|
|
164
|
+
* NOTE that, once added, classes will *not* be removed if the data changes.
|
|
165
|
+
* Use `rowClassRules` instead if StoreRecord data can change across refreshes.
|
|
166
|
+
*/
|
|
167
|
+
rowClassFn?: RowClassFn;
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Object keying CSS class names to functions determining if they should be added or
|
|
171
|
+
* removed from the row. See Ag-Grid docs on "row styles" for details.
|
|
172
|
+
*/
|
|
173
|
+
rowClassRules?: Record<string, RowClassRuleFn>;
|
|
174
|
+
|
|
175
|
+
/** Height (in px) of a group row. Note that this will override `sizingMode` for group rows. */
|
|
176
|
+
groupRowHeight?: number;
|
|
177
|
+
|
|
178
|
+
/** Function used to render group rows. */
|
|
179
|
+
groupRowRenderer?: GroupRowRenderer;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Function to use to sort full-row groups. Called with two group values to compare
|
|
183
|
+
* in the form of a standard JS comparator. Default is an ascending string sort.
|
|
184
|
+
* Set to `null` to prevent sorting of groups.
|
|
185
|
+
*/
|
|
186
|
+
groupSortFn?: GridGroupSortFn;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Callback when a key down event is detected on the grid. Note that the ag-Grid API provides
|
|
190
|
+
* limited ability to customize keyboard handling. This handler is designed to allow
|
|
191
|
+
* applications to work around this.
|
|
192
|
+
*/
|
|
193
|
+
onKeyDown?: (e: KeyboardEvent) => void;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Callback when a row is clicked. (Note that the event received may be null - e.g. for
|
|
197
|
+
* clicks on full-width group rows.)
|
|
198
|
+
*/
|
|
199
|
+
onRowClicked?: (e: RowClickedEvent) => void;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Callback when a row is double-clicked. (Note that the event received may be null - e.g.
|
|
203
|
+
* for clicks on full-width group rows.)
|
|
204
|
+
*/
|
|
205
|
+
onRowDoubleClicked?: (e: RowDoubleClickedEvent) => void;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Callback when a cell is clicked.
|
|
209
|
+
*/
|
|
210
|
+
onCellClicked?: (e: CellClickedEvent) => void;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Callback when a cell is double-clicked.
|
|
214
|
+
*/
|
|
215
|
+
onCellDoubleClicked?: (e: CellDoubleClickedEvent) => void;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Callback when the context menu is opened. Note that the event received can also be
|
|
219
|
+
* triggered via a long press (aka tap and hold) on mobile devices.
|
|
220
|
+
*/
|
|
221
|
+
onCellContextMenu?: (e: CellContextMenuEvent) => void;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Number of clicks required to expand / collapse a parent row in a tree grid. Defaults
|
|
225
|
+
* to 2 for desktop, 1 for mobile. Any other value prevents clicks on row body from
|
|
226
|
+
* expanding / collapsing (requires click on tree col affordance to expand/collapse).
|
|
227
|
+
*/
|
|
228
|
+
clicksToExpand?: number;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Array of RecordActions, dividers, or token strings with which to create a context menu.
|
|
232
|
+
* May also be specified as a function returning same.
|
|
233
|
+
*/
|
|
234
|
+
contextMenu?: GridContextMenuSpec;
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Governs if the grid should reuse a limited set of DOM elements for columns visible in the
|
|
238
|
+
* scroll area (versus rendering all columns). Consider this performance optimization for
|
|
239
|
+
* grids with a very large number of columns obscured by horizontal scrolling. Note that
|
|
240
|
+
* setting this value to true may limit the ability of the grid to autosize offscreen columns
|
|
241
|
+
* effectively. Default false.
|
|
242
|
+
*/
|
|
243
|
+
useVirtualColumns?: boolean;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Set to true to if application will be reloading data when the sortBy property changes on
|
|
247
|
+
* this model (either programmatically, or via user-click.) Useful for applications with large
|
|
248
|
+
* data sets that are performing external, or server-side sorting and filtering. Setting this
|
|
249
|
+
* flag means that the grid should not immediately respond to user or programmatic changes to
|
|
250
|
+
* the sortBy property, but will instead wait for the next load of data, which is assumed to be
|
|
251
|
+
* pre-sorted. Default false.
|
|
252
|
+
*/
|
|
253
|
+
externalSort?: boolean;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Set to true to highlight a row on click. Intended to provide feedback to users in grids
|
|
257
|
+
* without selection. Note this setting overrides the styling used by Column.highlightOnChange,
|
|
258
|
+
* and is not recommended for use alongside that feature. Default true for mobiles,
|
|
259
|
+
* otherwise false.
|
|
260
|
+
*/
|
|
261
|
+
highlightRowOnClick?: boolean;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Flags for experimental features. These features are designed for early client-access and
|
|
265
|
+
* testing, but are not yet part of the Hoist API.
|
|
266
|
+
*/
|
|
267
|
+
experimental?: PlainObject;
|
|
268
|
+
|
|
269
|
+
/** Extra app-specific data for the GridModel. */
|
|
270
|
+
appData?: PlainObject;
|
|
271
|
+
|
|
272
|
+
/** @internal */
|
|
273
|
+
xhImpl?: boolean;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* ZoneGridModel is a wrapper around GridModel, which shows date in a grid with multi-line
|
|
278
|
+
* full-width rows, each broken into four zones for top/bottom and left/right.
|
|
279
|
+
*
|
|
280
|
+
* This is the primary app entry-point for specifying ZoneGrid component options and behavior.
|
|
281
|
+
*/
|
|
282
|
+
export class ZoneGridModel extends HoistModel {
|
|
283
|
+
@managed
|
|
284
|
+
gridModel: GridModel;
|
|
285
|
+
|
|
286
|
+
@managed
|
|
287
|
+
mapperModel: ZoneMapperModel;
|
|
288
|
+
|
|
289
|
+
@observable.ref
|
|
290
|
+
mappings: Record<Zone, ZoneMapping[]>;
|
|
291
|
+
|
|
292
|
+
@bindable.ref
|
|
293
|
+
leftColumnSpec: Partial<ColumnSpec>;
|
|
294
|
+
|
|
295
|
+
@bindable.ref
|
|
296
|
+
rightColumnSpec: Partial<ColumnSpec>;
|
|
297
|
+
|
|
298
|
+
availableColumns: ColumnSpec[];
|
|
299
|
+
limits: Partial<Record<Zone, ZoneLimit>>;
|
|
300
|
+
delimiter: string;
|
|
301
|
+
restoreDefaultsFn: () => Awaitable<boolean>;
|
|
302
|
+
restoreDefaultsWarning: ReactNode;
|
|
303
|
+
|
|
304
|
+
private _defaultState; // initial state provided to ctor - powers restoreDefaults().
|
|
305
|
+
@managed persistenceModel: ZoneGridPersistenceModel;
|
|
306
|
+
|
|
307
|
+
constructor(config: ZoneGridConfig) {
|
|
308
|
+
super();
|
|
309
|
+
makeObservable(this);
|
|
310
|
+
|
|
311
|
+
const {
|
|
312
|
+
columns,
|
|
313
|
+
limits,
|
|
314
|
+
mappings,
|
|
315
|
+
sortBy,
|
|
316
|
+
groupBy,
|
|
317
|
+
leftColumnSpec,
|
|
318
|
+
rightColumnSpec,
|
|
319
|
+
delimiter,
|
|
320
|
+
zoneMapperModel,
|
|
321
|
+
restoreDefaultsFn,
|
|
322
|
+
restoreDefaultsWarning,
|
|
323
|
+
persistWith,
|
|
324
|
+
...rest
|
|
325
|
+
} = config;
|
|
326
|
+
|
|
327
|
+
this.availableColumns = columns.map(it => ({...it, hidden: true}));
|
|
328
|
+
this.limits = limits;
|
|
329
|
+
this.mappings = this.parseMappings(mappings);
|
|
330
|
+
|
|
331
|
+
this.leftColumnSpec = leftColumnSpec;
|
|
332
|
+
this.rightColumnSpec = rightColumnSpec;
|
|
333
|
+
this.delimiter = delimiter ?? ' • ';
|
|
334
|
+
this.restoreDefaultsFn = restoreDefaultsFn;
|
|
335
|
+
this.restoreDefaultsWarning = restoreDefaultsWarning;
|
|
336
|
+
|
|
337
|
+
this._defaultState = {
|
|
338
|
+
mappings: this.mappings,
|
|
339
|
+
sortBy: sortBy,
|
|
340
|
+
groupBy: groupBy
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
this.gridModel = this.createGridModel(rest);
|
|
344
|
+
|
|
345
|
+
this.setSortBy(sortBy);
|
|
346
|
+
this.setGroupBy(groupBy);
|
|
347
|
+
|
|
348
|
+
this.mapperModel = this.parseMapperModel(zoneMapperModel);
|
|
349
|
+
this.persistenceModel = persistWith
|
|
350
|
+
? new ZoneGridPersistenceModel(this, persistWith)
|
|
351
|
+
: null;
|
|
352
|
+
|
|
353
|
+
this.addReaction({
|
|
354
|
+
track: () => [this.leftColumnSpec, this.rightColumnSpec],
|
|
355
|
+
run: () => this.gridModel.setColumns(this.getColumns())
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Restore the mapping, sorting, and grouping configs as specified by the application at
|
|
361
|
+
* construction time. This is the state without any user changes applied.
|
|
362
|
+
* This method will clear the persistent grid state saved for this grid, if any.
|
|
363
|
+
*
|
|
364
|
+
* @returns true if defaults were restored
|
|
365
|
+
*/
|
|
366
|
+
async restoreDefaultsAsync(): Promise<boolean> {
|
|
367
|
+
if (this.restoreDefaultsWarning) {
|
|
368
|
+
const confirmed = await XH.confirm({
|
|
369
|
+
title: 'Please Confirm',
|
|
370
|
+
icon: Icon.warning(),
|
|
371
|
+
message: this.restoreDefaultsWarning,
|
|
372
|
+
confirmProps: {
|
|
373
|
+
text: 'Yes, restore defaults',
|
|
374
|
+
intent: 'primary'
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
if (!confirmed) return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const {mappings, sortBy, groupBy} = this._defaultState;
|
|
381
|
+
this.setMappings(mappings);
|
|
382
|
+
this.setSortBy(sortBy);
|
|
383
|
+
this.setGroupBy(groupBy);
|
|
384
|
+
|
|
385
|
+
this.persistenceModel?.clear();
|
|
386
|
+
|
|
387
|
+
if (this.restoreDefaultsFn) {
|
|
388
|
+
await this.restoreDefaultsFn();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
showMapper() {
|
|
395
|
+
this.mapperModel.open();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@action
|
|
399
|
+
setMappings(mappings: Record<Zone, Some<string | ZoneMapping>>) {
|
|
400
|
+
this.mappings = this.parseMappings(mappings);
|
|
401
|
+
this.gridModel.setColumns(this.getColumns());
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getDefaultContextMenu = () => [
|
|
405
|
+
'filter',
|
|
406
|
+
'-',
|
|
407
|
+
'copy',
|
|
408
|
+
'copyWithHeaders',
|
|
409
|
+
'copyCell',
|
|
410
|
+
'-',
|
|
411
|
+
'expandCollapseAll',
|
|
412
|
+
'-',
|
|
413
|
+
'restoreDefaults',
|
|
414
|
+
'-',
|
|
415
|
+
new RecordAction({
|
|
416
|
+
text: 'Customize Fields',
|
|
417
|
+
icon: Icon.gridLarge(),
|
|
418
|
+
hidden: !this?.mapperModel,
|
|
419
|
+
actionFn: () => (this?.mapperModel as any)?.open()
|
|
420
|
+
})
|
|
421
|
+
];
|
|
422
|
+
|
|
423
|
+
//-----------------------
|
|
424
|
+
// Getters and methods trampolined from GridModel.
|
|
425
|
+
//-----------------------
|
|
426
|
+
get sortBy(): GridSorter {
|
|
427
|
+
const ret = this.gridModel.sortBy?.[0];
|
|
428
|
+
if (!ret) return null;
|
|
429
|
+
|
|
430
|
+
// Normalize 'left_column' and 'right_column' to actual underlying fields
|
|
431
|
+
if (ret?.colId === 'left_column') {
|
|
432
|
+
const colId = this.mappings.tl[0]?.field;
|
|
433
|
+
return colId ? new GridSorter({...ret, colId}) : null;
|
|
434
|
+
} else if (ret?.colId === 'right_column') {
|
|
435
|
+
const colId = this.mappings.tr[0]?.field;
|
|
436
|
+
return colId ? new GridSorter({...ret, colId}) : null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return ret;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
setSortBy(cfg: GridSorterLike) {
|
|
443
|
+
// If the field is mapping to the primary field in a left/right column, set
|
|
444
|
+
// 'left_column'/'right_column' colId instead to display the arrows in the header.
|
|
445
|
+
const sorter = GridSorter.parse(cfg);
|
|
446
|
+
if (sorter?.colId === this.mappings.tl[0]?.field) {
|
|
447
|
+
return this.gridModel.setSortBy({...sorter, colId: 'left_column'});
|
|
448
|
+
}
|
|
449
|
+
if (sorter?.colId === this.mappings.tr[0]?.field) {
|
|
450
|
+
return this.gridModel.setSortBy({...sorter, colId: 'right_column'});
|
|
451
|
+
}
|
|
452
|
+
return this.gridModel.setSortBy(sorter);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
get store() {
|
|
456
|
+
return this.gridModel.store;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
get empty() {
|
|
460
|
+
return this.gridModel.empty;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
get selModel() {
|
|
464
|
+
return this.gridModel.selModel;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
get hasSelection() {
|
|
468
|
+
return this.gridModel.hasSelection;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
get selectedRecords() {
|
|
472
|
+
return this.gridModel.selectedRecords;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
get selectedRecord() {
|
|
476
|
+
return this.gridModel.selectedRecord;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
get selectedId() {
|
|
480
|
+
return this.gridModel.selectedId;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
get groupBy() {
|
|
484
|
+
return this.gridModel.groupBy;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
selectAsync(
|
|
488
|
+
records: Some<StoreRecordOrId>,
|
|
489
|
+
opts: {ensureVisible?: boolean; clearSelection?: boolean}
|
|
490
|
+
) {
|
|
491
|
+
return this.gridModel.selectAsync(records, opts);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
preSelectFirstAsync() {
|
|
495
|
+
return this.gridModel.preSelectFirstAsync();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
selectFirstAsync(opts: {ensureVisible?: boolean} = {}) {
|
|
499
|
+
return this.gridModel.selectFirstAsync(opts);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
ensureSelectionVisibleAsync() {
|
|
503
|
+
return this.gridModel.ensureSelectionVisibleAsync();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
override doLoadAsync(loadSpec: LoadSpec) {
|
|
507
|
+
return this.gridModel.doLoadAsync(loadSpec);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
loadData(rawData: any[], rawSummaryData?: PlainObject) {
|
|
511
|
+
return this.gridModel.loadData(rawData, rawSummaryData);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
updateData(rawData: PlainObject[] | StoreTransaction) {
|
|
515
|
+
return this.gridModel.updateData(rawData);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
clear() {
|
|
519
|
+
return this.gridModel.clear();
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
setGroupBy(colIds: Some<string>) {
|
|
523
|
+
return this.gridModel.setGroupBy(colIds);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
//-----------------------
|
|
527
|
+
// Implementation
|
|
528
|
+
//-----------------------
|
|
529
|
+
private createGridModel(config: GridConfig): GridModel {
|
|
530
|
+
return new GridModel({
|
|
531
|
+
...config,
|
|
532
|
+
xhImpl: true,
|
|
533
|
+
contextMenu: withDefault(config.contextMenu, this.getDefaultContextMenu),
|
|
534
|
+
sizingMode: 'standard',
|
|
535
|
+
cellBorders: true,
|
|
536
|
+
rowBorders: true,
|
|
537
|
+
stripeRows: false,
|
|
538
|
+
autosizeOptions: {mode: 'disabled'},
|
|
539
|
+
columns: this.getColumns()
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private getColumns(): ColumnSpec[] {
|
|
544
|
+
return [
|
|
545
|
+
this.buildZoneColumn(true),
|
|
546
|
+
this.buildZoneColumn(false),
|
|
547
|
+
// Ensure all available columns are provided as hidden columns for lookup by multifield renderer
|
|
548
|
+
...this.availableColumns
|
|
549
|
+
];
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private buildZoneColumn(isLeft: boolean): ColumnSpec {
|
|
553
|
+
const topMappings = this.mappings[isLeft ? 'tl' : 'tr'],
|
|
554
|
+
bottomMappings = this.mappings[isLeft ? 'bl' : 'br'];
|
|
555
|
+
|
|
556
|
+
throwIf(
|
|
557
|
+
isEmpty(topMappings),
|
|
558
|
+
`${isLeft ? 'Left' : 'Right'} column requires at least one top mapping`
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// Extract the primary column from the top mappings
|
|
562
|
+
const primaryCol = new Column(this.findColumnSpec(topMappings[0]), this.gridModel);
|
|
563
|
+
|
|
564
|
+
// Extract the sub-fields from the other mappings
|
|
565
|
+
const subFields = [];
|
|
566
|
+
topMappings.slice(1).forEach(it => {
|
|
567
|
+
subFields.push({colId: it.field, label: it.showLabel, position: 'top'});
|
|
568
|
+
});
|
|
569
|
+
bottomMappings.forEach(it => {
|
|
570
|
+
subFields.push({colId: it.field, label: it.showLabel, position: 'bottom'});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
// Controlled properties
|
|
575
|
+
field: isLeft ? 'left_column' : 'right_column',
|
|
576
|
+
flex: isLeft ? 2 : 1,
|
|
577
|
+
align: isLeft ? 'left' : 'right',
|
|
578
|
+
renderer: multiFieldRenderer,
|
|
579
|
+
rowHeight: Grid['MULTIFIELD_ROW_HEIGHT'],
|
|
580
|
+
resizable: false,
|
|
581
|
+
movable: false,
|
|
582
|
+
hideable: false,
|
|
583
|
+
appData: {
|
|
584
|
+
multiFieldConfig: {
|
|
585
|
+
mainRenderer: primaryCol.renderer,
|
|
586
|
+
delimiter: this.delimiter,
|
|
587
|
+
subFields
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
|
|
591
|
+
// Properties inherited from primary column
|
|
592
|
+
headerName: primaryCol.headerName,
|
|
593
|
+
absSort: primaryCol.absSort,
|
|
594
|
+
sortingOrder: primaryCol.sortingOrder,
|
|
595
|
+
sortValue: primaryCol.sortValue,
|
|
596
|
+
sortToBottom: primaryCol.sortToBottom,
|
|
597
|
+
comparator: primaryCol.comparator,
|
|
598
|
+
sortable: primaryCol.sortable,
|
|
599
|
+
getValueFn: primaryCol.getValueFn,
|
|
600
|
+
|
|
601
|
+
// Optional overrides
|
|
602
|
+
...(isLeft ? this.leftColumnSpec : this.rightColumnSpec)
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private findColumnSpec(mapping: ZoneMapping): ColumnSpec {
|
|
607
|
+
return this.availableColumns.find(it => {
|
|
608
|
+
const {field} = it;
|
|
609
|
+
return isString(field) ? field === mapping.field : field.name === mapping.field;
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private parseMappings(
|
|
614
|
+
mappings: Record<Zone, Some<string | ZoneMapping>>
|
|
615
|
+
): Record<Zone, ZoneMapping[]> {
|
|
616
|
+
const ret = {} as Record<Zone, ZoneMapping[]>;
|
|
617
|
+
forOwn(mappings, (rawMapping, zone) => {
|
|
618
|
+
// 1) Standardize mapping into an array of ZoneMappings
|
|
619
|
+
const mapping = [];
|
|
620
|
+
castArray(rawMapping).forEach(it => {
|
|
621
|
+
if (!it) return;
|
|
622
|
+
|
|
623
|
+
const ret = isString(it) ? {field: it} : it,
|
|
624
|
+
col = this.findColumnSpec(ret);
|
|
625
|
+
|
|
626
|
+
throwIf(!col, `Column not found for field ${ret.field}`);
|
|
627
|
+
return mapping.push(ret);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// 2) Ensure mapping respects configured limits
|
|
631
|
+
const limit = this.limits?.[zone];
|
|
632
|
+
if (limit) {
|
|
633
|
+
throwIf(
|
|
634
|
+
isFinite(limit.min) && mapping.length < limit.min,
|
|
635
|
+
`Requires minimum ${limit.min} mappings in zone "${zone}"`
|
|
636
|
+
);
|
|
637
|
+
throwIf(
|
|
638
|
+
isFinite(limit.max) && mapping.length > limit.max,
|
|
639
|
+
`Exceeds maximum ${limit.max} mappings in zone "${zone}"`
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
if (!isEmpty(limit.only)) {
|
|
643
|
+
mapping.forEach(it => {
|
|
644
|
+
throwIf(
|
|
645
|
+
!limit.only.includes(it.field),
|
|
646
|
+
`Field "${it.field}" not allowed in zone "${zone}"`
|
|
647
|
+
);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
ret[zone] = mapping;
|
|
653
|
+
});
|
|
654
|
+
return ret;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private parseMapperModel(mapperModel: ZoneMapperConfig | boolean): ZoneMapperModel {
|
|
658
|
+
if (isPlainObject(mapperModel)) {
|
|
659
|
+
return new ZoneMapperModel({
|
|
660
|
+
...(mapperModel as ZoneMapperConfig),
|
|
661
|
+
zoneGridModel: this
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return mapperModel ? new ZoneMapperModel({zoneGridModel: this}) : null;
|
|
665
|
+
}
|
|
666
|
+
}
|