@xh/hoist 72.0.0 → 72.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -2
- package/admin/jsonsearch/JsonSearch.ts +294 -0
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +175 -0
- package/admin/tabs/general/config/ConfigPanel.ts +26 -4
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +47 -10
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +45 -15
- package/admin/tabs/userData/roles/RoleModel.ts +3 -3
- package/admin/tabs/userData/roles/editor/form/RoleFormModel.ts +3 -3
- package/build/types/admin/jsonsearch/JsonSearch.d.ts +17 -0
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +32 -0
- package/build/types/core/persist/PersistenceProvider.d.ts +4 -0
- package/build/types/data/Store.d.ts +4 -0
- package/build/types/data/StoreRecord.d.ts +2 -0
- package/cmp/filter/FilterChooserModel.ts +6 -2
- package/cmp/grid/impl/InitPersist.ts +9 -3
- package/cmp/grouping/GroupingChooserModel.ts +6 -2
- package/cmp/zoneGrid/impl/InitPersist.ts +9 -3
- package/core/HoistBase.ts +1 -2
- package/core/HoistBaseDecorators.ts +4 -1
- package/core/persist/PersistenceProvider.ts +31 -0
- package/data/Store.ts +13 -3
- package/data/StoreRecord.ts +6 -1
- package/data/cube/row/BaseRow.ts +3 -2
- package/mobile/cmp/navigator/NavigatorModel.ts +7 -0
- package/package.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v72.1.0 - 2025-02-13
|
|
4
|
+
|
|
5
|
+
### 🎁 New Features
|
|
6
|
+
|
|
7
|
+
* Introduced a new "JSON Search" feature to the Hoist Admin Console, accessible from the Config,
|
|
8
|
+
User Preference, and JSON Blob tabs. Supports searching JSON values stored within these objects
|
|
9
|
+
to filter and match data using JSON Path expressions.
|
|
10
|
+
* ⚠️Requires `hoist-core >= 28.1` with new APIs for this (optional) feature to function.
|
|
11
|
+
* Added new getters `StoreRecord.isDirty`, `Store.dirtyRecords`, and `Store.isDirty` to provide a
|
|
12
|
+
more consistent API in the data package. The pre-existing `isModified` getters are retained as
|
|
13
|
+
aliases, with the same semantics.
|
|
14
|
+
|
|
15
|
+
### 🐞 Bug Fixes
|
|
16
|
+
|
|
17
|
+
* Tuned mobile swipe handling to prevent horizontal swipes on a scrolling grid view from triggering
|
|
18
|
+
the Navigator's back gesture.
|
|
19
|
+
* Prevented the Admin Console Roles grid from losing its expand/collapse/scroll state on refresh.
|
|
20
|
+
* Fixed bug when merging `PersistOptions` with conflicting implicit provider types.
|
|
21
|
+
* Fixed bug where explicit `persistGrouping` options were not being respected by `GridModel`.
|
|
22
|
+
|
|
3
23
|
## v72.0.0 - 2025-01-27
|
|
4
24
|
|
|
5
|
-
### 💥 Breaking Changes
|
|
25
|
+
### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - minor changes to mobile nav)
|
|
6
26
|
|
|
7
27
|
* Mobile `Navigator` no longer supports `animation` prop, and `NavigatorModel` no longer supports
|
|
8
28
|
`swipeToGoBack`. Both of these properties are now managed internally by the `Navigator` component.
|
|
@@ -15,7 +35,8 @@
|
|
|
15
35
|
### 🐞 Bug Fixes
|
|
16
36
|
|
|
17
37
|
* Fixed `ViewManagerModel` unique name validation.
|
|
18
|
-
* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply
|
|
38
|
+
* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply
|
|
39
|
+
clearing it.
|
|
19
40
|
* Improved suboptimal column state synchronization between `GridModel` and AG Grid.
|
|
20
41
|
|
|
21
42
|
### ⚙️ Technical
|
|
@@ -0,0 +1,294 @@
|
|
|
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 © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {errorMessage} from '@xh/hoist/cmp/error';
|
|
9
|
+
import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
10
|
+
import {a, box, filler, fragment, hframe, label, li, p, span, ul, vbox} from '@xh/hoist/cmp/layout';
|
|
11
|
+
import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core';
|
|
12
|
+
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
13
|
+
import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard';
|
|
14
|
+
import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input';
|
|
15
|
+
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
16
|
+
import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
17
|
+
import {Icon} from '@xh/hoist/icon';
|
|
18
|
+
import {dialog, popover} from '@xh/hoist/kit/blueprint';
|
|
19
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
20
|
+
import {startCase} from 'lodash';
|
|
21
|
+
import {JsonSearchImplModel} from './impl/JsonSearchImplModel';
|
|
22
|
+
|
|
23
|
+
export interface JsonSearchButtonProps extends HoistProps {
|
|
24
|
+
/** Descriptive label for the type of records being searched - will be auto-pluralized. */
|
|
25
|
+
subjectName: string;
|
|
26
|
+
|
|
27
|
+
/** Endpoint to search and return matches - Hoist `JsonSearchController` action expected. */
|
|
28
|
+
docSearchUrl: string;
|
|
29
|
+
|
|
30
|
+
/** Config for GridModel used to display search results. */
|
|
31
|
+
gridModelConfig: GridConfig;
|
|
32
|
+
|
|
33
|
+
/** Field names on returned results to enable for grouping in the search results grid. */
|
|
34
|
+
groupByOptions: Array<SelectOption | any>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Main entry point component for the JSON search feature. Supported out-of-the-box for a limited
|
|
39
|
+
* set of Hoist artifacts that hold JSON values: JSONBlob, Configs, and User Preferences.
|
|
40
|
+
*/
|
|
41
|
+
export const jsonSearchButton = hoistCmp.factory<JsonSearchButtonProps>({
|
|
42
|
+
displayName: 'JsonSearchButton',
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
const impl = useLocalModel(JsonSearchImplModel);
|
|
46
|
+
|
|
47
|
+
return fragment(
|
|
48
|
+
button({
|
|
49
|
+
icon: Icon.json(),
|
|
50
|
+
text: 'JSON Search',
|
|
51
|
+
onClick: () => impl.toggleSearchIsOpen()
|
|
52
|
+
}),
|
|
53
|
+
jsonSearchDialog({
|
|
54
|
+
omit: !impl.isOpen,
|
|
55
|
+
model: impl
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const jsonSearchDialog = hoistCmp.factory<JsonSearchImplModel>({
|
|
62
|
+
displayName: 'JsonSearchDialog',
|
|
63
|
+
|
|
64
|
+
render({model}) {
|
|
65
|
+
const {error, subjectName} = model;
|
|
66
|
+
|
|
67
|
+
return dialog({
|
|
68
|
+
title: `JSON Search: ${subjectName}`,
|
|
69
|
+
style: {
|
|
70
|
+
width: '90vw',
|
|
71
|
+
height: '90vh'
|
|
72
|
+
},
|
|
73
|
+
icon: Icon.json(),
|
|
74
|
+
isOpen: true,
|
|
75
|
+
className: 'xh-admin-diff-detail',
|
|
76
|
+
onClose: () => model.toggleSearchIsOpen(),
|
|
77
|
+
item: panel({
|
|
78
|
+
tbar: searchTbar(),
|
|
79
|
+
item: panel({
|
|
80
|
+
mask: model.docLoadTask,
|
|
81
|
+
items: [
|
|
82
|
+
errorMessage({
|
|
83
|
+
error,
|
|
84
|
+
title: error?.name ? startCase(error.name) : undefined
|
|
85
|
+
}),
|
|
86
|
+
hframe({
|
|
87
|
+
omit: error,
|
|
88
|
+
items: [
|
|
89
|
+
panel({
|
|
90
|
+
item: grid({model: model.gridModel}),
|
|
91
|
+
modelConfig: {
|
|
92
|
+
side: 'left',
|
|
93
|
+
defaultSize: '30%',
|
|
94
|
+
collapsible: true,
|
|
95
|
+
defaultCollapsed: false,
|
|
96
|
+
resizable: true
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
panel({
|
|
100
|
+
mask: model.nodeLoadTask,
|
|
101
|
+
tbar: readerTbar(),
|
|
102
|
+
item: jsonInput({
|
|
103
|
+
model,
|
|
104
|
+
bind: 'readerContent',
|
|
105
|
+
flex: 1,
|
|
106
|
+
width: '100%',
|
|
107
|
+
readonly: true,
|
|
108
|
+
showCopyButton: true
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const searchTbar = hoistCmp.factory<JsonSearchImplModel>({
|
|
121
|
+
render({model}) {
|
|
122
|
+
return toolbar(
|
|
123
|
+
pathField({model}),
|
|
124
|
+
button({
|
|
125
|
+
text: `Search ${model.subjectName}`,
|
|
126
|
+
intent: 'success',
|
|
127
|
+
outlined: true,
|
|
128
|
+
disabled: !model.path,
|
|
129
|
+
onClick: () => model.loadMatchingDocsAsync()
|
|
130
|
+
}),
|
|
131
|
+
'-',
|
|
132
|
+
helpButton({model}),
|
|
133
|
+
'-',
|
|
134
|
+
span('Group by:'),
|
|
135
|
+
select({
|
|
136
|
+
bind: 'groupBy',
|
|
137
|
+
options: model.groupByOptions,
|
|
138
|
+
width: 160,
|
|
139
|
+
enableFilter: false
|
|
140
|
+
}),
|
|
141
|
+
'-',
|
|
142
|
+
gridCountLabel({
|
|
143
|
+
gridModel: model.gridModel,
|
|
144
|
+
unit: 'match'
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const pathField = hoistCmp.factory<JsonSearchImplModel>({
|
|
151
|
+
render({model}) {
|
|
152
|
+
return textInput({
|
|
153
|
+
bind: 'path',
|
|
154
|
+
autoFocus: true,
|
|
155
|
+
commitOnChange: true,
|
|
156
|
+
leftIcon: Icon.search(),
|
|
157
|
+
enableClear: true,
|
|
158
|
+
placeholder: 'Provide a JSON Path expression to evaluate',
|
|
159
|
+
width: null,
|
|
160
|
+
flex: 1,
|
|
161
|
+
onKeyDown: e => {
|
|
162
|
+
if (e.key === 'Enter') model.loadMatchingDocsAsync();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const helpButton = hoistCmp.factory<JsonSearchImplModel>({
|
|
169
|
+
render({model}) {
|
|
170
|
+
return popover({
|
|
171
|
+
item: button({
|
|
172
|
+
icon: Icon.questionCircle(),
|
|
173
|
+
outlined: true
|
|
174
|
+
}),
|
|
175
|
+
content: vbox({
|
|
176
|
+
className: 'xh-pad',
|
|
177
|
+
items: [
|
|
178
|
+
p(
|
|
179
|
+
`JSON Path expressions allow you to recursively query JSON documents, matching nodes based on their path, properties, and values.`
|
|
180
|
+
),
|
|
181
|
+
p(
|
|
182
|
+
`Enter a path and press [Enter] to search for matches within the JSON content of ${model.subjectName}.`
|
|
183
|
+
),
|
|
184
|
+
ul({
|
|
185
|
+
items: queryExamples.map(({query, explanation}) =>
|
|
186
|
+
li({
|
|
187
|
+
key: query,
|
|
188
|
+
items: [
|
|
189
|
+
span({
|
|
190
|
+
className:
|
|
191
|
+
'xh-border xh-pad-half xh-bg-alt xh-font-family-mono',
|
|
192
|
+
item: query
|
|
193
|
+
}),
|
|
194
|
+
' ',
|
|
195
|
+
clipboardButton({
|
|
196
|
+
text: null,
|
|
197
|
+
icon: Icon.copy(),
|
|
198
|
+
getCopyText: () => query,
|
|
199
|
+
successMessage: 'Query copied to clipboard.'
|
|
200
|
+
}),
|
|
201
|
+
' ',
|
|
202
|
+
explanation
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
),
|
|
206
|
+
style: {marginTop: 0}
|
|
207
|
+
}),
|
|
208
|
+
a({
|
|
209
|
+
href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators',
|
|
210
|
+
target: '_blank',
|
|
211
|
+
item: 'Path Syntax Docs & More Examples'
|
|
212
|
+
})
|
|
213
|
+
]
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const readerTbar = hoistCmp.factory<JsonSearchImplModel>(({model}) => {
|
|
220
|
+
return toolbar({
|
|
221
|
+
items: [
|
|
222
|
+
buttonGroupInput({
|
|
223
|
+
model,
|
|
224
|
+
bind: 'readerContentType',
|
|
225
|
+
minimal: true,
|
|
226
|
+
outlined: true,
|
|
227
|
+
disabled: !model.selectedRecord,
|
|
228
|
+
items: [
|
|
229
|
+
button({
|
|
230
|
+
text: 'Document',
|
|
231
|
+
value: 'document'
|
|
232
|
+
}),
|
|
233
|
+
button({
|
|
234
|
+
text: 'Matches',
|
|
235
|
+
value: 'matches'
|
|
236
|
+
})
|
|
237
|
+
]
|
|
238
|
+
}),
|
|
239
|
+
fragment({
|
|
240
|
+
omit: model.readerContentType !== 'matches' || !model.selectedRecord,
|
|
241
|
+
items: [
|
|
242
|
+
toolbarSep(),
|
|
243
|
+
label('View path as'),
|
|
244
|
+
buttonGroupInput({
|
|
245
|
+
model,
|
|
246
|
+
bind: 'pathFormat',
|
|
247
|
+
minimal: true,
|
|
248
|
+
outlined: true,
|
|
249
|
+
items: [
|
|
250
|
+
button({
|
|
251
|
+
text: 'XPath',
|
|
252
|
+
value: 'XPath'
|
|
253
|
+
}),
|
|
254
|
+
button({
|
|
255
|
+
text: 'JSONPath',
|
|
256
|
+
value: 'JSONPath'
|
|
257
|
+
})
|
|
258
|
+
]
|
|
259
|
+
})
|
|
260
|
+
]
|
|
261
|
+
}),
|
|
262
|
+
filler(),
|
|
263
|
+
box({
|
|
264
|
+
omit: !model.matchingNodeCount,
|
|
265
|
+
item: `${pluralize('match', model.matchingNodeCount, true)} within this document`
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const queryExamples = [
|
|
272
|
+
{
|
|
273
|
+
query: '$.displayMode',
|
|
274
|
+
explanation: 'Return documents with a top-level property "displayMode"'
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
query: "$..[?(@.colId == 'trader')]",
|
|
278
|
+
explanation:
|
|
279
|
+
'Find all nodes (anywhere in the document) with a property "colId" equal to "trader"'
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
query: '$..[?(@.colId && @.width)]',
|
|
283
|
+
explanation: 'Find all nodes with a property "colId" and a property "width"'
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
query: '$..[?(@.colId && @.hidden != true)]',
|
|
287
|
+
explanation:
|
|
288
|
+
'Find all nodes with a property "colId" and a property "hidden" not equal to true'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
query: '$..grid[?(@.version == 1)]',
|
|
292
|
+
explanation: 'Find all nodes with a key of "grid" and a property "version" equal to 1'
|
|
293
|
+
}
|
|
294
|
+
];
|
|
@@ -0,0 +1,175 @@
|
|
|
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 © 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
|
|
9
|
+
import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
|
|
10
|
+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
|
|
11
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
12
|
+
import {camelCase, isEmpty, zipWith} from 'lodash';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class JsonSearchImplModel extends HoistModel {
|
|
18
|
+
override xhImpl = true;
|
|
19
|
+
|
|
20
|
+
private matchingNodesUrl = 'jsonSearch/getMatchingNodes';
|
|
21
|
+
|
|
22
|
+
@managed gridModel: GridModel;
|
|
23
|
+
@managed docLoadTask: TaskObserver = TaskObserver.trackLast();
|
|
24
|
+
@managed nodeLoadTask: TaskObserver = TaskObserver.trackLast();
|
|
25
|
+
|
|
26
|
+
@observable groupBy: string = null;
|
|
27
|
+
@observable isOpen: boolean = false;
|
|
28
|
+
|
|
29
|
+
@bindable.ref error = null;
|
|
30
|
+
@bindable path: string = '';
|
|
31
|
+
@bindable readerContentType: 'document' | 'matches' = 'matches';
|
|
32
|
+
@bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
|
|
33
|
+
@bindable readerContent: string = '';
|
|
34
|
+
@bindable matchingNodeCount: number = 0;
|
|
35
|
+
|
|
36
|
+
get subjectName(): string {
|
|
37
|
+
return pluralize(this.componentProps.subjectName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get docSearchUrl(): string {
|
|
41
|
+
return this.componentProps.docSearchUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get gridModelConfig(): GridConfig {
|
|
45
|
+
return this.componentProps.gridModelConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get selectedRecord() {
|
|
49
|
+
return this.gridModel.selectedRecord;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get groupByOptions() {
|
|
53
|
+
const cols = this.gridModel.getLeafColumns();
|
|
54
|
+
return [
|
|
55
|
+
...this.componentProps.groupByOptions.map(it => ({
|
|
56
|
+
value: it,
|
|
57
|
+
label: cols.find(col => col.colId === it)?.displayName ?? it
|
|
58
|
+
})),
|
|
59
|
+
{value: null, label: 'None'}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@action
|
|
64
|
+
toggleSearchIsOpen() {
|
|
65
|
+
this.isOpen = !this.isOpen;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
makeObservable(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override onLinked() {
|
|
74
|
+
this.gridModel = new GridModel({
|
|
75
|
+
...this.gridModelConfig,
|
|
76
|
+
emptyText: 'No matches found...',
|
|
77
|
+
selModel: 'single'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.markPersist('path', {localStorageKey: `xhJsonSearch${camelCase(this.subjectName)}`});
|
|
81
|
+
|
|
82
|
+
this.addReaction(
|
|
83
|
+
{
|
|
84
|
+
track: () => this.path,
|
|
85
|
+
run: path => {
|
|
86
|
+
if (isEmpty(path)) {
|
|
87
|
+
this.error = null;
|
|
88
|
+
this.gridModel.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
track: () => [this.selectedRecord, this.readerContentType, this.pathFormat],
|
|
94
|
+
run: () => this.loadReaderContentAsync(),
|
|
95
|
+
debounce: 300
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// We might have a persisted path - go ahead and load if so.
|
|
100
|
+
this.loadMatchingDocsAsync();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async loadMatchingDocsAsync() {
|
|
104
|
+
const {path, gridModel, docLoadTask} = this;
|
|
105
|
+
|
|
106
|
+
if (isEmpty(path)) {
|
|
107
|
+
this.error = null;
|
|
108
|
+
gridModel.clear();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const data = await XH.fetchJson({
|
|
114
|
+
url: this.docSearchUrl,
|
|
115
|
+
params: {path}
|
|
116
|
+
}).linkTo(docLoadTask);
|
|
117
|
+
|
|
118
|
+
this.error = null;
|
|
119
|
+
gridModel.loadData(data);
|
|
120
|
+
gridModel.preSelectFirstAsync();
|
|
121
|
+
} catch (e) {
|
|
122
|
+
gridModel.clear();
|
|
123
|
+
this.error = e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async loadReaderContentAsync() {
|
|
128
|
+
if (!this.selectedRecord) {
|
|
129
|
+
this.matchingNodeCount = 0;
|
|
130
|
+
this.readerContent = '';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const {json} = this.selectedRecord.data;
|
|
135
|
+
|
|
136
|
+
if (this.readerContentType === 'document') {
|
|
137
|
+
this.readerContent = JSON.stringify(JSON.parse(json), null, 2);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let nodes = await XH.fetchJson({
|
|
142
|
+
url: this.matchingNodesUrl,
|
|
143
|
+
params: {
|
|
144
|
+
path: this.path,
|
|
145
|
+
json
|
|
146
|
+
}
|
|
147
|
+
}).linkTo(this.nodeLoadTask);
|
|
148
|
+
|
|
149
|
+
this.matchingNodeCount = nodes.paths.length;
|
|
150
|
+
nodes = zipWith(nodes.paths, nodes.values, (path: string, value) => {
|
|
151
|
+
return {
|
|
152
|
+
path: this.pathFormat === 'XPath' ? this.convertToXPath(path) : path,
|
|
153
|
+
value
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
this.readerContent = JSON.stringify(nodes, null, 2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private convertToXPath(JSONPath: string): string {
|
|
160
|
+
return JSONPath.replaceAll(/^\$\['?/g, '/')
|
|
161
|
+
.replaceAll(/^\$/g, '')
|
|
162
|
+
.replaceAll(/'?]\['?/g, '/')
|
|
163
|
+
.replaceAll(/'?]$/g, '');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@action
|
|
167
|
+
private setGroupBy(groupBy: string) {
|
|
168
|
+
this.groupBy = groupBy;
|
|
169
|
+
|
|
170
|
+
// Always select first when regrouping.
|
|
171
|
+
const groupByArr = groupBy ? groupBy.split(',') : [];
|
|
172
|
+
this.gridModel.setGroupBy(groupByArr);
|
|
173
|
+
this.gridModel.preSelectFirstAsync();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import * as AdminCol from '@xh/hoist/admin/columns';
|
|
8
|
+
import * as Col from '@xh/hoist/admin/columns/Rest';
|
|
9
|
+
import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch';
|
|
7
10
|
import {fragment} from '@xh/hoist/cmp/layout';
|
|
8
11
|
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
9
12
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
10
13
|
import {restGrid} from '@xh/hoist/desktop/cmp/rest';
|
|
14
|
+
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
11
15
|
import {Icon} from '@xh/hoist/icon';
|
|
12
16
|
import {differ} from '../../../differ/Differ';
|
|
13
17
|
import {regroupDialog} from '../../../regroup/RegroupDialog';
|
|
@@ -20,13 +24,31 @@ export const configPanel = hoistCmp.factory({
|
|
|
20
24
|
return fragment(
|
|
21
25
|
restGrid({
|
|
22
26
|
testId: 'config',
|
|
23
|
-
extraToolbarItems: () =>
|
|
24
|
-
|
|
27
|
+
extraToolbarItems: () => [
|
|
28
|
+
button({
|
|
25
29
|
icon: Icon.diff(),
|
|
26
30
|
text: 'Compare w/ Remote',
|
|
27
31
|
onClick: () => model.openDiffer()
|
|
28
|
-
})
|
|
29
|
-
|
|
32
|
+
}),
|
|
33
|
+
toolbarSep(),
|
|
34
|
+
jsonSearchButton({
|
|
35
|
+
subjectName: 'Config',
|
|
36
|
+
docSearchUrl: 'jsonSearch/searchConfigs',
|
|
37
|
+
gridModelConfig: {
|
|
38
|
+
sortBy: ['groupName', 'name'],
|
|
39
|
+
columns: [
|
|
40
|
+
{...AdminCol.groupName},
|
|
41
|
+
{...AdminCol.name},
|
|
42
|
+
{
|
|
43
|
+
field: {name: 'json', type: 'string'},
|
|
44
|
+
hidden: true
|
|
45
|
+
},
|
|
46
|
+
{...Col.lastUpdated}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
groupByOptions: ['groupName']
|
|
50
|
+
})
|
|
51
|
+
]
|
|
30
52
|
}),
|
|
31
53
|
differ({omit: !model.differModel}),
|
|
32
54
|
regroupDialog()
|
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
* Copyright © 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import * as Col from '@xh/hoist/admin/columns/Rest';
|
|
9
|
+
import * as AdminCol from '@xh/hoist/admin/columns';
|
|
10
|
+
import {hframe} from '@xh/hoist/cmp/layout';
|
|
9
11
|
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
10
12
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
13
|
+
import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch';
|
|
14
|
+
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
11
15
|
import {restGrid} from '@xh/hoist/desktop/cmp/rest';
|
|
16
|
+
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
12
17
|
import {Icon} from '@xh/hoist/icon';
|
|
13
18
|
import {differ} from '../../../differ/Differ';
|
|
14
19
|
import {JsonBlobModel} from './JsonBlobModel';
|
|
@@ -17,15 +22,47 @@ export const jsonBlobPanel = hoistCmp.factory({
|
|
|
17
22
|
model: creates(JsonBlobModel),
|
|
18
23
|
|
|
19
24
|
render({model}) {
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
return hframe(
|
|
26
|
+
panel({
|
|
27
|
+
item: restGrid({
|
|
28
|
+
extraToolbarItems: () => [
|
|
29
|
+
button({
|
|
30
|
+
icon: Icon.diff(),
|
|
31
|
+
text: 'Compare w/ Remote',
|
|
32
|
+
onClick: () => model.openDiffer()
|
|
33
|
+
}),
|
|
34
|
+
toolbarSep(),
|
|
35
|
+
jsonSearchButton({
|
|
36
|
+
subjectName: 'JSON Blob',
|
|
37
|
+
docSearchUrl: 'jsonSearch/searchBlobs',
|
|
38
|
+
gridModelConfig: {
|
|
39
|
+
sortBy: ['type', 'name', 'owner'],
|
|
40
|
+
columns: [
|
|
41
|
+
{
|
|
42
|
+
field: {name: 'token', type: 'string'},
|
|
43
|
+
hidden: true,
|
|
44
|
+
width: 100
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
field: {name: 'type', type: 'string'},
|
|
48
|
+
width: 200
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
field: {name: 'owner', type: 'string'},
|
|
52
|
+
width: 200
|
|
53
|
+
},
|
|
54
|
+
{...AdminCol.name},
|
|
55
|
+
{
|
|
56
|
+
field: {name: 'json', type: 'string'},
|
|
57
|
+
hidden: true
|
|
58
|
+
},
|
|
59
|
+
{...Col.lastUpdated}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
groupByOptions: ['owner', 'type', 'name']
|
|
63
|
+
})
|
|
64
|
+
]
|
|
65
|
+
})
|
|
29
66
|
}),
|
|
30
67
|
differ({omit: !model.differModel})
|
|
31
68
|
);
|