@mongoosejs/studio 0.1.5 → 0.1.7
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/backend/actions/Model/getDocuments.js +17 -4
- package/backend/actions/Model/getDocumentsStream.js +17 -4
- package/frontend/public/app.js +386 -286
- package/frontend/public/tw.css +21 -0
- package/frontend/src/dashboard/dashboard.js +23 -0
- package/frontend/src/list-json/json-node.html +118 -0
- package/frontend/src/list-json/list-json.html +1 -0
- package/frontend/src/list-json/list-json.js +38 -88
- package/frontend/src/models/document-search/document-search.html +23 -0
- package/frontend/src/models/document-search/document-search.js +227 -0
- package/frontend/src/models/models.html +9 -8
- package/frontend/src/models/models.js +21 -184
- package/frontend/src/models/trie.js +44 -18
- package/package.json +3 -3
package/frontend/public/tw.css
CHANGED
|
@@ -2041,6 +2041,11 @@ video {
|
|
|
2041
2041
|
color: rgb(2 132 199 / var(--tw-text-opacity));
|
|
2042
2042
|
}
|
|
2043
2043
|
|
|
2044
|
+
.text-sky-700 {
|
|
2045
|
+
--tw-text-opacity: 1;
|
|
2046
|
+
color: rgb(3 105 161 / var(--tw-text-opacity));
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2044
2049
|
.text-sky-800 {
|
|
2045
2050
|
--tw-text-opacity: 1;
|
|
2046
2051
|
color: rgb(7 89 133 / var(--tw-text-opacity));
|
|
@@ -2086,6 +2091,18 @@ video {
|
|
|
2086
2091
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
|
2087
2092
|
}
|
|
2088
2093
|
|
|
2094
|
+
.underline {
|
|
2095
|
+
text-decoration-line: underline;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
.decoration-dotted {
|
|
2099
|
+
text-decoration-style: dotted;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
.underline-offset-2 {
|
|
2103
|
+
text-underline-offset: 2px;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2089
2106
|
.accent-sky-600 {
|
|
2090
2107
|
accent-color: #0284c7;
|
|
2091
2108
|
}
|
|
@@ -2469,6 +2486,10 @@ video {
|
|
|
2469
2486
|
border-color: transparent;
|
|
2470
2487
|
}
|
|
2471
2488
|
|
|
2489
|
+
.focus\:opacity-100:focus {
|
|
2490
|
+
opacity: 1;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2472
2493
|
.focus\:outline-none:focus {
|
|
2473
2494
|
outline: 2px solid transparent;
|
|
2474
2495
|
outline-offset: 2px;
|
|
@@ -44,6 +44,25 @@ module.exports = app => app.component('dashboard', {
|
|
|
44
44
|
} finally {
|
|
45
45
|
this.status = 'loaded';
|
|
46
46
|
}
|
|
47
|
+
},
|
|
48
|
+
shouldEvaluateDashboard() {
|
|
49
|
+
if (this.dashboardResults.length === 0) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const finishedEvaluatingAt = this.dashboardResults[0].finishedEvaluatingAt;
|
|
54
|
+
if (!finishedEvaluatingAt) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sixHoursAgo = Date.now() - 6 * 60 * 60 * 1000;
|
|
59
|
+
const finishedAt = new Date(finishedEvaluatingAt).getTime();
|
|
60
|
+
|
|
61
|
+
if (Number.isNaN(finishedAt)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return finishedAt < sixHoursAgo;
|
|
47
66
|
}
|
|
48
67
|
},
|
|
49
68
|
computed: {
|
|
@@ -62,6 +81,10 @@ module.exports = app => app.component('dashboard', {
|
|
|
62
81
|
this.title = this.dashboard.title;
|
|
63
82
|
this.description = this.dashboard.description ?? '';
|
|
64
83
|
this.dashboardResults = dashboardResults;
|
|
84
|
+
if (this.shouldEvaluateDashboard()) {
|
|
85
|
+
await this.evaluateDashboard();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
65
88
|
this.status = 'loaded';
|
|
66
89
|
}
|
|
67
90
|
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<div>
|
|
2
|
+
<div class="flex items-baseline whitespace-pre" :style="indentStyle">
|
|
3
|
+
<button
|
|
4
|
+
v-if="showToggle"
|
|
5
|
+
type="button"
|
|
6
|
+
class="w-4 h-4 mr-1 inline-flex items-center justify-center leading-none text-gray-500 hover:text-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-slate-400 cursor-pointer"
|
|
7
|
+
@click.stop="handleToggle"
|
|
8
|
+
>
|
|
9
|
+
{{ isCollapsedNode ? '+' : '-' }}
|
|
10
|
+
</button>
|
|
11
|
+
<span v-else class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible flex-shrink-0"></span>
|
|
12
|
+
<template v-if="hasKey">
|
|
13
|
+
<span class="text-blue-600">"{{ nodeKey }}"</span><span>: </span>
|
|
14
|
+
</template>
|
|
15
|
+
<template v-if="isComplex">
|
|
16
|
+
<template v-if="hasChildren">
|
|
17
|
+
<span>{{ openingBracket }}</span>
|
|
18
|
+
<span v-if="isCollapsedNode" class="mx-1">…</span>
|
|
19
|
+
<span v-if="isCollapsedNode">{{ closingBracket }}{{ comma }}</span>
|
|
20
|
+
</template>
|
|
21
|
+
<template v-else>
|
|
22
|
+
<span>{{ openingBracket }}{{ closingBracket }}{{ comma }}</span>
|
|
23
|
+
</template>
|
|
24
|
+
</template>
|
|
25
|
+
<template v-else>
|
|
26
|
+
<!--
|
|
27
|
+
If value is a string and overflows its container (i.e. goes over one line), show an ellipsis.
|
|
28
|
+
This is done via CSS ellipsis strategy.
|
|
29
|
+
-->
|
|
30
|
+
<span
|
|
31
|
+
v-if="shouldShowReferenceLink"
|
|
32
|
+
class="inline-flex items-baseline group"
|
|
33
|
+
>
|
|
34
|
+
<span
|
|
35
|
+
:class="[...valueClasses, 'underline', 'decoration-dotted', 'underline-offset-2']"
|
|
36
|
+
:style="typeof value === 'string'
|
|
37
|
+
? {
|
|
38
|
+
display: 'inline-block',
|
|
39
|
+
maxWidth: '100%',
|
|
40
|
+
overflow: 'hidden',
|
|
41
|
+
textOverflow: 'ellipsis',
|
|
42
|
+
whiteSpace: 'nowrap',
|
|
43
|
+
verticalAlign: 'bottom'
|
|
44
|
+
}
|
|
45
|
+
: {}"
|
|
46
|
+
:title="typeof value === 'string' && $el && $el.scrollWidth > $el.clientWidth ? value : undefined"
|
|
47
|
+
>
|
|
48
|
+
{{ formattedValue }}
|
|
49
|
+
</span>
|
|
50
|
+
<span>
|
|
51
|
+
{{ comma }}
|
|
52
|
+
</span>
|
|
53
|
+
<a
|
|
54
|
+
href="#"
|
|
55
|
+
class="ml-1 text-sm text-sky-700 opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity"
|
|
56
|
+
@click.stop.prevent="goToReference(value)"
|
|
57
|
+
>
|
|
58
|
+
View Document
|
|
59
|
+
</a>
|
|
60
|
+
</span>
|
|
61
|
+
<span
|
|
62
|
+
v-else
|
|
63
|
+
:class="valueClasses"
|
|
64
|
+
:style="typeof value === 'string'
|
|
65
|
+
? {
|
|
66
|
+
display: 'inline-block',
|
|
67
|
+
maxWidth: '100%',
|
|
68
|
+
overflow: 'hidden',
|
|
69
|
+
textOverflow: 'ellipsis',
|
|
70
|
+
whiteSpace: 'nowrap',
|
|
71
|
+
verticalAlign: 'bottom'
|
|
72
|
+
}
|
|
73
|
+
: {}"
|
|
74
|
+
:title="typeof value === 'string' && $el && $el.scrollWidth > $el.clientWidth ? value : undefined"
|
|
75
|
+
>
|
|
76
|
+
{{ formattedValue }}{{ comma }}
|
|
77
|
+
</span>
|
|
78
|
+
</template>
|
|
79
|
+
</div>
|
|
80
|
+
<template v-if="isComplex && hasChildren && !isCollapsedNode">
|
|
81
|
+
<json-node
|
|
82
|
+
v-for="child in children"
|
|
83
|
+
:key="child.path"
|
|
84
|
+
:node-key="child.displayKey"
|
|
85
|
+
:value="child.value"
|
|
86
|
+
:level="level + 1"
|
|
87
|
+
:is-last="child.isLast"
|
|
88
|
+
:path="child.path"
|
|
89
|
+
:toggle-collapse="toggleCollapse"
|
|
90
|
+
:is-collapsed="isCollapsed"
|
|
91
|
+
:create-child-path="createChildPath"
|
|
92
|
+
:indent-size="indentSize"
|
|
93
|
+
:max-top-level-fields="maxTopLevelFields"
|
|
94
|
+
:top-level-expanded="topLevelExpanded"
|
|
95
|
+
:expand-top-level="expandTopLevel"
|
|
96
|
+
:references="references"
|
|
97
|
+
></json-node>
|
|
98
|
+
<div
|
|
99
|
+
v-if="hasHiddenRootChildren"
|
|
100
|
+
class="flex items-baseline whitespace-pre"
|
|
101
|
+
:style="indentStyle"
|
|
102
|
+
>
|
|
103
|
+
<span class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible"></span>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="text-xs inline-flex items-center gap-1 ml-4 text-slate-500 hover:text-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-slate-400"
|
|
107
|
+
:title="hiddenChildrenTooltip"
|
|
108
|
+
@click.stop="handleExpandTopLevel"
|
|
109
|
+
>
|
|
110
|
+
<span aria-hidden="true">{{hiddenChildrenLabel}}…</span>
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="flex items-baseline whitespace-pre" :style="indentStyle">
|
|
114
|
+
<span class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible"></span>
|
|
115
|
+
<span>{{ closingBracket }}{{ comma }}</span>
|
|
116
|
+
</div>
|
|
117
|
+
</template>
|
|
118
|
+
</div>
|
|
@@ -2,97 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const template = require('./list-json.html');
|
|
4
4
|
|
|
5
|
-
const JsonNodeTemplate =
|
|
6
|
-
<div>
|
|
7
|
-
<div class="flex items-baseline whitespace-pre" :style="indentStyle">
|
|
8
|
-
<button
|
|
9
|
-
v-if="showToggle"
|
|
10
|
-
type="button"
|
|
11
|
-
class="w-4 h-4 mr-1 inline-flex items-center justify-center leading-none text-gray-500 hover:text-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-slate-400 cursor-pointer"
|
|
12
|
-
@click.stop="handleToggle"
|
|
13
|
-
>
|
|
14
|
-
{{ isCollapsedNode ? '+' : '-' }}
|
|
15
|
-
</button>
|
|
16
|
-
<span v-else class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible flex-shrink-0"></span>
|
|
17
|
-
<template v-if="hasKey">
|
|
18
|
-
<span class="text-blue-600">"{{ nodeKey }}"</span><span>: </span>
|
|
19
|
-
</template>
|
|
20
|
-
<template v-if="isComplex">
|
|
21
|
-
<template v-if="hasChildren">
|
|
22
|
-
<span>{{ openingBracket }}</span>
|
|
23
|
-
<span v-if="isCollapsedNode" class="mx-1">…</span>
|
|
24
|
-
<span v-if="isCollapsedNode">{{ closingBracket }}{{ comma }}</span>
|
|
25
|
-
</template>
|
|
26
|
-
<template v-else>
|
|
27
|
-
<span>{{ openingBracket }}{{ closingBracket }}{{ comma }}</span>
|
|
28
|
-
</template>
|
|
29
|
-
</template>
|
|
30
|
-
<template v-else>
|
|
31
|
-
<!--
|
|
32
|
-
If value is a string and overflows its container (i.e. goes over one line), show an ellipsis.
|
|
33
|
-
This is done via CSS ellipsis strategy.
|
|
34
|
-
-->
|
|
35
|
-
<span
|
|
36
|
-
:class="valueClasses"
|
|
37
|
-
:style="typeof value === 'string'
|
|
38
|
-
? {
|
|
39
|
-
display: 'inline-block',
|
|
40
|
-
maxWidth: '100%',
|
|
41
|
-
overflow: 'hidden',
|
|
42
|
-
textOverflow: 'ellipsis',
|
|
43
|
-
whiteSpace: 'nowrap',
|
|
44
|
-
verticalAlign: 'bottom'
|
|
45
|
-
}
|
|
46
|
-
: {}"
|
|
47
|
-
:title="typeof value === 'string' && $el && $el.scrollWidth > $el.clientWidth ? value : undefined"
|
|
48
|
-
>
|
|
49
|
-
{{ formattedValue }}{{ comma }}
|
|
50
|
-
</span>
|
|
51
|
-
</template>
|
|
52
|
-
</div>
|
|
53
|
-
<template v-if="isComplex && hasChildren && !isCollapsedNode">
|
|
54
|
-
<json-node
|
|
55
|
-
v-for="child in children"
|
|
56
|
-
:key="child.path"
|
|
57
|
-
:node-key="child.displayKey"
|
|
58
|
-
:value="child.value"
|
|
59
|
-
:level="level + 1"
|
|
60
|
-
:is-last="child.isLast"
|
|
61
|
-
:path="child.path"
|
|
62
|
-
:toggle-collapse="toggleCollapse"
|
|
63
|
-
:is-collapsed="isCollapsed"
|
|
64
|
-
:create-child-path="createChildPath"
|
|
65
|
-
:indent-size="indentSize"
|
|
66
|
-
:max-top-level-fields="maxTopLevelFields"
|
|
67
|
-
:top-level-expanded="topLevelExpanded"
|
|
68
|
-
:expand-top-level="expandTopLevel"
|
|
69
|
-
></json-node>
|
|
70
|
-
<div
|
|
71
|
-
v-if="hasHiddenRootChildren"
|
|
72
|
-
class="flex items-baseline whitespace-pre"
|
|
73
|
-
:style="indentStyle"
|
|
74
|
-
>
|
|
75
|
-
<span class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible"></span>
|
|
76
|
-
<button
|
|
77
|
-
type="button"
|
|
78
|
-
class="text-xs inline-flex items-center gap-1 ml-4 text-slate-500 hover:text-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-slate-400"
|
|
79
|
-
:title="hiddenChildrenTooltip"
|
|
80
|
-
@click.stop="handleExpandTopLevel"
|
|
81
|
-
>
|
|
82
|
-
<span aria-hidden="true">{{hiddenChildrenLabel}}…</span>
|
|
83
|
-
</button>
|
|
84
|
-
</div>
|
|
85
|
-
<div class="flex items-baseline whitespace-pre" :style="indentStyle">
|
|
86
|
-
<span class="w-4 h-4 mr-1 inline-flex items-center justify-center invisible"></span>
|
|
87
|
-
<span>{{ closingBracket }}{{ comma }}</span>
|
|
88
|
-
</div>
|
|
89
|
-
</template>
|
|
90
|
-
</div>
|
|
91
|
-
`;
|
|
5
|
+
const JsonNodeTemplate = require('./json-node.html');
|
|
92
6
|
|
|
93
7
|
module.exports = app => app.component('list-json', {
|
|
94
8
|
template: template,
|
|
95
|
-
props:
|
|
9
|
+
props: {
|
|
10
|
+
value: {
|
|
11
|
+
required: true
|
|
12
|
+
},
|
|
13
|
+
references: {
|
|
14
|
+
type: Object,
|
|
15
|
+
default: () => ({})
|
|
16
|
+
}
|
|
17
|
+
},
|
|
96
18
|
data() {
|
|
97
19
|
return {
|
|
98
20
|
collapsedMap: {},
|
|
@@ -196,6 +118,10 @@ module.exports = app => app.component('list-json', {
|
|
|
196
118
|
expandTopLevel: {
|
|
197
119
|
type: Function,
|
|
198
120
|
default: null
|
|
121
|
+
},
|
|
122
|
+
references: {
|
|
123
|
+
type: Object,
|
|
124
|
+
default: () => ({})
|
|
199
125
|
}
|
|
200
126
|
},
|
|
201
127
|
computed: {
|
|
@@ -334,6 +260,24 @@ module.exports = app => app.component('list-json', {
|
|
|
334
260
|
},
|
|
335
261
|
hiddenChildrenTooltip() {
|
|
336
262
|
return this.hiddenChildrenLabel;
|
|
263
|
+
},
|
|
264
|
+
normalizedPath() {
|
|
265
|
+
if (typeof this.path !== 'string') {
|
|
266
|
+
return '';
|
|
267
|
+
}
|
|
268
|
+
return this.path
|
|
269
|
+
.replace(/^root\.?/, '')
|
|
270
|
+
.replace(/\[\d+\]/g, '')
|
|
271
|
+
.replace(/^\./, '');
|
|
272
|
+
},
|
|
273
|
+
referenceModel() {
|
|
274
|
+
if (!this.normalizedPath || !this.references) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
return this.references[this.normalizedPath] || null;
|
|
278
|
+
},
|
|
279
|
+
shouldShowReferenceLink() {
|
|
280
|
+
return Boolean(this.referenceModel) && typeof this.value === 'string';
|
|
337
281
|
}
|
|
338
282
|
},
|
|
339
283
|
methods: {
|
|
@@ -358,6 +302,12 @@ module.exports = app => app.component('list-json', {
|
|
|
358
302
|
if (this.isRoot && typeof this.expandTopLevel === 'function') {
|
|
359
303
|
this.expandTopLevel();
|
|
360
304
|
}
|
|
305
|
+
},
|
|
306
|
+
goToReference(id) {
|
|
307
|
+
if (!this.referenceModel) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.$router.push({ path: `/model/${this.referenceModel}/document/${id}` });
|
|
361
311
|
}
|
|
362
312
|
}
|
|
363
313
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<form @submit.prevent="emitSearch" class="relative flex-grow m-0">
|
|
2
|
+
<input
|
|
3
|
+
ref="searchInput"
|
|
4
|
+
class="w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none"
|
|
5
|
+
type="text"
|
|
6
|
+
placeholder="Filter"
|
|
7
|
+
v-model="searchText"
|
|
8
|
+
@click="initFilter"
|
|
9
|
+
@input="updateAutocomplete"
|
|
10
|
+
@keydown="handleKeyDown"
|
|
11
|
+
/>
|
|
12
|
+
<ul v-if="autocompleteSuggestions.length" class="absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow">
|
|
13
|
+
<li
|
|
14
|
+
v-for="(suggestion, index) in autocompleteSuggestions"
|
|
15
|
+
:key="suggestion"
|
|
16
|
+
class="px-2 py-1 cursor-pointer"
|
|
17
|
+
:class="{ 'bg-ultramarine-100': index === autocompleteIndex }"
|
|
18
|
+
@mousedown.prevent="applySuggestion(index)"
|
|
19
|
+
>
|
|
20
|
+
{{ suggestion }}
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
</form>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const template = require('./document-search.html');
|
|
4
|
+
const { Trie } = require('../trie');
|
|
5
|
+
|
|
6
|
+
const QUERY_SELECTORS = [
|
|
7
|
+
'$eq',
|
|
8
|
+
'$ne',
|
|
9
|
+
'$gt',
|
|
10
|
+
'$gte',
|
|
11
|
+
'$lt',
|
|
12
|
+
'$lte',
|
|
13
|
+
'$in',
|
|
14
|
+
'$nin',
|
|
15
|
+
'$exists',
|
|
16
|
+
'$regex',
|
|
17
|
+
'$options',
|
|
18
|
+
'$text',
|
|
19
|
+
'$search',
|
|
20
|
+
'$and',
|
|
21
|
+
'$or',
|
|
22
|
+
'$nor',
|
|
23
|
+
'$not',
|
|
24
|
+
'$elemMatch',
|
|
25
|
+
'$size',
|
|
26
|
+
'$all',
|
|
27
|
+
'$type',
|
|
28
|
+
'$expr',
|
|
29
|
+
'$jsonSchema',
|
|
30
|
+
'$mod'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
module.exports = app => app.component('document-search', {
|
|
34
|
+
template,
|
|
35
|
+
props: {
|
|
36
|
+
value: {
|
|
37
|
+
type: String,
|
|
38
|
+
default: ''
|
|
39
|
+
},
|
|
40
|
+
schemaPaths: {
|
|
41
|
+
type: Array,
|
|
42
|
+
default: () => []
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
data() {
|
|
46
|
+
return {
|
|
47
|
+
autocompleteSuggestions: [],
|
|
48
|
+
autocompleteIndex: 0,
|
|
49
|
+
autocompleteTrie: null,
|
|
50
|
+
searchText: this.value || ''
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
watch: {
|
|
54
|
+
value(val) {
|
|
55
|
+
this.searchText = val || '';
|
|
56
|
+
},
|
|
57
|
+
schemaPaths: {
|
|
58
|
+
handler() {
|
|
59
|
+
this.buildAutocompleteTrie();
|
|
60
|
+
},
|
|
61
|
+
deep: true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
created() {
|
|
65
|
+
this.buildAutocompleteTrie();
|
|
66
|
+
},
|
|
67
|
+
methods: {
|
|
68
|
+
emitSearch() {
|
|
69
|
+
this.$emit('input', this.searchText);
|
|
70
|
+
this.$emit('search', this.searchText);
|
|
71
|
+
},
|
|
72
|
+
buildAutocompleteTrie() {
|
|
73
|
+
this.autocompleteTrie = new Trie();
|
|
74
|
+
this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5, 'operator');
|
|
75
|
+
if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
|
|
76
|
+
const paths = this.schemaPaths
|
|
77
|
+
.map(path => path?.path)
|
|
78
|
+
.filter(path => typeof path === 'string' && path.length > 0);
|
|
79
|
+
for (const path of this.schemaPaths) {
|
|
80
|
+
if (path.schema) {
|
|
81
|
+
paths.push(...Object.keys(path.schema).map(subpath => `${path.path}.${subpath}`));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.autocompleteTrie.bulkInsert(paths, 10, 'fieldName');
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
initFilter(ev) {
|
|
88
|
+
if (!this.searchText) {
|
|
89
|
+
this.searchText = '{}';
|
|
90
|
+
this.$nextTick(() => {
|
|
91
|
+
ev.target.setSelectionRange(1, 1);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
updateAutocomplete() {
|
|
96
|
+
const input = this.$refs.searchInput;
|
|
97
|
+
const cursorPos = input ? input.selectionStart : 0;
|
|
98
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
99
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
100
|
+
if (match && match[1]) {
|
|
101
|
+
const token = match[1];
|
|
102
|
+
const leadingQuoteMatch = token.match(/^["']/);
|
|
103
|
+
const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
|
|
104
|
+
? token[token.length - 1]
|
|
105
|
+
: '';
|
|
106
|
+
const term = token
|
|
107
|
+
.replace(/^["']/, '')
|
|
108
|
+
.replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
|
|
109
|
+
.trim();
|
|
110
|
+
if (!term) {
|
|
111
|
+
this.autocompleteSuggestions = [];
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const colonMatch = before.match(/:\s*([^,\}\]]*)$/);
|
|
116
|
+
const role = colonMatch ? 'operator' : 'fieldName';
|
|
117
|
+
|
|
118
|
+
if (this.autocompleteTrie) {
|
|
119
|
+
const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10, role);
|
|
120
|
+
const suggestionsSet = new Set(primarySuggestions);
|
|
121
|
+
if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
|
|
122
|
+
for (const schemaPath of this.schemaPaths) {
|
|
123
|
+
const path = schemaPath?.path;
|
|
124
|
+
if (
|
|
125
|
+
typeof path === 'string' &&
|
|
126
|
+
path.startsWith(`${term}.`) &&
|
|
127
|
+
!suggestionsSet.has(path)
|
|
128
|
+
) {
|
|
129
|
+
suggestionsSet.add(path);
|
|
130
|
+
if (suggestionsSet.size >= 10) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let suggestions = Array.from(suggestionsSet);
|
|
137
|
+
if (leadingQuoteMatch) {
|
|
138
|
+
const leadingQuote = leadingQuoteMatch[0];
|
|
139
|
+
suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
|
|
140
|
+
}
|
|
141
|
+
if (trailingQuoteMatch) {
|
|
142
|
+
suggestions = suggestions.map(suggestion =>
|
|
143
|
+
suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
this.autocompleteSuggestions = suggestions;
|
|
147
|
+
this.autocompleteIndex = 0;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.autocompleteSuggestions = [];
|
|
152
|
+
},
|
|
153
|
+
handleKeyDown(ev) {
|
|
154
|
+
if (this.autocompleteSuggestions.length === 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (ev.key === 'Tab' || ev.key === 'Enter') {
|
|
158
|
+
ev.preventDefault();
|
|
159
|
+
this.applySuggestion(this.autocompleteIndex);
|
|
160
|
+
} else if (ev.key === 'ArrowDown') {
|
|
161
|
+
ev.preventDefault();
|
|
162
|
+
this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
|
|
163
|
+
} else if (ev.key === 'ArrowUp') {
|
|
164
|
+
ev.preventDefault();
|
|
165
|
+
this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
applySuggestion(index) {
|
|
169
|
+
const suggestion = this.autocompleteSuggestions[index];
|
|
170
|
+
if (!suggestion) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const input = this.$refs.searchInput;
|
|
174
|
+
const cursorPos = input.selectionStart;
|
|
175
|
+
const before = this.searchText.slice(0, cursorPos);
|
|
176
|
+
const after = this.searchText.slice(cursorPos);
|
|
177
|
+
const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
|
|
178
|
+
const colonNeeded = !/^\s*:/.test(after);
|
|
179
|
+
if (!match) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const token = match[1];
|
|
183
|
+
const start = cursorPos - token.length;
|
|
184
|
+
let replacement = suggestion;
|
|
185
|
+
const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
|
|
186
|
+
const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
|
|
187
|
+
if (leadingQuote && !replacement.startsWith(leadingQuote)) {
|
|
188
|
+
replacement = `${leadingQuote}${replacement}`;
|
|
189
|
+
}
|
|
190
|
+
if (trailingQuote && !replacement.endsWith(trailingQuote)) {
|
|
191
|
+
replacement = `${replacement}${trailingQuote}`;
|
|
192
|
+
}
|
|
193
|
+
// Only insert : if we know the user isn't entering in a nested path
|
|
194
|
+
if (colonNeeded && (!leadingQuote || trailingQuote)) {
|
|
195
|
+
replacement = `${replacement}:`;
|
|
196
|
+
}
|
|
197
|
+
this.searchText = this.searchText.slice(0, start) + replacement + after;
|
|
198
|
+
this.$nextTick(() => {
|
|
199
|
+
const pos = start + replacement.length;
|
|
200
|
+
input.setSelectionRange(pos, pos);
|
|
201
|
+
});
|
|
202
|
+
this.autocompleteSuggestions = [];
|
|
203
|
+
},
|
|
204
|
+
addPathFilter(path) {
|
|
205
|
+
if (this.searchText) {
|
|
206
|
+
if (this.searchText.endsWith('}')) {
|
|
207
|
+
this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
|
|
208
|
+
} else {
|
|
209
|
+
this.searchText += `, ${path}: }`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
} else {
|
|
213
|
+
// If this.searchText is empty or undefined, initialize it with a new object
|
|
214
|
+
this.searchText = `{ ${path}: }`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
this.$nextTick(() => {
|
|
219
|
+
const input = this.$refs.searchInput;
|
|
220
|
+
const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
|
|
221
|
+
|
|
222
|
+
input.focus();
|
|
223
|
+
input.setSelectionRange(cursorIndex, cursorIndex);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
@@ -37,12 +37,13 @@
|
|
|
37
37
|
<div class="relative h-[42px] z-10">
|
|
38
38
|
<div class="documents-menu">
|
|
39
39
|
<div class="flex flex-row items-center w-full gap-2">
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
<document-search
|
|
41
|
+
ref="documentSearch"
|
|
42
|
+
:value="searchText"
|
|
43
|
+
:schema-paths="schemaPaths"
|
|
44
|
+
@search="search"
|
|
45
|
+
>
|
|
46
|
+
</document-search>
|
|
46
47
|
<div>
|
|
47
48
|
<span v-if="numDocuments == null">Loading ...</span>
|
|
48
49
|
<span v-else-if="typeof numDocuments === 'number'">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>
|
|
@@ -127,7 +128,7 @@
|
|
|
127
128
|
</div>
|
|
128
129
|
<table v-else-if="outputType === 'table'">
|
|
129
130
|
<thead>
|
|
130
|
-
<th v-for="path in filteredPaths" @click="
|
|
131
|
+
<th v-for="path in filteredPaths" @click="addPathFilter(path.path)" class="cursor-pointer">
|
|
131
132
|
{{path.path}}
|
|
132
133
|
<span class="path-type">
|
|
133
134
|
({{(path.instance || 'unknown')}})
|
|
@@ -165,7 +166,7 @@
|
|
|
165
166
|
>
|
|
166
167
|
Open this Document
|
|
167
168
|
</button>
|
|
168
|
-
<list-json :value="filterDocument(document)">
|
|
169
|
+
<list-json :value="filterDocument(document)" :references="referenceMap">
|
|
169
170
|
</list-json>
|
|
170
171
|
</div>
|
|
171
172
|
</div>
|