@mongoosejs/studio 0.5.3 → 0.5.5
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/analyzeSchema.js +202 -0
- package/backend/actions/Model/index.js +2 -0
- package/backend/actions/Model/validateDocument.js +35 -0
- package/backend/authorize.js +2 -0
- package/backend/helpers/validateDocumentWithTimeout.js +39 -0
- package/frontend/index.js +1 -2
- package/frontend/public/app.js +689 -114
- package/frontend/public/tw.css +221 -28
- package/frontend/src/api.js +12 -0
- package/frontend/src/array-utils.js +60 -2
- package/frontend/src/chat/chat.js +2 -2
- package/frontend/src/detail-array/detail-array.html +98 -14
- package/frontend/src/detail-array/detail-array.js +73 -3
- package/frontend/src/document/document.html +51 -0
- package/frontend/src/document/document.js +13 -0
- package/frontend/src/document-details/document-details.html +10 -3
- package/frontend/src/document-details/document-details.js +28 -0
- package/frontend/src/document-details/document-property/document-property.html +36 -44
- package/frontend/src/document-details/document-property/document-property.js +78 -14
- package/frontend/src/edit-array/edit-array.html +143 -8
- package/frontend/src/edit-array/edit-array.js +104 -2
- package/frontend/src/index.js +45 -9
- package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.html +169 -0
- package/frontend/src/models/analyze-schema-modal/analyze-schema-modal.js +149 -0
- package/frontend/src/models/models.html +25 -13
- package/frontend/src/models/models.js +5 -13
- package/frontend/src/navbar/navbar.html +4 -4
- package/frontend/src/navbar/navbar.js +2 -3
- package/package.json +2 -1
|
@@ -11,6 +11,7 @@ const appendCSS = require('../../appendCSS');
|
|
|
11
11
|
appendCSS(require('./document-property.css'));
|
|
12
12
|
|
|
13
13
|
const UNSET = Symbol('unset');
|
|
14
|
+
const { isArrayOfObjects } = require('../../array-utils');
|
|
14
15
|
|
|
15
16
|
module.exports = app => app.component('document-property', {
|
|
16
17
|
template,
|
|
@@ -22,6 +23,8 @@ module.exports = app => app.component('document-property', {
|
|
|
22
23
|
renderedValue: UNSET,
|
|
23
24
|
isCollapsed: false, // Start uncollapsed by default
|
|
24
25
|
isValueExpanded: false, // Track if the value is expanded
|
|
26
|
+
/** 'list' | 'table' for array-of-objects display and editing */
|
|
27
|
+
arrayDetailViewMode: 'list',
|
|
25
28
|
detailViewMode: 'text',
|
|
26
29
|
copyButtonLabel: 'Copy',
|
|
27
30
|
copyResetTimeoutId: null,
|
|
@@ -34,7 +37,7 @@ module.exports = app => app.component('document-property', {
|
|
|
34
37
|
this.copyResetTimeoutId = null;
|
|
35
38
|
}
|
|
36
39
|
},
|
|
37
|
-
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight'],
|
|
40
|
+
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight', 'searchQuery'],
|
|
38
41
|
computed: {
|
|
39
42
|
isDatePath() {
|
|
40
43
|
return this.path?.instance === 'Date';
|
|
@@ -97,18 +100,6 @@ module.exports = app => app.component('document-property', {
|
|
|
97
100
|
}
|
|
98
101
|
return this.valueAsString;
|
|
99
102
|
},
|
|
100
|
-
truncatedArrayItems() {
|
|
101
|
-
if (this.isArray && this.needsTruncation && !this.isValueExpanded) {
|
|
102
|
-
return this.arrayValue.slice(0, 2);
|
|
103
|
-
}
|
|
104
|
-
return [];
|
|
105
|
-
},
|
|
106
|
-
remainingArrayCount() {
|
|
107
|
-
if (this.isArray && this.needsTruncation && !this.isValueExpanded) {
|
|
108
|
-
return this.arrayValue.length - 2;
|
|
109
|
-
}
|
|
110
|
-
return 0;
|
|
111
|
-
},
|
|
112
103
|
isGeoJsonGeometry() {
|
|
113
104
|
const value = this.getValueForPath(this.path.path);
|
|
114
105
|
return value != null
|
|
@@ -128,6 +119,34 @@ module.exports = app => app.component('document-property', {
|
|
|
128
119
|
isMultiPolygon() {
|
|
129
120
|
const value = this.getValueForPath(this.path.path);
|
|
130
121
|
return this.isGeoJsonGeometry && value.type === 'MultiPolygon';
|
|
122
|
+
},
|
|
123
|
+
arrayPathCurrentValue() {
|
|
124
|
+
if (!this.path || this.path.instance !== 'Array') {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
if (this.editting) {
|
|
128
|
+
return this.getEditValueForPath(this.path);
|
|
129
|
+
}
|
|
130
|
+
return this.getValueForPath(this.path.path);
|
|
131
|
+
},
|
|
132
|
+
canUseArrayTableView() {
|
|
133
|
+
const v = this.arrayPathCurrentValue;
|
|
134
|
+
return Array.isArray(v) && isArrayOfObjects(v);
|
|
135
|
+
},
|
|
136
|
+
propertyDetailViewMode() {
|
|
137
|
+
if (this.isDatePath) {
|
|
138
|
+
return this.dateViewMode;
|
|
139
|
+
}
|
|
140
|
+
if (this.isObjectIdPath) {
|
|
141
|
+
return this.objectIdViewMode;
|
|
142
|
+
}
|
|
143
|
+
if (this.path?.instance === 'Array') {
|
|
144
|
+
return this.arrayDetailViewMode;
|
|
145
|
+
}
|
|
146
|
+
return this.detailViewMode;
|
|
147
|
+
},
|
|
148
|
+
pathNameParts() {
|
|
149
|
+
return this.getSearchMatchParts(this.path?.path || '', this.searchQuery);
|
|
131
150
|
}
|
|
132
151
|
},
|
|
133
152
|
watch: {
|
|
@@ -144,12 +163,56 @@ module.exports = app => app.component('document-property', {
|
|
|
144
163
|
if (newValue && this.isGeoJsonGeometry) {
|
|
145
164
|
this.detailViewMode = 'map';
|
|
146
165
|
}
|
|
166
|
+
},
|
|
167
|
+
canUseArrayTableView(can) {
|
|
168
|
+
if (!can && this.arrayDetailViewMode === 'table') {
|
|
169
|
+
this.arrayDetailViewMode = 'list';
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
'path.path'() {
|
|
173
|
+
this.arrayDetailViewMode = 'list';
|
|
147
174
|
}
|
|
148
175
|
},
|
|
149
176
|
methods: {
|
|
177
|
+
getSearchMatchParts(text, query) {
|
|
178
|
+
const normalizedQuery = typeof query === 'string' ? query.trim() : '';
|
|
179
|
+
if (!normalizedQuery) {
|
|
180
|
+
return [{ text, matched: false }];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const lowerText = String(text).toLowerCase();
|
|
184
|
+
const lowerQuery = normalizedQuery.toLowerCase();
|
|
185
|
+
const parts = [];
|
|
186
|
+
let position = 0;
|
|
187
|
+
let matchIndex = lowerText.indexOf(lowerQuery, position);
|
|
188
|
+
|
|
189
|
+
while (matchIndex !== -1) {
|
|
190
|
+
if (matchIndex > position) {
|
|
191
|
+
parts.push({ text: text.slice(position, matchIndex), matched: false });
|
|
192
|
+
}
|
|
193
|
+
const matchEnd = matchIndex + normalizedQuery.length;
|
|
194
|
+
parts.push({ text: text.slice(matchIndex, matchEnd), matched: true });
|
|
195
|
+
position = matchEnd;
|
|
196
|
+
matchIndex = lowerText.indexOf(lowerQuery, position);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (position < text.length) {
|
|
200
|
+
parts.push({ text: text.slice(position), matched: false });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return parts.length > 0 ? parts : [{ text, matched: false }];
|
|
204
|
+
},
|
|
205
|
+
setArrayDetailViewMode(mode) {
|
|
206
|
+
this.arrayDetailViewMode = mode;
|
|
207
|
+
if (mode === 'table') {
|
|
208
|
+
if (this.isCollapsed) {
|
|
209
|
+
this.isCollapsed = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
},
|
|
150
213
|
setDetailViewMode(mode) {
|
|
151
214
|
this.detailViewMode = mode;
|
|
152
|
-
|
|
215
|
+
|
|
153
216
|
// When switching to map view, expand the container and value so the map is visible
|
|
154
217
|
if (mode === 'map' && this.isGeoJsonGeometry) {
|
|
155
218
|
if (this.isCollapsed) {
|
|
@@ -220,6 +283,7 @@ module.exports = app => app.component('document-property', {
|
|
|
220
283
|
if (path.instance === 'Array') {
|
|
221
284
|
props.path = path;
|
|
222
285
|
props.schemaPaths = this.schemaPaths;
|
|
286
|
+
props.viewMode = this.arrayDetailViewMode === 'table' ? 'table' : 'json';
|
|
223
287
|
}
|
|
224
288
|
return props;
|
|
225
289
|
},
|
|
@@ -1,9 +1,144 @@
|
|
|
1
1
|
<div class="w-full">
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
<div v-if="!useTableEditor">
|
|
3
|
+
<ace-editor
|
|
4
|
+
:value="arrayStr"
|
|
5
|
+
@input="onEditorInput"
|
|
6
|
+
mode="json"
|
|
7
|
+
:line-numbers="true"
|
|
8
|
+
class="min-h-[300px]"
|
|
9
|
+
/>
|
|
10
|
+
</div>
|
|
11
|
+
<div v-else class="overflow-hidden rounded-lg border border-edge bg-surface">
|
|
12
|
+
<div class="border-b border-edge bg-slate-50/90 px-3 py-2.5">
|
|
13
|
+
<div class="flex flex-col gap-2 lg:flex-row lg:items-center lg:gap-4">
|
|
14
|
+
<div class="flex flex-wrap items-stretch gap-2">
|
|
15
|
+
<button
|
|
16
|
+
type="button"
|
|
17
|
+
class="rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-text shadow-sm hover:bg-primary-hover"
|
|
18
|
+
@click="addRow">
|
|
19
|
+
+ Row
|
|
20
|
+
</button>
|
|
21
|
+
<div class="flex min-w-0 max-w-full sm:max-w-md">
|
|
22
|
+
<input
|
|
23
|
+
id="edit-array-new-column"
|
|
24
|
+
v-model="newColumnKeyInput"
|
|
25
|
+
type="text"
|
|
26
|
+
class="min-w-0 flex-1 rounded-l-md border border-r-0 border-edge bg-surface px-2.5 py-1.5 text-xs font-mono text-content placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
27
|
+
placeholder="New field name"
|
|
28
|
+
autocomplete="off"
|
|
29
|
+
@keyup.enter="addColumn"
|
|
30
|
+
/>
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
class="shrink-0 rounded-r-md border border-edge bg-slate-100 px-3 py-1.5 text-xs font-semibold text-content hover:bg-slate-200"
|
|
34
|
+
@click="addColumn">
|
|
35
|
+
Add field
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<p class="text-[11px] leading-snug text-content-tertiary lg:ml-auto lg:max-w-[18rem] lg:text-right">
|
|
40
|
+
Each cell is JSON. Clear a cell to remove that field from the row.
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
<p v-if="newColumnKeyError" class="mt-2 text-xs font-medium text-red-600">{{ newColumnKeyError }}</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div v-if="arrayValue.length > 0" class="border-b border-edge bg-slate-50/50 px-3 py-2">
|
|
47
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
|
48
|
+
<div class="relative min-w-0 flex-1 sm:max-w-md">
|
|
49
|
+
<input
|
|
50
|
+
v-model="arraySearchQuery"
|
|
51
|
+
type="search"
|
|
52
|
+
autocomplete="off"
|
|
53
|
+
aria-label="Search rows in this array"
|
|
54
|
+
placeholder="Search rows…"
|
|
55
|
+
class="w-full rounded-md border border-edge bg-surface py-1.5 pl-2 pr-16 text-xs text-content placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
56
|
+
/>
|
|
57
|
+
<button
|
|
58
|
+
v-if="arraySearchNormalized"
|
|
59
|
+
type="button"
|
|
60
|
+
class="absolute right-1 top-1/2 -translate-y-1/2 rounded px-1.5 py-0.5 text-[11px] text-content-tertiary hover:bg-slate-200 hover:text-content"
|
|
61
|
+
title="Clear search"
|
|
62
|
+
@click="arraySearchQuery = ''">
|
|
63
|
+
Clear
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
<span v-if="arraySearchNormalized" class="shrink-0 text-xs text-content-tertiary">
|
|
67
|
+
{{ filteredTableRows.length }} of {{ arrayValue.length }} shown
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div v-if="arrayValue.length === 0" class="px-4 py-10 text-center text-sm text-content-secondary">
|
|
73
|
+
No rows yet. Use <span class="font-medium text-content">+ Row</span> or enter a field name and choose <span class="font-medium text-content">Add field</span>.
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div
|
|
77
|
+
v-else-if="filteredTableRows.length === 0"
|
|
78
|
+
class="px-4 py-8 text-center text-sm text-content-secondary">
|
|
79
|
+
No rows match "{{ arraySearchQuery.trim() }}".
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div v-else class="overflow-x-auto">
|
|
83
|
+
<table class="min-w-full text-left text-xs font-mono">
|
|
84
|
+
<thead class="border-b border-edge bg-slate-100 text-content-secondary">
|
|
85
|
+
<tr>
|
|
86
|
+
<th scope="col" class="w-24 border-r border-edge px-2 py-2 text-left text-[11px] font-semibold uppercase tracking-wide">
|
|
87
|
+
Actions
|
|
88
|
+
</th>
|
|
89
|
+
<th
|
|
90
|
+
v-for="key in tableColumnKeys"
|
|
91
|
+
:key="key"
|
|
92
|
+
scope="col"
|
|
93
|
+
class="border-r border-edge px-3 py-2 text-left text-xs font-semibold last:border-r-0 min-w-[8rem] whitespace-nowrap">
|
|
94
|
+
{{ key }}
|
|
95
|
+
</th>
|
|
96
|
+
</tr>
|
|
97
|
+
</thead>
|
|
98
|
+
<tbody class="divide-y divide-edge bg-surface">
|
|
99
|
+
<tr v-for="entry in filteredTableRows" :key="entry.index" class="hover:bg-slate-50/60">
|
|
100
|
+
<td class="align-middle border-r border-edge p-2">
|
|
101
|
+
<div class="flex flex-col gap-1">
|
|
102
|
+
<span class="text-center text-[10px] font-medium text-content-tertiary">#{{ entry.index }}</span>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
class="rounded border border-edge bg-surface px-2 py-1 text-[11px] font-medium hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-40"
|
|
106
|
+
:disabled="entry.index === 0"
|
|
107
|
+
title="Move row up"
|
|
108
|
+
@click="moveRow(entry.index, -1)">
|
|
109
|
+
Up
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
class="rounded border border-edge bg-surface px-2 py-1 text-[11px] font-medium hover:bg-slate-100 disabled:cursor-not-allowed disabled:opacity-40"
|
|
114
|
+
:disabled="entry.index === arrayValue.length - 1"
|
|
115
|
+
title="Move row down"
|
|
116
|
+
@click="moveRow(entry.index, 1)">
|
|
117
|
+
Down
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
class="rounded border border-red-200 bg-surface px-2 py-1 text-[11px] font-medium text-red-700 hover:bg-red-50"
|
|
122
|
+
title="Remove this row"
|
|
123
|
+
@click="removeRow(entry.index)">
|
|
124
|
+
Remove
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
</td>
|
|
128
|
+
<td
|
|
129
|
+
v-for="key in tableColumnKeys"
|
|
130
|
+
:key="key"
|
|
131
|
+
class="max-w-md min-w-[8rem] border-r border-edge p-1.5 align-top last:border-r-0">
|
|
132
|
+
<textarea
|
|
133
|
+
rows="2"
|
|
134
|
+
class="w-full resize-y rounded border border-edge bg-surface px-2 py-1.5 text-xs text-content focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
135
|
+
:value="serializeCell(entry.item[key])"
|
|
136
|
+
@blur="onCellChange(entry.index, key, $event.target.value)"
|
|
137
|
+
></textarea>
|
|
138
|
+
</td>
|
|
139
|
+
</tr>
|
|
140
|
+
</tbody>
|
|
141
|
+
</table>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const template = require('./edit-array.html');
|
|
4
4
|
const { BSON } = require('mongodb/lib/bson');
|
|
5
|
+
const { isArrayOfObjects, unionKeysForArrayOfObjects, arrayItemMatchesSearch } = require('../array-utils');
|
|
5
6
|
|
|
6
7
|
const ObjectId = new Proxy(BSON.ObjectId, {
|
|
7
8
|
apply(target, thisArg, argumentsList) {
|
|
@@ -12,15 +13,44 @@ const ObjectId = new Proxy(BSON.ObjectId, {
|
|
|
12
13
|
|
|
13
14
|
module.exports = app => app.component('edit-array', {
|
|
14
15
|
template: template,
|
|
15
|
-
props:
|
|
16
|
+
props: {
|
|
17
|
+
value: {},
|
|
18
|
+
path: {},
|
|
19
|
+
schemaPaths: {},
|
|
20
|
+
/** 'json' (ACE) or 'table' for arrays of plain objects */
|
|
21
|
+
viewMode: {
|
|
22
|
+
type: String,
|
|
23
|
+
default: 'json'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
16
26
|
data() {
|
|
17
27
|
return {
|
|
18
|
-
arrayValue: []
|
|
28
|
+
arrayValue: [],
|
|
29
|
+
newColumnKeyInput: '',
|
|
30
|
+
newColumnKeyError: '',
|
|
31
|
+
arraySearchQuery: ''
|
|
19
32
|
};
|
|
20
33
|
},
|
|
21
34
|
computed: {
|
|
22
35
|
arrayStr() {
|
|
23
36
|
return JSON.stringify(this.arrayValue, null, 2);
|
|
37
|
+
},
|
|
38
|
+
useTableEditor() {
|
|
39
|
+
return this.viewMode === 'table' && isArrayOfObjects(this.arrayValue);
|
|
40
|
+
},
|
|
41
|
+
tableColumnKeys() {
|
|
42
|
+
return unionKeysForArrayOfObjects(this.arrayValue);
|
|
43
|
+
},
|
|
44
|
+
arraySearchNormalized() {
|
|
45
|
+
return (this.arraySearchQuery || '').trim().toLowerCase();
|
|
46
|
+
},
|
|
47
|
+
filteredTableRows() {
|
|
48
|
+
if (!Array.isArray(this.arrayValue)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return this.arrayValue
|
|
52
|
+
.map((item, index) => ({ item, index }))
|
|
53
|
+
.filter(({ item }) => arrayItemMatchesSearch(item, this.arraySearchNormalized));
|
|
24
54
|
}
|
|
25
55
|
},
|
|
26
56
|
methods: {
|
|
@@ -51,6 +81,78 @@ module.exports = app => app.component('edit-array', {
|
|
|
51
81
|
} catch (err) {
|
|
52
82
|
this.$emit('error', err);
|
|
53
83
|
}
|
|
84
|
+
},
|
|
85
|
+
serializeCell(value) {
|
|
86
|
+
if (value === undefined) {
|
|
87
|
+
return '';
|
|
88
|
+
}
|
|
89
|
+
return JSON.stringify(value);
|
|
90
|
+
},
|
|
91
|
+
onCellChange(rowIndex, key, raw) {
|
|
92
|
+
const row = this.arrayValue[rowIndex];
|
|
93
|
+
if (row == null || typeof row !== 'object') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const trimmed = raw.trim();
|
|
97
|
+
if (trimmed === '') {
|
|
98
|
+
delete row[key];
|
|
99
|
+
this.emitUpdate();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
row[key] = JSON.parse(trimmed);
|
|
104
|
+
this.emitUpdate();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.$emit('error', err);
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
addRow() {
|
|
110
|
+
const row = {};
|
|
111
|
+
for (const k of this.tableColumnKeys) {
|
|
112
|
+
row[k] = null;
|
|
113
|
+
}
|
|
114
|
+
this.arrayValue.push(row);
|
|
115
|
+
this.emitUpdate();
|
|
116
|
+
},
|
|
117
|
+
addColumn() {
|
|
118
|
+
const key = (this.newColumnKeyInput || '').trim();
|
|
119
|
+
this.newColumnKeyError = '';
|
|
120
|
+
if (!key) {
|
|
121
|
+
this.newColumnKeyError = 'Enter a column name.';
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (this.tableColumnKeys.includes(key)) {
|
|
125
|
+
this.newColumnKeyError = 'That column already exists.';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!Array.isArray(this.arrayValue)) {
|
|
129
|
+
this.arrayValue = [];
|
|
130
|
+
}
|
|
131
|
+
if (this.arrayValue.length === 0) {
|
|
132
|
+
this.arrayValue.push({ [key]: null });
|
|
133
|
+
} else {
|
|
134
|
+
for (const row of this.arrayValue) {
|
|
135
|
+
if (row != null && typeof row === 'object' && !Array.isArray(row) && row.constructor === Object) {
|
|
136
|
+
row[key] = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
this.newColumnKeyInput = '';
|
|
141
|
+
this.emitUpdate();
|
|
142
|
+
},
|
|
143
|
+
removeRow(index) {
|
|
144
|
+
this.arrayValue.splice(index, 1);
|
|
145
|
+
this.emitUpdate();
|
|
146
|
+
},
|
|
147
|
+
moveRow(index, delta) {
|
|
148
|
+
const j = index + delta;
|
|
149
|
+
if (j < 0 || j >= this.arrayValue.length) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const copy = this.arrayValue[index];
|
|
153
|
+
this.arrayValue.splice(index, 1);
|
|
154
|
+
this.arrayValue.splice(j, 0, copy);
|
|
155
|
+
this.emitUpdate();
|
|
54
156
|
}
|
|
55
157
|
},
|
|
56
158
|
mounted() {
|
package/frontend/src/index.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
appendCSS(require('vue-toastification/dist/index.css'));
|
|
26
26
|
|
|
27
27
|
const TRACKED_RECENT_PAGE_ROUTE_NAMES = new Set(['model', 'document', 'dashboard', 'chat']);
|
|
28
|
+
const CHAT_ROUTE_NAMES = new Set(['chat index', 'chat']);
|
|
28
29
|
|
|
29
30
|
function formatHistoryLabel(route) {
|
|
30
31
|
if (!route || typeof route.path !== 'string') {
|
|
@@ -240,8 +241,12 @@ app.component('app-component', {
|
|
|
240
241
|
window.localStorage.setItem('_mongooseStudioAccessToken', accessToken._id);
|
|
241
242
|
|
|
242
243
|
try {
|
|
243
|
-
const { nodeEnv } = await
|
|
244
|
+
const [{ nodeEnv }, capabilities] = await Promise.all([
|
|
245
|
+
api.status(),
|
|
246
|
+
loadStudioCapabilities()
|
|
247
|
+
]);
|
|
244
248
|
this.nodeEnv = nodeEnv;
|
|
249
|
+
this.capabilities = capabilities;
|
|
245
250
|
} catch (err) {
|
|
246
251
|
this.authError = 'Error connecting to Mongoose Studio API: ' + err.response?.data?.message ?? err.message;
|
|
247
252
|
this.status = 'loaded';
|
|
@@ -261,12 +266,14 @@ app.component('app-component', {
|
|
|
261
266
|
const { user, roles } = await mothership.me();
|
|
262
267
|
|
|
263
268
|
try {
|
|
264
|
-
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
269
|
+
const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
|
|
265
270
|
api.status(),
|
|
266
|
-
api.Model.listModels()
|
|
271
|
+
api.Model.listModels(),
|
|
272
|
+
loadStudioCapabilities()
|
|
267
273
|
]);
|
|
268
274
|
this.nodeEnv = nodeEnv;
|
|
269
275
|
this.modelSchemaPaths = modelSchemaPaths;
|
|
276
|
+
this.capabilities = capabilities;
|
|
270
277
|
} catch (err) {
|
|
271
278
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
272
279
|
this.status = 'loaded';
|
|
@@ -279,12 +286,14 @@ app.component('app-component', {
|
|
|
279
286
|
}
|
|
280
287
|
} else {
|
|
281
288
|
try {
|
|
282
|
-
const [{ nodeEnv }, { modelSchemaPaths }] = await Promise.all([
|
|
289
|
+
const [{ nodeEnv }, { modelSchemaPaths }, capabilities] = await Promise.all([
|
|
283
290
|
api.status(),
|
|
284
|
-
api.Model.listModels()
|
|
291
|
+
api.Model.listModels(),
|
|
292
|
+
loadStudioCapabilities()
|
|
285
293
|
]);
|
|
286
294
|
this.nodeEnv = nodeEnv;
|
|
287
295
|
this.modelSchemaPaths = modelSchemaPaths;
|
|
296
|
+
this.capabilities = capabilities;
|
|
288
297
|
} catch (err) {
|
|
289
298
|
this.authError = 'Error connecting to Mongoose Studio API: ' + (err.response?.data?.message ?? err.message);
|
|
290
299
|
}
|
|
@@ -299,6 +308,7 @@ app.component('app-component', {
|
|
|
299
308
|
const nodeEnv = Vue.ref(null);
|
|
300
309
|
const authError = Vue.ref(null);
|
|
301
310
|
const modelSchemaPaths = Vue.ref(null);
|
|
311
|
+
const capabilities = Vue.ref(null);
|
|
302
312
|
|
|
303
313
|
const state = Vue.reactive({
|
|
304
314
|
user,
|
|
@@ -306,7 +316,8 @@ app.component('app-component', {
|
|
|
306
316
|
status,
|
|
307
317
|
nodeEnv,
|
|
308
318
|
authError,
|
|
309
|
-
modelSchemaPaths
|
|
319
|
+
modelSchemaPaths,
|
|
320
|
+
capabilities
|
|
310
321
|
});
|
|
311
322
|
Vue.provide('state', state);
|
|
312
323
|
|
|
@@ -358,13 +369,13 @@ router.beforeEach((to, from, next) => {
|
|
|
358
369
|
next();
|
|
359
370
|
});
|
|
360
371
|
|
|
361
|
-
router.beforeEach((to, from, next) => {
|
|
362
|
-
if (
|
|
372
|
+
router.beforeEach(async(to, from, next) => {
|
|
373
|
+
if (CHAT_ROUTE_NAMES.has(to.name) && !(await supportsAI())) {
|
|
363
374
|
next({ name: 'root' });
|
|
364
375
|
return;
|
|
365
376
|
}
|
|
366
377
|
|
|
367
|
-
if (to.name === 'root' && window.state
|
|
378
|
+
if (to.name === 'root' && window.state?.roles && window.state.roles[0] === 'dashboards') {
|
|
368
379
|
return next({ name: 'dashboards' });
|
|
369
380
|
} else {
|
|
370
381
|
next();
|
|
@@ -379,3 +390,28 @@ app.config.globalProperties = { format, arrayUtils, $toast: toast };
|
|
|
379
390
|
app.use(router);
|
|
380
391
|
|
|
381
392
|
app.mount('#content');
|
|
393
|
+
|
|
394
|
+
async function getStudioCapabilities() {
|
|
395
|
+
if (window.__mongooseStudioCapabilitiesPromise == null) {
|
|
396
|
+
window.__mongooseStudioCapabilitiesPromise = api.getCapabilities().catch(err => {
|
|
397
|
+
window.__mongooseStudioCapabilitiesPromise = null;
|
|
398
|
+
throw err;
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return window.__mongooseStudioCapabilitiesPromise;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function loadStudioCapabilities() {
|
|
405
|
+
try {
|
|
406
|
+
return await getStudioCapabilities();
|
|
407
|
+
} catch (err) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function supportsAI() {
|
|
413
|
+
if (window.state?.capabilities == null) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
return window.state.capabilities.supportsAI !== false;
|
|
417
|
+
}
|