@mongoosejs/studio 0.5.4 → 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/public/app.js +639 -99
- 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/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/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/package.json +2 -1
|
@@ -3,23 +3,107 @@
|
|
|
3
3
|
Empty array
|
|
4
4
|
</div>
|
|
5
5
|
|
|
6
|
-
<div v-else
|
|
6
|
+
<div v-else>
|
|
7
|
+
<div class="mb-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
|
|
8
|
+
<div class="relative min-w-0 flex-1">
|
|
9
|
+
<input
|
|
10
|
+
v-model="arraySearchQuery"
|
|
11
|
+
type="text"
|
|
12
|
+
autocomplete="off"
|
|
13
|
+
aria-label="Search in this array"
|
|
14
|
+
placeholder="Search in this array…"
|
|
15
|
+
class="w-full rounded-md border border-edge bg-surface py-1.5 pl-2 pr-8 text-xs text-content placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
|
16
|
+
/>
|
|
17
|
+
<button
|
|
18
|
+
v-if="arraySearchNormalized"
|
|
19
|
+
type="button"
|
|
20
|
+
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-100 hover:text-content"
|
|
21
|
+
title="Clear search"
|
|
22
|
+
@click="arraySearchQuery = ''">
|
|
23
|
+
Clear
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
<span v-if="arraySearchNormalized" class="shrink-0 text-xs text-content-tertiary">
|
|
27
|
+
{{ filteredArrayRows.length }} of {{ arrayValue.length }} shown
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
7
31
|
<div
|
|
8
|
-
v-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
v-if="filteredArrayRows.length === 0"
|
|
33
|
+
class="rounded-md border border-dashed border-edge py-6 text-center text-xs text-content-tertiary">
|
|
34
|
+
No items match "{{ arraySearchQuery.trim() }}".
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div v-else-if="showAsTable" class="mt-2 overflow-x-auto rounded-md border border-edge">
|
|
38
|
+
<table class="min-w-full text-left text-xs font-mono text-content">
|
|
39
|
+
<thead class="bg-slate-100 text-content-secondary border-b border-edge">
|
|
40
|
+
<tr>
|
|
41
|
+
<th scope="col" class="sticky left-0 z-[1] bg-slate-100 px-2 py-2 font-semibold border-r border-edge w-10 text-center">#</th>
|
|
42
|
+
<th
|
|
43
|
+
v-for="key in tableColumnKeys"
|
|
44
|
+
:key="key"
|
|
45
|
+
scope="col"
|
|
46
|
+
class="px-3 py-2 font-semibold whitespace-nowrap border-r border-edge last:border-r-0">
|
|
47
|
+
{{ key }}
|
|
48
|
+
</th>
|
|
49
|
+
</tr>
|
|
50
|
+
</thead>
|
|
51
|
+
<tbody class="divide-y divide-edge bg-surface">
|
|
52
|
+
<tr v-for="row in displayedArrayRows" :key="row.index" class="hover:bg-slate-50/80">
|
|
53
|
+
<td class="sticky left-0 z-[1] bg-inherit px-2 py-2 text-center text-content-tertiary border-r border-edge">{{ row.index }}</td>
|
|
54
|
+
<td
|
|
55
|
+
v-for="key in tableColumnKeys"
|
|
56
|
+
:key="key"
|
|
57
|
+
class="px-3 py-2 align-top break-words whitespace-pre-wrap max-w-md border-r border-edge last:border-r-0">
|
|
58
|
+
{{ Object.prototype.hasOwnProperty.call(row.item, key) ? arrayUtils.formatItemValue(row.item, key) : '' }}
|
|
59
|
+
</td>
|
|
60
|
+
</tr>
|
|
61
|
+
</tbody>
|
|
62
|
+
</table>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div v-else class="mt-2">
|
|
66
|
+
<div
|
|
67
|
+
v-for="row in displayedArrayRows"
|
|
68
|
+
:key="row.index"
|
|
69
|
+
:title="'Index: ' + row.index"
|
|
70
|
+
class="mb-1.5 last:mb-0 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-slate-100 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-slate-100 hover:shadow-lg hover:-translate-y-0.5">
|
|
71
|
+
<div class="absolute -left-3 top-1/2 -translate-y-1/2 w-5 h-5 bg-slate-100 text-content rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-slate-100">{{ row.index >= 1000 ? '1k+' : row.index }}</div>
|
|
72
|
+
<div v-if="arrayUtils.isObjectItem(row.item)" class="flex flex-col gap-1 mt-1 px-2">
|
|
73
|
+
<div
|
|
74
|
+
v-for="key in arrayUtils.getItemKeys(row.item)"
|
|
75
|
+
:key="key"
|
|
76
|
+
class="flex items-start gap-2 text-xs font-mono">
|
|
77
|
+
<span class="font-semibold text-gray-600 flex-shrink-0 min-w-[80px]">{{ key }}:</span>
|
|
78
|
+
<span class="text-gray-800 break-words whitespace-pre-wrap flex-1">{{ arrayUtils.formatItemValue(row.item, key) }}</span>
|
|
79
|
+
</div>
|
|
20
80
|
</div>
|
|
81
|
+
<div v-else class="text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1">{{ arrayUtils.formatValue(row.item) }}</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div
|
|
86
|
+
v-if="remainingArrayCount > 0"
|
|
87
|
+
class="mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-slate-100 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100">
|
|
88
|
+
<div class="text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1">
|
|
89
|
+
... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}
|
|
21
90
|
</div>
|
|
22
|
-
<div v-else class="text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1">{{ arrayUtils.formatValue(item) }}</div>
|
|
23
91
|
</div>
|
|
92
|
+
|
|
93
|
+
<button
|
|
94
|
+
v-if="canToggleRows"
|
|
95
|
+
@click="toggleExpansion"
|
|
96
|
+
class="mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5"
|
|
97
|
+
>
|
|
98
|
+
<svg
|
|
99
|
+
class="w-4 h-4"
|
|
100
|
+
:class="{ 'rotate-180': isExpanded }"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
viewBox="0 0 24 24">
|
|
104
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
105
|
+
</svg>
|
|
106
|
+
{{ expansionToggleLabel }}
|
|
107
|
+
</button>
|
|
24
108
|
</div>
|
|
25
109
|
</div>
|
|
@@ -1,16 +1,83 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const template = require('./detail-array.html');
|
|
4
|
+
const { isArrayOfObjects, unionKeysForArrayOfObjects, arrayItemMatchesSearch } = require('../array-utils');
|
|
4
5
|
|
|
5
6
|
module.exports = app => app.component('detail-array', {
|
|
6
7
|
template: template,
|
|
7
|
-
props:
|
|
8
|
+
props: {
|
|
9
|
+
value: {},
|
|
10
|
+
/** 'list' | 'table' — table applies to arrays of plain objects */
|
|
11
|
+
viewMode: {
|
|
12
|
+
type: String,
|
|
13
|
+
default: 'list'
|
|
14
|
+
},
|
|
15
|
+
truncate: {
|
|
16
|
+
type: Boolean,
|
|
17
|
+
default: false
|
|
18
|
+
},
|
|
19
|
+
initialLimit: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: 2
|
|
22
|
+
}
|
|
23
|
+
},
|
|
8
24
|
data() {
|
|
9
25
|
return {
|
|
10
|
-
arrayValue: []
|
|
26
|
+
arrayValue: [],
|
|
27
|
+
arraySearchQuery: '',
|
|
28
|
+
isExpanded: false
|
|
11
29
|
};
|
|
12
30
|
},
|
|
31
|
+
computed: {
|
|
32
|
+
effectiveViewMode() {
|
|
33
|
+
return this.viewMode === 'table' ? 'table' : 'list';
|
|
34
|
+
},
|
|
35
|
+
showAsTable() {
|
|
36
|
+
return this.effectiveViewMode === 'table' && isArrayOfObjects(this.arrayValue);
|
|
37
|
+
},
|
|
38
|
+
tableColumnKeys() {
|
|
39
|
+
return unionKeysForArrayOfObjects(this.arrayValue);
|
|
40
|
+
},
|
|
41
|
+
arraySearchNormalized() {
|
|
42
|
+
return (this.arraySearchQuery || '').trim().toLowerCase();
|
|
43
|
+
},
|
|
44
|
+
filteredArrayRows() {
|
|
45
|
+
if (!Array.isArray(this.arrayValue)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return this.arrayValue
|
|
49
|
+
.map((item, index) => ({ item, index }))
|
|
50
|
+
.filter(({ item }) => arrayItemMatchesSearch(item, this.arraySearchNormalized));
|
|
51
|
+
},
|
|
52
|
+
displayedArrayRows() {
|
|
53
|
+
if (!this.shouldLimitRows) {
|
|
54
|
+
return this.filteredArrayRows;
|
|
55
|
+
}
|
|
56
|
+
return this.filteredArrayRows.slice(0, this.initialLimit);
|
|
57
|
+
},
|
|
58
|
+
shouldLimitRows() {
|
|
59
|
+
return this.truncate
|
|
60
|
+
&& !this.isExpanded
|
|
61
|
+
&& !this.arraySearchNormalized
|
|
62
|
+
&& this.filteredArrayRows.length > this.initialLimit;
|
|
63
|
+
},
|
|
64
|
+
canToggleRows() {
|
|
65
|
+
return this.truncate && !this.arraySearchNormalized && this.arrayValue.length > this.initialLimit;
|
|
66
|
+
},
|
|
67
|
+
expansionToggleLabel() {
|
|
68
|
+
return this.isExpanded ? 'Show less' : `Show all ${this.arrayValue.length} items`;
|
|
69
|
+
},
|
|
70
|
+
remainingArrayCount() {
|
|
71
|
+
if (!this.shouldLimitRows) {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
return this.filteredArrayRows.length - this.displayedArrayRows.length;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
13
77
|
methods: {
|
|
78
|
+
toggleExpansion() {
|
|
79
|
+
this.isExpanded = !this.isExpanded;
|
|
80
|
+
},
|
|
14
81
|
initializeArray() {
|
|
15
82
|
if (this.value == null) {
|
|
16
83
|
this.arrayValue = [];
|
|
@@ -28,9 +95,12 @@ module.exports = app => app.component('detail-array', {
|
|
|
28
95
|
value: {
|
|
29
96
|
handler(newValue) {
|
|
30
97
|
this.initializeArray();
|
|
98
|
+
if (this.arrayValue.length <= this.initialLimit) {
|
|
99
|
+
this.isExpanded = false;
|
|
100
|
+
}
|
|
31
101
|
},
|
|
32
102
|
deep: true,
|
|
33
103
|
immediate: true
|
|
34
104
|
}
|
|
35
105
|
}
|
|
36
|
-
});
|
|
106
|
+
});
|
|
@@ -120,6 +120,13 @@
|
|
|
120
120
|
>
|
|
121
121
|
Copy Document
|
|
122
122
|
</button>
|
|
123
|
+
<button
|
|
124
|
+
@click="openValidationModal()"
|
|
125
|
+
type="button"
|
|
126
|
+
class="flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100"
|
|
127
|
+
>
|
|
128
|
+
Validate
|
|
129
|
+
</button>
|
|
123
130
|
<button
|
|
124
131
|
@click="openScriptDrawer()"
|
|
125
132
|
type="button"
|
|
@@ -222,6 +229,13 @@
|
|
|
222
229
|
>
|
|
223
230
|
Copy Document
|
|
224
231
|
</button>
|
|
232
|
+
<button
|
|
233
|
+
@click="openValidationModal()"
|
|
234
|
+
type="button"
|
|
235
|
+
class="flex items-center px-4 py-2 text-sm text-content-secondary hover:bg-green-100"
|
|
236
|
+
>
|
|
237
|
+
Validate
|
|
238
|
+
</button>
|
|
225
239
|
<button
|
|
226
240
|
@click="openScriptDrawer()"
|
|
227
241
|
type="button"
|
|
@@ -280,6 +294,43 @@
|
|
|
280
294
|
<clone-document :currentModel="model" :doc="document" :schemaPaths="schemaPaths" @close="showClonedDocument"></clone-document>
|
|
281
295
|
</template>
|
|
282
296
|
</modal>
|
|
297
|
+
<modal v-if="shouldShowValidationModal">
|
|
298
|
+
<template v-slot:body>
|
|
299
|
+
<div class="modal-exit" @click="shouldShowValidationModal = false;">×</div>
|
|
300
|
+
<div class="text-xl font-bold mb-2">Validate Document</div>
|
|
301
|
+
<div v-if="!validationResult" class="space-y-3">
|
|
302
|
+
<div class="h-4 w-32 animate-pulse rounded bg-muted"></div>
|
|
303
|
+
<div class="h-20 w-full animate-pulse rounded bg-muted"></div>
|
|
304
|
+
</div>
|
|
305
|
+
<div v-else class="space-y-3">
|
|
306
|
+
<div
|
|
307
|
+
class="rounded px-3 py-2 text-sm font-semibold"
|
|
308
|
+
:class="validationResult.valid ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'"
|
|
309
|
+
>
|
|
310
|
+
{{ validationResult.valid ? 'Document is valid' : 'Document is invalid' }}
|
|
311
|
+
</div>
|
|
312
|
+
<div v-if="!validationResult.valid" class="rounded border border-edge bg-muted p-3 text-sm text-content-secondary">
|
|
313
|
+
{{ validationResult.error }}
|
|
314
|
+
</div>
|
|
315
|
+
<div v-if="validationResult?.errors && Object.keys(validationResult.errors).length > 0" class="rounded border border-edge">
|
|
316
|
+
<div class="border-b border-edge px-3 py-2 text-sm font-semibold text-content-secondary">Invalid Paths</div>
|
|
317
|
+
<div class="max-h-72 overflow-auto">
|
|
318
|
+
<div
|
|
319
|
+
v-for="[path, pathError] in Object.entries(validationResult.errors)"
|
|
320
|
+
:key="path"
|
|
321
|
+
class="grid gap-2 border-b border-edge px-3 py-2 text-sm last:border-b-0 md:grid-cols-[minmax(160px,220px)_1fr]"
|
|
322
|
+
>
|
|
323
|
+
<div class="font-mono text-content">{{ path }}</div>
|
|
324
|
+
<div class="text-content-secondary">
|
|
325
|
+
<div>{{ pathError.message }}</div>
|
|
326
|
+
<div v-if="pathError.kind" class="mt-1 text-xs text-content-tertiary">Kind: {{ pathError.kind }}</div>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</template>
|
|
333
|
+
</modal>
|
|
283
334
|
</div>
|
|
284
335
|
<execute-script
|
|
285
336
|
:visible="scriptDrawerOpen"
|
|
@@ -26,6 +26,8 @@ module.exports = app => app.component('document', {
|
|
|
26
26
|
shouldShowConfirmModal: false,
|
|
27
27
|
shouldShowDeleteModal: false,
|
|
28
28
|
shouldShowCloneModal: false,
|
|
29
|
+
shouldShowValidationModal: false,
|
|
30
|
+
validationResult: null,
|
|
29
31
|
previousQuery: null,
|
|
30
32
|
lastUpdatedAt: null,
|
|
31
33
|
isRefreshing: false,
|
|
@@ -351,6 +353,17 @@ module.exports = app => app.component('document', {
|
|
|
351
353
|
this.desktopMenuOpen = false;
|
|
352
354
|
this.mobileMenuOpen = false;
|
|
353
355
|
},
|
|
356
|
+
async openValidationModal() {
|
|
357
|
+
this.desktopMenuOpen = false;
|
|
358
|
+
this.mobileMenuOpen = false;
|
|
359
|
+
this.shouldShowValidationModal = true;
|
|
360
|
+
this.validationResult = null;
|
|
361
|
+
const { result } = await api.Model.validateDocument({
|
|
362
|
+
model: this.model,
|
|
363
|
+
documentId: this.documentId
|
|
364
|
+
});
|
|
365
|
+
this.validationResult = result;
|
|
366
|
+
},
|
|
354
367
|
closeScriptDrawer() {
|
|
355
368
|
this.scriptDrawerOpen = false;
|
|
356
369
|
},
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
:editting="editting"
|
|
56
56
|
:changes="changes"
|
|
57
57
|
:highlight="true"
|
|
58
|
+
:searchQuery="searchQuery"
|
|
58
59
|
:invalid="invalid"></document-property>
|
|
59
60
|
</div>
|
|
60
61
|
|
|
@@ -67,7 +68,7 @@
|
|
|
67
68
|
<!-- Virtual Field Header (Always Visible) -->
|
|
68
69
|
<div
|
|
69
70
|
@click="toggleVirtualField(path.name)"
|
|
70
|
-
class="p-3 bg-
|
|
71
|
+
class="p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out"
|
|
71
72
|
>
|
|
72
73
|
<div class="flex items-center">
|
|
73
74
|
<svg
|
|
@@ -79,7 +80,12 @@
|
|
|
79
80
|
>
|
|
80
81
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
81
82
|
</svg>
|
|
82
|
-
<span class="font-medium text-content">
|
|
83
|
+
<span class="font-medium text-content">
|
|
84
|
+
<span
|
|
85
|
+
v-for="(part, index) in getSearchMatchParts(path.name)"
|
|
86
|
+
:key="index"
|
|
87
|
+
:class="{ 'font-bold': part.matched }">{{ part.text }}</span>
|
|
88
|
+
</span>
|
|
83
89
|
<span v-if="path.isVirtual" class="ml-2 text-sm text-purple-600">(virtual - {{getVirtualFieldType(path)}})</span>
|
|
84
90
|
<span v-else class="ml-2 text-sm text-blue-600">(user-added - {{getVirtualFieldType(path)}})</span>
|
|
85
91
|
</div>
|
|
@@ -109,6 +115,7 @@
|
|
|
109
115
|
:editting="editting"
|
|
110
116
|
:changes="changes"
|
|
111
117
|
:highlight="false"
|
|
118
|
+
:searchQuery="searchQuery"
|
|
112
119
|
:invalid="invalid"></document-property>
|
|
113
120
|
</div>
|
|
114
121
|
|
|
@@ -121,7 +128,7 @@
|
|
|
121
128
|
<!-- Virtual Field Header (Always Visible) -->
|
|
122
129
|
<div
|
|
123
130
|
@click="toggleVirtualField(path.name)"
|
|
124
|
-
class="p-3 bg-
|
|
131
|
+
class="p-3 bg-slate-100 hover:bg-muted cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out"
|
|
125
132
|
>
|
|
126
133
|
<div class="flex items-center">
|
|
127
134
|
<svg
|
|
@@ -286,6 +286,34 @@ module.exports = app => app.component('document-details', {
|
|
|
286
286
|
|
|
287
287
|
return this.virtualSearchMatchSet.has(virtual.name);
|
|
288
288
|
},
|
|
289
|
+
getSearchMatchParts(text) {
|
|
290
|
+
const normalizedQuery = typeof this.searchQuery === 'string' ? this.searchQuery.trim() : '';
|
|
291
|
+
if (!normalizedQuery) {
|
|
292
|
+
return [{ text, matched: false }];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const lowerText = String(text).toLowerCase();
|
|
296
|
+
const lowerQuery = normalizedQuery.toLowerCase();
|
|
297
|
+
const parts = [];
|
|
298
|
+
let position = 0;
|
|
299
|
+
let matchIndex = lowerText.indexOf(lowerQuery, position);
|
|
300
|
+
|
|
301
|
+
while (matchIndex !== -1) {
|
|
302
|
+
if (matchIndex > position) {
|
|
303
|
+
parts.push({ text: text.slice(position, matchIndex), matched: false });
|
|
304
|
+
}
|
|
305
|
+
const matchEnd = matchIndex + normalizedQuery.length;
|
|
306
|
+
parts.push({ text: text.slice(matchIndex, matchEnd), matched: true });
|
|
307
|
+
position = matchEnd;
|
|
308
|
+
matchIndex = lowerText.indexOf(lowerQuery, position);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (position < text.length) {
|
|
312
|
+
parts.push({ text: text.slice(position), matched: false });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return parts.length > 0 ? parts : [{ text, matched: false }];
|
|
316
|
+
},
|
|
289
317
|
isVirtualFieldCollapsed(fieldName) {
|
|
290
318
|
return this.collapsedVirtuals.has(fieldName);
|
|
291
319
|
},
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
<!-- Collapsible Header -->
|
|
3
3
|
<div
|
|
4
4
|
@click="toggleCollapse"
|
|
5
|
-
class="p-1 cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out"
|
|
6
|
-
:class="{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-muted': !highlight }"
|
|
5
|
+
class="p-1 cursor-pointer flex items-center justify-between border-b border-edge bg-slate-100 hover:bg-muted transition-colors duration-200 ease-in-out"
|
|
7
6
|
style="overflow: visible; position: relative;"
|
|
8
7
|
>
|
|
9
8
|
<div class="flex items-center" >
|
|
@@ -16,11 +15,32 @@
|
|
|
16
15
|
>
|
|
17
16
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
18
17
|
</svg>
|
|
19
|
-
<span class="font-medium text-content">
|
|
18
|
+
<span class="font-medium text-content">
|
|
19
|
+
<span
|
|
20
|
+
v-for="(part, index) in pathNameParts"
|
|
21
|
+
:key="index"
|
|
22
|
+
:class="{ 'font-bold': part.matched }">{{ part.text }}</span>
|
|
23
|
+
</span>
|
|
20
24
|
<span class="ml-2 text-sm text-content-tertiary">({{(path.instance || 'unknown').toLowerCase()}})</span>
|
|
25
|
+
<div v-if="path.instance === 'Array' && canUseArrayTableView" class="ml-3 inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold">
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
class="rounded-full px-2.5 py-0.5 transition"
|
|
29
|
+
:class="arrayDetailViewMode === 'list' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'"
|
|
30
|
+
@click.stop="setArrayDetailViewMode('list')">
|
|
31
|
+
List
|
|
32
|
+
</button>
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
class="rounded-full px-2.5 py-0.5 transition"
|
|
36
|
+
:class="arrayDetailViewMode === 'table' ? 'bg-merlot-700 text-white shadow' : 'text-content-secondary hover:text-content'"
|
|
37
|
+
@click.stop="setArrayDetailViewMode('table')">
|
|
38
|
+
Table
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
21
41
|
<div v-if="isGeoJsonGeometry" class="ml-3 inline-flex items-center gap-2">
|
|
22
42
|
<div class="inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold">
|
|
23
|
-
|
|
43
|
+
<button
|
|
24
44
|
type="button"
|
|
25
45
|
class="rounded-full px-2.5 py-0.5 transition"
|
|
26
46
|
:class="detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'"
|
|
@@ -153,48 +173,20 @@
|
|
|
153
173
|
</component>
|
|
154
174
|
</div>
|
|
155
175
|
<div v-else>
|
|
156
|
-
<!--
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<div class="absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600">{{ index }}</div>
|
|
165
|
-
<div v-if="arrayUtils.isObjectItem(item)" class="flex flex-col gap-1 mt-1 px-2">
|
|
166
|
-
<div
|
|
167
|
-
v-for="key in arrayUtils.getItemKeys(item)"
|
|
168
|
-
:key="key"
|
|
169
|
-
class="flex items-start gap-2 text-xs font-mono">
|
|
170
|
-
<span class="font-semibold text-gray-600 flex-shrink-0 min-w-[80px]">{{ key }}:</span>
|
|
171
|
-
<span class="text-gray-800 break-words whitespace-pre-wrap flex-1">{{ arrayUtils.formatItemValue(item, key) }}</span>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
<div v-else class="text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1">{{ arrayUtils.formatValue(item) }}</div>
|
|
175
|
-
</div>
|
|
176
|
-
<div class="mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100">
|
|
177
|
-
<div class="text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1">
|
|
178
|
-
... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
<button
|
|
183
|
-
@click="toggleValueExpansion"
|
|
184
|
-
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5"
|
|
185
|
-
>
|
|
186
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
187
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
188
|
-
</svg>
|
|
189
|
-
Show all {{ arrayValue.length }} items
|
|
190
|
-
</button>
|
|
191
|
-
</div>
|
|
176
|
+
<!-- Arrays own their truncation and array search behavior in detail-array. -->
|
|
177
|
+
<component
|
|
178
|
+
v-if="isArray"
|
|
179
|
+
:is="getComponentForPath(path)"
|
|
180
|
+
:value="rawValue"
|
|
181
|
+
:viewMode="propertyDetailViewMode"
|
|
182
|
+
:truncate="needsTruncation"
|
|
183
|
+
@updated="renderedValue = $event"></component>
|
|
192
184
|
<!-- Non-array truncated view -->
|
|
193
185
|
<div v-else-if="shouldShowTruncated && !isArray" class="relative">
|
|
194
186
|
<div class="text-content-secondary whitespace-pre-wrap break-words font-mono text-sm">{{truncatedString}}</div>
|
|
195
187
|
<button
|
|
196
188
|
@click="toggleValueExpansion"
|
|
197
|
-
class="mt-2 text-
|
|
189
|
+
class="mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5"
|
|
198
190
|
>
|
|
199
191
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
200
192
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
@@ -207,11 +199,11 @@
|
|
|
207
199
|
<component
|
|
208
200
|
:is="getComponentForPath(path)"
|
|
209
201
|
:value="rawValue"
|
|
210
|
-
:viewMode="
|
|
202
|
+
:viewMode="propertyDetailViewMode"
|
|
211
203
|
@updated="renderedValue = $event"></component>
|
|
212
204
|
<button
|
|
213
205
|
@click="toggleValueExpansion"
|
|
214
|
-
class="mt-2 text-
|
|
206
|
+
class="mt-2 text-merlot-700 hover:text-merlot-900 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5"
|
|
215
207
|
>
|
|
216
208
|
<svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
217
209
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
@@ -224,7 +216,7 @@
|
|
|
224
216
|
<component
|
|
225
217
|
:is="getComponentForPath(path)"
|
|
226
218
|
:value="rawValue"
|
|
227
|
-
:viewMode="
|
|
219
|
+
:viewMode="propertyDetailViewMode"
|
|
228
220
|
@updated="renderedValue = $event"></component>
|
|
229
221
|
</div>
|
|
230
222
|
</div>
|