@xh/hoist 72.0.0-SNAPSHOT.1737733122023 → 72.0.0-SNAPSHOT.1737748132453

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,210 @@
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 {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
9
+ import {errorMessage} from '@xh/hoist/cmp/error';
10
+ import {JsonBlobModel} from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel';
11
+ import {grid, gridCountLabel} from '@xh/hoist/cmp/grid';
12
+ import {a, box, filler, h4, hframe, label, li, span, ul, vbox} from '@xh/hoist/cmp/layout';
13
+ import {hoistCmp, useLocalModel} from '@xh/hoist/core';
14
+ import {button} from '@xh/hoist/desktop/cmp/button';
15
+ import {buttonGroupInput, jsonInput, textInput} from '@xh/hoist/desktop/cmp/input';
16
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
17
+ import {Icon} from '@xh/hoist/icon';
18
+ import {popover} from '@xh/hoist/kit/blueprint';
19
+ import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard';
20
+ import {startCase} from 'lodash';
21
+
22
+ import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel';
23
+
24
+ export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory<JsonBlobModel>({
25
+ render() {
26
+ const impl = useLocalModel(JsonSearchPanelImplModel),
27
+ {error} = impl;
28
+
29
+ return panel({
30
+ title: 'JSON Path Search',
31
+ icon: Icon.json(),
32
+ modelConfig: {
33
+ side: 'right',
34
+ defaultSize: '75%',
35
+ collapsible: true,
36
+ defaultCollapsed: true
37
+ },
38
+ compactHeader: true,
39
+ tbar: searchTbar({model: impl}),
40
+ flex: 1,
41
+ item: panel({
42
+ mask: impl.docLoadTask,
43
+ items: [
44
+ errorMessage({
45
+ error,
46
+ title: error?.name ? startCase(error.name) : undefined
47
+ }),
48
+ hframe({
49
+ omit: impl.error,
50
+ items: [
51
+ panel({
52
+ item: grid({model: impl.gridModel})
53
+ }),
54
+ panel({
55
+ mask: impl.nodeLoadTask,
56
+ tbar: nodeTbar({model: impl}),
57
+ bbar: nodeBbar({
58
+ omit: !impl.asPathList,
59
+ model: impl
60
+ }),
61
+ item: jsonInput({
62
+ model: impl,
63
+ bind: 'matchingNodes',
64
+ flex: 1,
65
+ width: '100%',
66
+ readonly: true,
67
+ showCopyButton: true
68
+ })
69
+ })
70
+ ]
71
+ })
72
+ ]
73
+ })
74
+ });
75
+ }
76
+ });
77
+
78
+ const searchTbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
79
+ return toolbar(
80
+ pathField({model}),
81
+ helpButton(),
82
+ toolbarSep(),
83
+ gridCountLabel({
84
+ gridModel: model.gridModel,
85
+ unit: 'document'
86
+ })
87
+ );
88
+ });
89
+
90
+ const pathField = hoistCmp.factory<JsonSearchPanelImplModel>({
91
+ render({model}) {
92
+ return textInput({
93
+ bind: 'path',
94
+ autoFocus: true,
95
+ commitOnChange: true,
96
+ leftIcon: Icon.search(),
97
+ enableClear: true,
98
+ placeholder:
99
+ "JSON Path - e.g. $..[?(@.colId == 'trader')] - type a path and hit ENTER to search",
100
+ width: null,
101
+ flex: 1,
102
+ onKeyDown: e => {
103
+ if (e.key === 'Enter') model.loadJsonDocsAsync();
104
+ }
105
+ });
106
+ }
107
+ });
108
+
109
+ const helpButton = hoistCmp.factory({
110
+ model: false,
111
+ render() {
112
+ return popover({
113
+ item: button({
114
+ icon: Icon.questionCircle(),
115
+ outlined: true
116
+ }),
117
+ content: vbox({
118
+ style: {
119
+ padding: '0px 20px 10px 20px'
120
+ },
121
+ items: [
122
+ h4('Sample Queries'),
123
+ ul({
124
+ style: {listStyleType: 'none'},
125
+ items: [
126
+ {
127
+ query: "$..[?(@.colId == 'trader')]",
128
+ explanation:
129
+ 'Find all nodes with a property "colId" equal to "trader"'
130
+ }
131
+ ].map(({query, explanation}) =>
132
+ li({
133
+ key: query,
134
+ items: [
135
+ span({
136
+ className:
137
+ 'xh-border xh-pad-half xh-bg-alt xh-font-family-mono',
138
+ item: query
139
+ }),
140
+ ' ',
141
+ clipboardButton({
142
+ text: null,
143
+ icon: Icon.copy(),
144
+ getCopyText: () => query,
145
+ successMessage: 'Query copied to clipboard.'
146
+ }),
147
+ ' ',
148
+ explanation
149
+ ]
150
+ })
151
+ )
152
+ }),
153
+ a({
154
+ href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators',
155
+ target: '_blank',
156
+ item: 'Path Syntax Docs & More Examples'
157
+ })
158
+ ]
159
+ })
160
+ });
161
+ }
162
+ });
163
+
164
+ const nodeTbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
165
+ return toolbar(
166
+ buttonGroupInput({
167
+ model,
168
+ bind: 'pathOrValue',
169
+ minimal: true,
170
+ outlined: true,
171
+ items: [
172
+ button({
173
+ text: 'Path',
174
+ value: 'path'
175
+ }),
176
+ button({
177
+ text: 'Value',
178
+ value: 'value'
179
+ })
180
+ ]
181
+ }),
182
+ filler(),
183
+ box({
184
+ omit: !model.matchingNodeCount,
185
+ item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}`
186
+ })
187
+ );
188
+ });
189
+
190
+ const nodeBbar = hoistCmp.factory<JsonSearchPanelImplModel>(({model}) => {
191
+ return toolbar(
192
+ label('Path Format:'),
193
+ buttonGroupInput({
194
+ model,
195
+ bind: 'pathFormat',
196
+ minimal: true,
197
+ outlined: true,
198
+ items: [
199
+ button({
200
+ text: 'XPath',
201
+ value: 'XPath'
202
+ }),
203
+ button({
204
+ text: 'JSONPath',
205
+ value: 'JSONPath'
206
+ })
207
+ ]
208
+ })
209
+ );
210
+ });
@@ -0,0 +1,130 @@
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 {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
10
+ import {bindable, makeObservable} from '@xh/hoist/mobx';
11
+ import {isEmpty} from 'lodash';
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export class JsonSearchPanelImplModel extends HoistModel {
17
+ override xhImpl = true;
18
+
19
+ @managed gridModel: GridModel;
20
+ @managed docLoadTask: TaskObserver = TaskObserver.trackLast();
21
+ @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast();
22
+
23
+ @bindable.ref error = null;
24
+ @bindable path: string = '';
25
+ @bindable pathOrValue: 'path' | 'value' = 'value';
26
+ @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
27
+ @bindable matchingNodes: string = '';
28
+ @bindable matchingNodeCount: number = 0;
29
+
30
+ get asPathList(): boolean {
31
+ return this.pathOrValue === 'path';
32
+ }
33
+
34
+ get queryBuffer(): number {
35
+ return this.componentProps.queryBuffer ?? 200;
36
+ }
37
+
38
+ get docSearchUrl(): string {
39
+ return this.componentProps.docSearchUrl;
40
+ }
41
+
42
+ get matchingNodesUrl(): string {
43
+ return this.componentProps.matchingNodesUrl;
44
+ }
45
+
46
+ get gridModelConfig() {
47
+ return this.componentProps.gridModelConfig;
48
+ }
49
+
50
+ constructor() {
51
+ super();
52
+ makeObservable(this);
53
+ }
54
+
55
+ override onLinked() {
56
+ this.gridModel = new GridModel({
57
+ ...this.gridModelConfig,
58
+ selModel: 'single'
59
+ });
60
+
61
+ this.addReaction(
62
+ {
63
+ track: () => this.path,
64
+ run: path => {
65
+ if (isEmpty(path)) {
66
+ this.error = null;
67
+ this.gridModel.clear();
68
+ }
69
+ }
70
+ },
71
+ {
72
+ track: () => [this.gridModel.selectedRecord, this.pathOrValue, this.pathFormat],
73
+ run: () => this.loadJsonNodesAsync(),
74
+ debounce: 300
75
+ }
76
+ );
77
+ }
78
+
79
+ async loadJsonDocsAsync() {
80
+ if (isEmpty(this.path)) {
81
+ this.error = null;
82
+ this.gridModel.clear();
83
+ return;
84
+ }
85
+
86
+ try {
87
+ const data = await XH.fetchJson({
88
+ url: this.docSearchUrl,
89
+ params: {path: this.path}
90
+ }).linkTo(this.docLoadTask);
91
+
92
+ this.error = null;
93
+ this.gridModel.loadData(data);
94
+ this.gridModel.selectFirstAsync();
95
+ } catch (e) {
96
+ this.gridModel.clear();
97
+ this.error = e;
98
+ }
99
+ }
100
+
101
+ private async loadJsonNodesAsync() {
102
+ if (!this.gridModel.selectedRecord) {
103
+ this.matchingNodeCount = 0;
104
+ this.matchingNodes = '';
105
+ return;
106
+ }
107
+
108
+ let nodes = await XH.fetchJson({
109
+ url: this.matchingNodesUrl,
110
+ params: {
111
+ path: this.path,
112
+ asPathList: this.pathOrValue === 'path',
113
+ json: this.gridModel.selectedRecord.data.json
114
+ }
115
+ }).linkTo(this.nodeLoadTask);
116
+
117
+ this.matchingNodeCount = nodes.length;
118
+ if (this.asPathList && this.pathFormat === 'XPath') {
119
+ nodes = nodes.map(it => this.convertToPath(it));
120
+ }
121
+ this.matchingNodes = JSON.stringify(nodes, null, 2);
122
+ }
123
+
124
+ private convertToPath(JSONPath: string): string {
125
+ return JSONPath.replaceAll(/^\$\['?/g, '/')
126
+ .replaceAll(/^\$/g, '')
127
+ .replaceAll(/'?]\['?/g, '/')
128
+ .replaceAll(/'?]$/g, '');
129
+ }
130
+ }
@@ -5,9 +5,13 @@
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 {jsonSearchPanel} 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';
12
16
  import {Icon} from '@xh/hoist/icon';
13
17
  import {differ} from '../../../differ/Differ';
@@ -17,14 +21,48 @@ export const jsonBlobPanel = hoistCmp.factory({
17
21
  model: creates(JsonBlobModel),
18
22
 
19
23
  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
- });
24
+ return hframe(
25
+ panel({
26
+ item: restGrid({
27
+ extraToolbarItems: () => {
28
+ return button({
29
+ icon: Icon.diff(),
30
+ text: 'Compare w/ Remote',
31
+ onClick: () => model.openDiffer()
32
+ });
33
+ }
34
+ })
35
+ }),
36
+ jsonSearchPanel({
37
+ docSearchUrl: 'jsonBlobSearchAdmin/searchByJsonPath',
38
+ matchingNodesUrl: 'jsonBlobSearchAdmin/getMatchingNodes',
39
+ gridModelConfig: {
40
+ sortBy: ['owner', 'name'],
41
+ store: {
42
+ idSpec: 'token'
43
+ },
44
+ groupBy: 'type',
45
+ columns: [
46
+ {
47
+ field: {name: 'token', type: 'string'},
48
+ hidden: true,
49
+ width: 100
50
+ },
51
+ {
52
+ field: {name: 'type', type: 'string'},
53
+ width: 200
54
+ },
55
+ {
56
+ field: {name: 'owner', type: 'string'},
57
+ width: 200
58
+ },
59
+ {...AdminCol.name},
60
+ {
61
+ field: {name: 'json', type: 'string'},
62
+ hidden: true
63
+ },
64
+ {...Col.lastUpdated}
65
+ ]
28
66
  }
29
67
  }),
30
68
  differ({omit: !model.differModel})
@@ -4,10 +4,14 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import * as Col from '@xh/hoist/admin/columns/Rest';
8
+ import * as AdminCol from '@xh/hoist/admin/columns';
7
9
  import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog';
8
10
  import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceModel';
11
+ import {hframe} from '@xh/hoist/cmp/layout';
9
12
  import {creates, hoistCmp} from '@xh/hoist/core';
10
13
  import {button} from '@xh/hoist/desktop/cmp/button';
14
+ import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel';
11
15
  import {panel} from '@xh/hoist/desktop/cmp/panel';
12
16
  import {restGrid} from '@xh/hoist/desktop/cmp/rest';
13
17
  import {Icon} from '@xh/hoist/icon';
@@ -16,20 +20,43 @@ export const userPreferencePanel = hoistCmp.factory({
16
20
  model: creates(UserPreferenceModel),
17
21
 
18
22
  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
- });
23
+ return hframe(
24
+ panel({
25
+ items: [
26
+ restGrid({
27
+ extraToolbarItems: () => {
28
+ return button({
29
+ icon: Icon.gear(),
30
+ text: 'Configure',
31
+ onClick: () => (model.showEditorDialog = true)
32
+ });
33
+ }
34
+ }),
35
+ prefEditorDialog()
36
+ ],
37
+ mask: 'onLoad'
38
+ }),
39
+ jsonSearchPanel({
40
+ docSearchUrl: 'preferenceJsonSearchAdmin/searchByJsonPath',
41
+ matchingNodesUrl: 'preferenceJsonSearchAdmin/getMatchingNodes',
42
+ gridModelConfig: {
43
+ sortBy: ['name'],
44
+ groupBy: 'groupName',
45
+ columns: [
46
+ {
47
+ field: {name: 'owner', type: 'string'},
48
+ width: 200
49
+ },
50
+ {...AdminCol.groupName},
51
+ {...AdminCol.name},
52
+ {
53
+ field: {name: 'json', type: 'string'},
54
+ hidden: true
55
+ },
56
+ {...Col.lastUpdated}
57
+ ]
58
+ }
59
+ })
60
+ );
34
61
  }
35
62
  });
@@ -0,0 +1,3 @@
1
+ /// <reference types="react" />
2
+ import { JsonBlobModel } from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel';
3
+ export declare const JsonSearchPanel: import("react").FC<import("@xh/hoist/core").DefaultHoistProps<JsonBlobModel>>, jsonSearchPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<JsonBlobModel>>;
@@ -0,0 +1,27 @@
1
+ import { GridModel } from '@xh/hoist/cmp/grid';
2
+ import { HoistModel, TaskObserver } from '@xh/hoist/core';
3
+ /**
4
+ * @internal
5
+ */
6
+ export declare class JsonSearchPanelImplModel extends HoistModel {
7
+ xhImpl: boolean;
8
+ gridModel: GridModel;
9
+ docLoadTask: TaskObserver;
10
+ nodeLoadTask: TaskObserver;
11
+ error: any;
12
+ path: string;
13
+ pathOrValue: 'path' | 'value';
14
+ pathFormat: 'XPath' | 'JSONPath';
15
+ matchingNodes: string;
16
+ matchingNodeCount: number;
17
+ get asPathList(): boolean;
18
+ get queryBuffer(): number;
19
+ get docSearchUrl(): string;
20
+ get matchingNodesUrl(): string;
21
+ get gridModelConfig(): any;
22
+ constructor();
23
+ onLinked(): void;
24
+ loadJsonDocsAsync(): Promise<void>;
25
+ private loadJsonNodesAsync;
26
+ private convertToPath;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "72.0.0-SNAPSHOT.1737733122023",
3
+ "version": "72.0.0-SNAPSHOT.1737748132453",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",