@xh/hoist 73.0.0-SNAPSHOT.1738098319236 → 73.0.0-SNAPSHOT.1738169109530

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.
@@ -0,0 +1,300 @@
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 {startCase} from 'lodash';
9
+ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
10
+ import {errorMessage} from '@xh/hoist/cmp/error';
11
+ import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid';
12
+ import {
13
+ a,
14
+ box,
15
+ filler,
16
+ fragment,
17
+ h4,
18
+ hframe,
19
+ label,
20
+ li,
21
+ span,
22
+ ul,
23
+ vbox
24
+ } from '@xh/hoist/cmp/layout';
25
+ import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core';
26
+ import {button} from '@xh/hoist/desktop/cmp/button';
27
+ import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input';
28
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
29
+ import {Icon} from '@xh/hoist/icon';
30
+ import {dialog, popover} from '@xh/hoist/kit/blueprint';
31
+ import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard';
32
+ import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel';
33
+
34
+ export interface JsonSearchButtonProps extends HoistProps {
35
+ /** Name of the type of Json Documents being searched. This appears in the dialog title. */
36
+ subjectName: string;
37
+
38
+ /** Url to endpoint for searching for matching JSON documents */
39
+ docSearchUrl: string;
40
+
41
+ /**
42
+ * Config for GridModel used to display search results.
43
+ */
44
+ gridModelConfig: GridConfig;
45
+
46
+ /**
47
+ * Names of fields that can be used to group by.
48
+ */
49
+ groupByOptions: Array<SelectOption | any>;
50
+ }
51
+
52
+ export const [JsonSearchButton, jsonSearchButton] = hoistCmp.withFactory<JsonSearchButtonProps>({
53
+ displayName: 'JsonSearchPanel',
54
+
55
+ render() {
56
+ const impl = useLocalModel(JsonSearchPanelImplModel);
57
+
58
+ return fragment(
59
+ button({
60
+ icon: Icon.json(),
61
+ text: 'JSON Search',
62
+ onClick: () => impl.toggleSearchIsOpen()
63
+ }),
64
+ jsonSearchDialog({
65
+ omit: !impl.isOpen,
66
+ model: impl
67
+ })
68
+ );
69
+ }
70
+ });
71
+
72
+ const jsonSearchDialog = hoistCmp.factory<JsonSearchPanelImplModel>({
73
+ displayName: 'JsonSearchPanel',
74
+
75
+ render({model}) {
76
+ const {error, subjectName} = model;
77
+
78
+ return dialog({
79
+ title: `${subjectName} Json Search`,
80
+ style: {
81
+ width: '90vw',
82
+ height: '90vh'
83
+ },
84
+ icon: Icon.json(),
85
+ isOpen: true,
86
+ className: 'xh-admin-diff-detail',
87
+ onClose: () => model.toggleSearchIsOpen(),
88
+ item: panel({
89
+ tbar: searchTbar(),
90
+ item: panel({
91
+ mask: model.docLoadTask,
92
+ items: [
93
+ errorMessage({
94
+ error,
95
+ title: error?.name ? startCase(error.name) : undefined
96
+ }),
97
+ hframe({
98
+ omit: error,
99
+ items: [
100
+ panel({
101
+ item: grid({model: model.gridModel}),
102
+ modelConfig: {
103
+ side: 'left',
104
+ defaultSize: '30%',
105
+ collapsible: true,
106
+ defaultCollapsed: false,
107
+ resizable: true
108
+ }
109
+ }),
110
+ panel({
111
+ mask: model.nodeLoadTask,
112
+ tbar: readerTbar(),
113
+ bbar: nodeBbar({
114
+ omit: model.readerContentType !== 'matches',
115
+ model
116
+ }),
117
+ item: jsonInput({
118
+ model,
119
+ bind: 'readerContent',
120
+ flex: 1,
121
+ width: '100%',
122
+ readonly: true,
123
+ showCopyButton: true
124
+ })
125
+ })
126
+ ]
127
+ })
128
+ ]
129
+ })
130
+ })
131
+ });
132
+ }
133
+ });
134
+
135
+ const searchTbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
136
+ return toolbar(
137
+ pathField({model}),
138
+ helpButton(),
139
+ toolbarSep(),
140
+ span('Group by:'),
141
+ select({
142
+ bind: 'groupBy',
143
+ options: model.groupByOptions,
144
+ width: 160,
145
+ enableFilter: false
146
+ }),
147
+ toolbarSep(),
148
+ gridCountLabel({
149
+ gridModel: model.gridModel,
150
+ unit: 'document'
151
+ })
152
+ );
153
+ });
154
+
155
+ const pathField = hoistCmp.factory<JsonSearchPanelImplModel>({
156
+ render({model}) {
157
+ return textInput({
158
+ bind: 'path',
159
+ autoFocus: true,
160
+ commitOnChange: true,
161
+ leftIcon: Icon.search(),
162
+ enableClear: true,
163
+ placeholder:
164
+ "JSON Path - e.g. $..[?(@.colId == 'trader')] - type a path and hit ENTER to search",
165
+ width: null,
166
+ flex: 1,
167
+ onKeyDown: e => {
168
+ if (e.key === 'Enter') model.loadJsonDocsAsync();
169
+ }
170
+ });
171
+ }
172
+ });
173
+
174
+ const helpButton = hoistCmp.factory({
175
+ model: false,
176
+ render() {
177
+ return popover({
178
+ item: button({
179
+ icon: Icon.questionCircle(),
180
+ outlined: true
181
+ }),
182
+ content: vbox({
183
+ style: {
184
+ padding: '0px 20px 10px 20px'
185
+ },
186
+ items: [
187
+ h4('Sample Queries'),
188
+ ul({
189
+ items: queryExamples.map(({query, explanation}) =>
190
+ li({
191
+ key: query,
192
+ items: [
193
+ span({
194
+ className:
195
+ 'xh-border xh-pad-half xh-bg-alt xh-font-family-mono',
196
+ item: query
197
+ }),
198
+ ' ',
199
+ clipboardButton({
200
+ text: null,
201
+ icon: Icon.copy(),
202
+ getCopyText: () => query,
203
+ successMessage: 'Query copied to clipboard.'
204
+ }),
205
+ ' ',
206
+ explanation
207
+ ]
208
+ })
209
+ )
210
+ }),
211
+ a({
212
+ href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators',
213
+ target: '_blank',
214
+ item: 'Path Syntax Docs & More Examples'
215
+ })
216
+ ]
217
+ })
218
+ });
219
+ }
220
+ });
221
+
222
+ const readerTbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
223
+ return toolbar({
224
+ items: [
225
+ buttonGroupInput({
226
+ model,
227
+ bind: 'readerContentType',
228
+ minimal: true,
229
+ outlined: true,
230
+ disabled: !model.selectedRecord,
231
+ items: [
232
+ button({
233
+ text: 'Document',
234
+ value: 'document'
235
+ }),
236
+ button({
237
+ text: 'Matches',
238
+ value: 'matches'
239
+ })
240
+ ]
241
+ }),
242
+ filler(),
243
+ box({
244
+ omit: !model.matchingNodeCount,
245
+ item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}`
246
+ })
247
+ ]
248
+ });
249
+ });
250
+
251
+ const nodeBbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
252
+ return toolbar(
253
+ label('Path Format:'),
254
+ buttonGroupInput({
255
+ model,
256
+ bind: 'pathFormat',
257
+ minimal: true,
258
+ outlined: true,
259
+ disabled: !model.selectedRecord,
260
+ items: [
261
+ button({
262
+ text: 'XPath',
263
+ value: 'XPath'
264
+ }),
265
+ button({
266
+ text: 'JSONPath',
267
+ value: 'JSONPath'
268
+ })
269
+ ]
270
+ })
271
+ );
272
+ });
273
+
274
+ const queryExamples = [
275
+ {
276
+ query: '$',
277
+ explanation: 'Return the root object'
278
+ },
279
+ {
280
+ query: '$..*',
281
+ explanation: 'Return all nodes, recursively'
282
+ },
283
+ {
284
+ query: '$..[?(@.colId && @.width && @.hidden != true)]',
285
+ explanation:
286
+ 'Find all nodes with a property "colId" and a property "width" and a property "hidden" not equal to true'
287
+ },
288
+ {
289
+ query: '$..[?(@.colId && @.width)]',
290
+ explanation: 'Find all nodes with a property "colId" and a property "width"'
291
+ },
292
+ {
293
+ query: "$..[?(@.colId == 'trader')]",
294
+ explanation: 'Find all nodes with a property "colId" equal to "trader"'
295
+ },
296
+ {
297
+ query: '$..grid[?(@.version == 1)]',
298
+ explanation: 'Find all grid nodes with a property "version" equal to 1'
299
+ }
300
+ ];
@@ -0,0 +1,161 @@
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 {GridModel} from '@xh/hoist/cmp/grid';
9
+ import {GroupingChooserModel} from '@xh/hoist/cmp/grouping';
10
+ import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
11
+ import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
12
+ import {isEmpty, zipWith} from 'lodash';
13
+
14
+ /**
15
+ * @internal
16
+ */
17
+ export class JsonSearchPanelImplModel extends HoistModel {
18
+ override xhImpl = true;
19
+
20
+ private matchingNodesUrl = 'jsonSearch/getMatchingNodes';
21
+
22
+ @managed gridModel: GridModel;
23
+ @managed groupingChooserModel: GroupingChooserModel;
24
+ @managed docLoadTask: TaskObserver = TaskObserver.trackLast();
25
+ @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast();
26
+
27
+ @observable groupBy: string = null;
28
+ @observable isOpen: boolean = false;
29
+
30
+ @bindable.ref error = null;
31
+ @bindable path: string = '';
32
+ @bindable readerContentType: 'document' | 'matches' = 'matches';
33
+ @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
34
+ @bindable readerContent: string = '';
35
+ @bindable matchingNodeCount: number = 0;
36
+
37
+ get subjectName(): string {
38
+ return this.componentProps.subjectName;
39
+ }
40
+
41
+ get docSearchUrl(): string {
42
+ return this.componentProps.docSearchUrl;
43
+ }
44
+
45
+ get selectedRecord() {
46
+ return this.gridModel.selectedRecord;
47
+ }
48
+
49
+ get gridModelConfig() {
50
+ return this.componentProps.gridModelConfig;
51
+ }
52
+
53
+ get groupByOptions() {
54
+ return [...this.componentProps.groupByOptions, {value: null, label: 'None'}];
55
+ }
56
+
57
+ @action
58
+ toggleSearchIsOpen() {
59
+ this.isOpen = !this.isOpen;
60
+ }
61
+
62
+ constructor() {
63
+ super();
64
+ makeObservable(this);
65
+ }
66
+
67
+ override onLinked() {
68
+ this.gridModel = new GridModel({
69
+ ...this.gridModelConfig,
70
+ selModel: 'single'
71
+ });
72
+
73
+ this.addReaction(
74
+ {
75
+ track: () => this.path,
76
+ run: path => {
77
+ if (isEmpty(path)) {
78
+ this.error = null;
79
+ this.gridModel.clear();
80
+ }
81
+ }
82
+ },
83
+ {
84
+ track: () => [this.selectedRecord, this.readerContentType, this.pathFormat],
85
+ run: () => this.loadreaderContentAsync(),
86
+ debounce: 300
87
+ }
88
+ );
89
+ }
90
+
91
+ async loadJsonDocsAsync() {
92
+ if (isEmpty(this.path)) {
93
+ this.error = null;
94
+ this.gridModel.clear();
95
+ return;
96
+ }
97
+
98
+ try {
99
+ const data = await XH.fetchJson({
100
+ url: this.docSearchUrl,
101
+ params: {path: this.path}
102
+ }).linkTo(this.docLoadTask);
103
+
104
+ this.error = null;
105
+ this.gridModel.loadData(data);
106
+ this.gridModel.selectFirstAsync();
107
+ } catch (e) {
108
+ this.gridModel.clear();
109
+ this.error = e;
110
+ }
111
+ }
112
+
113
+ private async loadreaderContentAsync() {
114
+ if (!this.selectedRecord) {
115
+ this.matchingNodeCount = 0;
116
+ this.readerContent = '';
117
+ return;
118
+ }
119
+
120
+ const {json} = this.selectedRecord.data;
121
+
122
+ if (this.readerContentType === 'document') {
123
+ this.readerContent = JSON.stringify(JSON.parse(json), null, 2);
124
+ return;
125
+ }
126
+
127
+ let nodes = await XH.fetchJson({
128
+ url: this.matchingNodesUrl,
129
+ params: {
130
+ path: this.path,
131
+ json
132
+ }
133
+ }).linkTo(this.nodeLoadTask);
134
+
135
+ this.matchingNodeCount = nodes.paths.length;
136
+ nodes = zipWith(nodes.paths, nodes.values, (path: string, value) => {
137
+ return {
138
+ path: this.pathFormat === 'XPath' ? this.convertToXPath(path) : path,
139
+ value
140
+ };
141
+ });
142
+ this.readerContent = JSON.stringify(nodes, null, 2);
143
+ }
144
+
145
+ private convertToXPath(JSONPath: string): string {
146
+ return JSONPath.replaceAll(/^\$\['?/g, '/')
147
+ .replaceAll(/^\$/g, '')
148
+ .replaceAll(/'?]\['?/g, '/')
149
+ .replaceAll(/'?]$/g, '');
150
+ }
151
+
152
+ @action
153
+ private setGroupBy(groupBy: string) {
154
+ this.groupBy = groupBy;
155
+
156
+ // Always select first when regrouping.
157
+ const groupByArr = groupBy ? groupBy.split(',') : [];
158
+ this.gridModel.setGroupBy(groupByArr);
159
+ this.gridModel.preSelectFirstAsync();
160
+ }
161
+ }
@@ -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/JsonSearchPanel';
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,35 @@ export const configPanel = hoistCmp.factory({
20
24
  return fragment(
21
25
  restGrid({
22
26
  testId: 'config',
23
- extraToolbarItems: () => {
24
- return button({
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', 'owner'],
39
+ columns: [
40
+ {
41
+ field: {name: 'owner', type: 'string'},
42
+ width: 200
43
+ },
44
+ {...AdminCol.groupName},
45
+ {...AdminCol.name},
46
+ {
47
+ field: {name: 'json', type: 'string'},
48
+ hidden: true
49
+ },
50
+ {...Col.lastUpdated}
51
+ ]
52
+ },
53
+ groupByOptions: ['owner', 'groupName', 'name']
54
+ })
55
+ ]
30
56
  }),
31
57
  differ({omit: !model.differModel}),
32
58
  regroupDialog()
@@ -5,10 +5,15 @@
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
7
 
8
- import {fragment} from '@xh/hoist/cmp/layout';
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/JsonSearchPanel';
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 fragment(
21
- restGrid({
22
- extraToolbarItems: () => {
23
- return button({
24
- icon: Icon.diff(),
25
- text: 'Compare w/ Remote',
26
- onClick: () => model.openDiffer()
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
  );
@@ -4,32 +4,62 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+
8
+ import * as Col from '@xh/hoist/admin/columns/Rest';
9
+ import * as AdminCol from '@xh/hoist/admin/columns';
7
10
  import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog';
8
11
  import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceModel';
12
+ import {hframe} from '@xh/hoist/cmp/layout';
9
13
  import {creates, hoistCmp} from '@xh/hoist/core';
10
14
  import {button} from '@xh/hoist/desktop/cmp/button';
15
+ import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel';
11
16
  import {panel} from '@xh/hoist/desktop/cmp/panel';
12
17
  import {restGrid} from '@xh/hoist/desktop/cmp/rest';
18
+ import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
13
19
  import {Icon} from '@xh/hoist/icon';
14
20
 
15
21
  export const userPreferencePanel = hoistCmp.factory({
16
22
  model: creates(UserPreferenceModel),
17
23
 
18
24
  render({model}) {
19
- return panel({
20
- items: [
21
- restGrid({
22
- extraToolbarItems: () => {
23
- return button({
24
- icon: Icon.gear(),
25
- text: 'Configure',
26
- onClick: () => (model.showEditorDialog = true)
27
- });
28
- }
29
- }),
30
- prefEditorDialog()
31
- ],
32
- mask: 'onLoad'
33
- });
25
+ return hframe(
26
+ panel({
27
+ items: [
28
+ restGrid({
29
+ extraToolbarItems: () => [
30
+ button({
31
+ icon: Icon.gear(),
32
+ text: 'Configure',
33
+ onClick: () => (model.showEditorDialog = true)
34
+ }),
35
+ toolbarSep(),
36
+ jsonSearchButton({
37
+ subjectName: 'User Preference',
38
+ docSearchUrl: 'jsonSearch/searchUserPreferences',
39
+ gridModelConfig: {
40
+ sortBy: ['groupName', 'name', 'owner'],
41
+ columns: [
42
+ {
43
+ field: {name: 'owner', type: 'string'},
44
+ width: 200
45
+ },
46
+ {...AdminCol.groupName},
47
+ {...AdminCol.name},
48
+ {
49
+ field: {name: 'json', type: 'string'},
50
+ hidden: true
51
+ },
52
+ {...Col.lastUpdated}
53
+ ]
54
+ },
55
+ groupByOptions: ['owner', 'groupName', 'name']
56
+ })
57
+ ]
58
+ }),
59
+ prefEditorDialog()
60
+ ],
61
+ mask: 'onLoad'
62
+ })
63
+ );
34
64
  }
35
65
  });
@@ -0,0 +1,18 @@
1
+ /// <reference types="react" />
2
+ import { GridConfig } from '@xh/hoist/cmp/grid';
3
+ import { HoistProps, SelectOption } from '@xh/hoist/core';
4
+ export interface JsonSearchButtonProps extends HoistProps {
5
+ /** Name of the type of Json Documents being searched. This appears in the dialog title. */
6
+ subjectName: string;
7
+ /** Url to endpoint for searching for matching JSON documents */
8
+ docSearchUrl: string;
9
+ /**
10
+ * Config for GridModel used to display search results.
11
+ */
12
+ gridModelConfig: GridConfig;
13
+ /**
14
+ * Names of fields that can be used to group by.
15
+ */
16
+ groupByOptions: Array<SelectOption | any>;
17
+ }
18
+ export declare const JsonSearchButton: import("react").FC<JsonSearchButtonProps>, jsonSearchButton: import("@xh/hoist/core").ElementFactory<JsonSearchButtonProps>;