@mongoosejs/studio 0.3.3 → 0.3.4
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 +16 -7
- package/backend/actions/Model/getDocumentsStream.js +16 -9
- package/backend/actions/Model/getSuggestedProjection.js +33 -0
- package/backend/actions/Model/index.js +1 -0
- package/backend/authorize.js +1 -0
- package/backend/helpers/getSuggestedProjection.js +37 -0
- package/backend/helpers/parseFieldsParam.js +36 -0
- package/frontend/public/app.js +690 -172
- package/frontend/public/dark-theme.css +166 -0
- package/frontend/public/theme-variables.css +30 -0
- package/frontend/public/tw.css +110 -46
- package/frontend/src/api.js +7 -1
- package/frontend/src/dashboard/dashboard.js +1 -1
- package/frontend/src/list-mixed/list-mixed.html +0 -1
- package/frontend/src/list-string/list-string.html +0 -1
- package/frontend/src/models/models.css +25 -90
- package/frontend/src/models/models.html +213 -53
- package/frontend/src/models/models.js +611 -132
- package/frontend/src/navbar/navbar.js +1 -1
- package/frontend/src/task-by-name/task-by-name.html +11 -11
- package/frontend/src/task-single/task-single.html +4 -4
- package/frontend/src/tasks/task-details/task-details.html +6 -6
- package/package.json +1 -1
|
@@ -11,11 +11,66 @@ const appendCSS = require('../appendCSS');
|
|
|
11
11
|
appendCSS(require('./models.css'));
|
|
12
12
|
|
|
13
13
|
const limit = 20;
|
|
14
|
+
const DEFAULT_FIRST_N_FIELDS = 6;
|
|
14
15
|
const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
|
|
15
16
|
const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
|
|
17
|
+
const PROJECTION_STORAGE_KEY_PREFIX = 'studio:model-projection:';
|
|
18
|
+
const SHOW_ROW_NUMBERS_STORAGE_KEY = 'studio:model-show-row-numbers';
|
|
19
|
+
const PROJECTION_MODE_QUERY_KEY = 'projectionMode';
|
|
16
20
|
const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
|
|
17
21
|
const MAX_RECENT_MODELS = 4;
|
|
18
22
|
|
|
23
|
+
/** Parse `fields` from the route (JSON array or inclusion projection object only). */
|
|
24
|
+
function parseFieldsQueryParam(fields) {
|
|
25
|
+
if (fields == null || fields === '') {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
const s = typeof fields === 'string' ? fields : String(fields);
|
|
29
|
+
const trimmed = s.trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(trimmed);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(parsed)) {
|
|
40
|
+
return parsed.map(x => String(x).trim()).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
if (parsed != null && typeof parsed === 'object') {
|
|
43
|
+
return Object.keys(parsed).filter(k =>
|
|
44
|
+
Object.prototype.hasOwnProperty.call(parsed, k) && parsed[k]
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Pass through a valid JSON `fields` string for Model.getDocuments / getDocumentsStream. */
|
|
51
|
+
function normalizeFieldsParamForApi(fieldsStr) {
|
|
52
|
+
if (fieldsStr == null || fieldsStr === '') {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const s = typeof fieldsStr === 'string' ? fieldsStr : String(fieldsStr);
|
|
56
|
+
const trimmed = s.trim();
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(trimmed);
|
|
62
|
+
if (Array.isArray(parsed)) {
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
66
|
+
return trimmed;
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
19
74
|
module.exports = app => app.component('models', {
|
|
20
75
|
template: template,
|
|
21
76
|
props: ['model', 'user', 'roles'],
|
|
@@ -31,6 +86,7 @@ module.exports = app => app.component('models', {
|
|
|
31
86
|
mongoDBIndexes: [],
|
|
32
87
|
schemaIndexes: [],
|
|
33
88
|
status: 'loading',
|
|
89
|
+
loadingMore: false,
|
|
34
90
|
loadedAllDocs: false,
|
|
35
91
|
edittingDoc: null,
|
|
36
92
|
docEdits: null,
|
|
@@ -39,7 +95,10 @@ module.exports = app => app.component('models', {
|
|
|
39
95
|
searchText: '',
|
|
40
96
|
shouldShowExportModal: false,
|
|
41
97
|
shouldShowCreateModal: false,
|
|
42
|
-
|
|
98
|
+
projectionText: '',
|
|
99
|
+
isProjectionMenuSelected: false,
|
|
100
|
+
addFieldFilterText: '',
|
|
101
|
+
showAddFieldDropdown: false,
|
|
43
102
|
shouldShowIndexModal: false,
|
|
44
103
|
shouldShowCollectionInfoModal: false,
|
|
45
104
|
shouldShowUpdateMultipleModal: false,
|
|
@@ -61,27 +120,44 @@ module.exports = app => app.component('models', {
|
|
|
61
120
|
collectionInfo: null,
|
|
62
121
|
modelSearch: '',
|
|
63
122
|
recentlyViewedModels: [],
|
|
64
|
-
showModelSwitcher: false
|
|
123
|
+
showModelSwitcher: false,
|
|
124
|
+
showRowNumbers: true,
|
|
125
|
+
suppressScrollCheck: false,
|
|
126
|
+
scrollTopToRestore: null
|
|
65
127
|
}),
|
|
66
128
|
created() {
|
|
67
129
|
this.currentModel = this.model;
|
|
68
130
|
this.setSearchTextFromRoute();
|
|
69
131
|
this.loadOutputPreference();
|
|
70
132
|
this.loadSelectedGeoField();
|
|
133
|
+
this.loadShowRowNumbersPreference();
|
|
71
134
|
this.loadRecentlyViewedModels();
|
|
135
|
+
this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
|
|
72
136
|
},
|
|
73
137
|
beforeDestroy() {
|
|
74
|
-
document.removeEventListener('scroll', this.onScroll, true);
|
|
75
138
|
window.removeEventListener('popstate', this.onPopState, true);
|
|
76
139
|
document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
|
|
140
|
+
document.removeEventListener('click', this.onOutsideAddFieldDropdownClick, true);
|
|
77
141
|
document.documentElement.removeEventListener('studio-theme-changed', this.onStudioThemeChanged);
|
|
78
142
|
document.removeEventListener('keydown', this.onCtrlP, true);
|
|
79
143
|
this.destroyMap();
|
|
80
144
|
},
|
|
81
145
|
async mounted() {
|
|
146
|
+
// Persist scroll restoration across remounts.
|
|
147
|
+
// This component is keyed by `$route.fullPath`, so query changes (e.g. projection updates)
|
|
148
|
+
// recreate the component and reset scroll position.
|
|
149
|
+
if (typeof window !== 'undefined') {
|
|
150
|
+
if (typeof window.__studioModelsScrollTopToRestore === 'number') {
|
|
151
|
+
this.scrollTopToRestore = window.__studioModelsScrollTopToRestore;
|
|
152
|
+
}
|
|
153
|
+
if (window.__studioModelsSuppressScrollCheck === true) {
|
|
154
|
+
this.suppressScrollCheck = true;
|
|
155
|
+
}
|
|
156
|
+
delete window.__studioModelsScrollTopToRestore;
|
|
157
|
+
delete window.__studioModelsSuppressScrollCheck;
|
|
158
|
+
}
|
|
159
|
+
|
|
82
160
|
window.pageState = this;
|
|
83
|
-
this.onScroll = () => this.checkIfScrolledToBottom();
|
|
84
|
-
document.addEventListener('scroll', this.onScroll, true);
|
|
85
161
|
this.onPopState = () => this.initSearchFromUrl();
|
|
86
162
|
window.addEventListener('popstate', this.onPopState, true);
|
|
87
163
|
this.onOutsideActionsMenuClick = event => {
|
|
@@ -93,7 +169,18 @@ module.exports = app => app.component('models', {
|
|
|
93
169
|
this.closeActionsMenu();
|
|
94
170
|
}
|
|
95
171
|
};
|
|
172
|
+
this.onOutsideAddFieldDropdownClick = event => {
|
|
173
|
+
if (!this.showAddFieldDropdown) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const container = this.$refs.addFieldContainer;
|
|
177
|
+
if (container && !container.contains(event.target)) {
|
|
178
|
+
this.showAddFieldDropdown = false;
|
|
179
|
+
this.addFieldFilterText = '';
|
|
180
|
+
}
|
|
181
|
+
};
|
|
96
182
|
document.addEventListener('click', this.onOutsideActionsMenuClick, true);
|
|
183
|
+
document.addEventListener('click', this.onOutsideAddFieldDropdownClick, true);
|
|
97
184
|
this.onStudioThemeChanged = () => this.updateMapTileLayer();
|
|
98
185
|
document.documentElement.addEventListener('studio-theme-changed', this.onStudioThemeChanged);
|
|
99
186
|
this.onCtrlP = (event) => {
|
|
@@ -104,6 +191,8 @@ module.exports = app => app.component('models', {
|
|
|
104
191
|
};
|
|
105
192
|
document.addEventListener('keydown', this.onCtrlP, true);
|
|
106
193
|
this.query = Object.assign({}, this.$route.query);
|
|
194
|
+
// Keep UI mode in sync with the URL on remounts.
|
|
195
|
+
this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
|
|
107
196
|
const { models, readyState } = await api.Model.listModels();
|
|
108
197
|
this.models = models;
|
|
109
198
|
await this.loadModelCounts();
|
|
@@ -119,8 +208,27 @@ module.exports = app => app.component('models', {
|
|
|
119
208
|
}
|
|
120
209
|
|
|
121
210
|
await this.initSearchFromUrl();
|
|
211
|
+
if (this.isProjectionMenuSelected && this.outputType === 'map') {
|
|
212
|
+
// Projection input is not rendered in map view.
|
|
213
|
+
this.setOutputType('json');
|
|
214
|
+
}
|
|
215
|
+
this.$nextTick(() => {
|
|
216
|
+
if (!this.isProjectionMenuSelected) return;
|
|
217
|
+
const input = this.$refs.projectionInput;
|
|
218
|
+
if (input && typeof input.focus === 'function') {
|
|
219
|
+
input.focus();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
122
222
|
},
|
|
123
223
|
watch: {
|
|
224
|
+
model(newModel) {
|
|
225
|
+
if (newModel !== this.currentModel) {
|
|
226
|
+
this.currentModel = newModel;
|
|
227
|
+
if (this.currentModel != null) {
|
|
228
|
+
this.initSearchFromUrl();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
},
|
|
124
232
|
documents: {
|
|
125
233
|
handler() {
|
|
126
234
|
if (this.outputType === 'map' && this.mapInstance) {
|
|
@@ -225,6 +333,19 @@ module.exports = app => app.component('models', {
|
|
|
225
333
|
}
|
|
226
334
|
|
|
227
335
|
return geoFields;
|
|
336
|
+
},
|
|
337
|
+
availablePathsToAdd() {
|
|
338
|
+
const currentPaths = new Set(this.filteredPaths.map(p => p.path));
|
|
339
|
+
return this.schemaPaths.filter(p => !currentPaths.has(p.path));
|
|
340
|
+
},
|
|
341
|
+
filteredPathsToAdd() {
|
|
342
|
+
const available = this.availablePathsToAdd;
|
|
343
|
+
const query = (this.addFieldFilterText || '').trim().toLowerCase();
|
|
344
|
+
if (!query) return available;
|
|
345
|
+
return available.filter(p => p.path.toLowerCase().includes(query));
|
|
346
|
+
},
|
|
347
|
+
tableDisplayPaths() {
|
|
348
|
+
return this.filteredPaths.length > 0 ? this.filteredPaths : this.schemaPaths;
|
|
228
349
|
}
|
|
229
350
|
},
|
|
230
351
|
methods: {
|
|
@@ -289,6 +410,24 @@ module.exports = app => app.component('models', {
|
|
|
289
410
|
this.selectedGeoField = storedField;
|
|
290
411
|
}
|
|
291
412
|
},
|
|
413
|
+
loadShowRowNumbersPreference() {
|
|
414
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const stored = window.localStorage.getItem(SHOW_ROW_NUMBERS_STORAGE_KEY);
|
|
418
|
+
if (stored === '0') {
|
|
419
|
+
this.showRowNumbers = false;
|
|
420
|
+
} else if (stored === '1') {
|
|
421
|
+
this.showRowNumbers = true;
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
toggleRowNumbers() {
|
|
425
|
+
this.showRowNumbers = !this.showRowNumbers;
|
|
426
|
+
if (typeof window !== 'undefined' && window.localStorage) {
|
|
427
|
+
window.localStorage.setItem(SHOW_ROW_NUMBERS_STORAGE_KEY, this.showRowNumbers ? '1' : '0');
|
|
428
|
+
}
|
|
429
|
+
this.showActionsMenu = false;
|
|
430
|
+
},
|
|
292
431
|
setOutputType(type) {
|
|
293
432
|
if (type !== 'json' && type !== 'table' && type !== 'map') {
|
|
294
433
|
return;
|
|
@@ -484,6 +623,22 @@ module.exports = app => app.component('models', {
|
|
|
484
623
|
params.searchText = this.searchText;
|
|
485
624
|
}
|
|
486
625
|
|
|
626
|
+
// Prefer explicit URL projection (`query.fields`) so the first fetch after
|
|
627
|
+
// mount/remount respects deep-linked projections before `filteredPaths`
|
|
628
|
+
// is rehydrated from schema paths.
|
|
629
|
+
let fieldsParam = normalizeFieldsParamForApi(this.query?.fields);
|
|
630
|
+
if (!fieldsParam) {
|
|
631
|
+
const fieldPaths = this.filteredPaths && this.filteredPaths.length > 0
|
|
632
|
+
? this.filteredPaths.map(p => p.path).filter(Boolean)
|
|
633
|
+
: null;
|
|
634
|
+
if (fieldPaths && fieldPaths.length > 0) {
|
|
635
|
+
fieldsParam = JSON.stringify(fieldPaths);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (fieldsParam) {
|
|
639
|
+
params.fields = fieldsParam;
|
|
640
|
+
}
|
|
641
|
+
|
|
487
642
|
return params;
|
|
488
643
|
},
|
|
489
644
|
setSearchTextFromRoute() {
|
|
@@ -497,20 +652,35 @@ module.exports = app => app.component('models', {
|
|
|
497
652
|
this.status = 'loading';
|
|
498
653
|
this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
|
|
499
654
|
this.setSearchTextFromRoute();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
|
|
655
|
+
// Avoid eval() on user-controlled query params.
|
|
656
|
+
// Use explicit sortKey + sortDirection query params.
|
|
657
|
+
const sortKey = this.$route.query?.sortKey;
|
|
658
|
+
const sortDirectionRaw = this.$route.query?.sortDirection;
|
|
659
|
+
const sortDirection = typeof sortDirectionRaw === 'string' ? Number(sortDirectionRaw) : sortDirectionRaw;
|
|
507
660
|
|
|
661
|
+
if (typeof sortKey === 'string' && sortKey.trim().length > 0 &&
|
|
662
|
+
(sortDirection === 1 || sortDirection === -1)) {
|
|
663
|
+
for (const key in this.sortBy) {
|
|
664
|
+
delete this.sortBy[key];
|
|
665
|
+
}
|
|
666
|
+
this.sortBy[sortKey] = sortDirection;
|
|
667
|
+
// Normalize to new params and remove legacy key if present.
|
|
668
|
+
this.query.sortKey = sortKey;
|
|
669
|
+
this.query.sortDirection = sortDirection;
|
|
670
|
+
delete this.query.sort;
|
|
671
|
+
}
|
|
508
672
|
if (this.currentModel != null) {
|
|
509
673
|
await this.getDocuments();
|
|
510
674
|
}
|
|
511
675
|
if (this.$route.query?.fields) {
|
|
512
|
-
const
|
|
513
|
-
|
|
676
|
+
const urlPaths = parseFieldsQueryParam(this.$route.query.fields);
|
|
677
|
+
if (urlPaths.length > 0) {
|
|
678
|
+
this.filteredPaths = urlPaths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
|
|
679
|
+
if (this.filteredPaths.length > 0) {
|
|
680
|
+
this.syncProjectionFromPaths();
|
|
681
|
+
this.saveProjectionPreference();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
514
684
|
}
|
|
515
685
|
this.status = 'loaded';
|
|
516
686
|
|
|
@@ -530,10 +700,8 @@ module.exports = app => app.component('models', {
|
|
|
530
700
|
this.shouldShowCreateModal = false;
|
|
531
701
|
await this.getDocuments();
|
|
532
702
|
},
|
|
533
|
-
initializeDocumentData() {
|
|
534
|
-
this.shouldShowCreateModal = true;
|
|
535
|
-
},
|
|
536
703
|
filterDocument(doc) {
|
|
704
|
+
if (this.filteredPaths.length === 0) return doc;
|
|
537
705
|
const filteredDoc = {};
|
|
538
706
|
for (let i = 0; i < this.filteredPaths.length; i++) {
|
|
539
707
|
const path = this.filteredPaths[i].path;
|
|
@@ -555,23 +723,47 @@ module.exports = app => app.component('models', {
|
|
|
555
723
|
if (this.status === 'loading' || this.loadedAllDocs) {
|
|
556
724
|
return;
|
|
557
725
|
}
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
726
|
+
// Infinite scroll only applies to table/json views.
|
|
727
|
+
if (this.outputType !== 'table' && this.outputType !== 'json') {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (this.documents.length === 0) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const container = this.outputType === 'table'
|
|
734
|
+
? this.$refs.documentsScrollContainer
|
|
735
|
+
: this.$refs.documentsContainerScroll;
|
|
736
|
+
if (!container || container.scrollHeight <= 0) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const threshold = 150;
|
|
740
|
+
const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - threshold;
|
|
741
|
+
if (!nearBottom) {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this.loadingMore = true;
|
|
745
|
+
this.status = 'loading';
|
|
746
|
+
try {
|
|
747
|
+
const skip = this.documents.length;
|
|
748
|
+
const params = this.buildDocumentFetchParams({ skip });
|
|
562
749
|
const { docs } = await api.Model.getDocuments(params);
|
|
563
750
|
if (docs.length < limit) {
|
|
564
751
|
this.loadedAllDocs = true;
|
|
565
752
|
}
|
|
566
753
|
this.documents.push(...docs);
|
|
754
|
+
} finally {
|
|
755
|
+
this.loadingMore = false;
|
|
567
756
|
this.status = 'loaded';
|
|
568
757
|
}
|
|
758
|
+
this.$nextTick(() => this.checkIfScrolledToBottom());
|
|
569
759
|
},
|
|
570
760
|
async sortDocs(num, path) {
|
|
571
761
|
let sorted = false;
|
|
572
762
|
if (this.sortBy[path] == num) {
|
|
573
763
|
sorted = true;
|
|
574
764
|
delete this.query.sort;
|
|
765
|
+
delete this.query.sortKey;
|
|
766
|
+
delete this.query.sortDirection;
|
|
575
767
|
this.$router.push({ query: this.query });
|
|
576
768
|
}
|
|
577
769
|
for (const key in this.sortBy) {
|
|
@@ -579,9 +771,13 @@ module.exports = app => app.component('models', {
|
|
|
579
771
|
}
|
|
580
772
|
if (!sorted) {
|
|
581
773
|
this.sortBy[path] = num;
|
|
582
|
-
this.query.
|
|
774
|
+
this.query.sortKey = path;
|
|
775
|
+
this.query.sortDirection = num;
|
|
776
|
+
delete this.query.sort;
|
|
583
777
|
this.$router.push({ query: this.query });
|
|
584
778
|
}
|
|
779
|
+
this.documents = [];
|
|
780
|
+
this.loadedAllDocs = false;
|
|
585
781
|
await this.loadMoreDocuments();
|
|
586
782
|
},
|
|
587
783
|
async search(searchText) {
|
|
@@ -618,6 +814,23 @@ module.exports = app => app.component('models', {
|
|
|
618
814
|
closeActionsMenu() {
|
|
619
815
|
this.showActionsMenu = false;
|
|
620
816
|
},
|
|
817
|
+
toggleProjectionMenu() {
|
|
818
|
+
const next = !this.isProjectionMenuSelected;
|
|
819
|
+
this.isProjectionMenuSelected = next;
|
|
820
|
+
|
|
821
|
+
// Because the route-view is keyed on `$route.fullPath`, query changes remount this component.
|
|
822
|
+
// Persist projection UI state in the URL so Reset/Suggest don't turn the mode off.
|
|
823
|
+
if (next) {
|
|
824
|
+
this.query[PROJECTION_MODE_QUERY_KEY] = '1';
|
|
825
|
+
if (this.outputType === 'map') {
|
|
826
|
+
this.setOutputType('json');
|
|
827
|
+
}
|
|
828
|
+
} else {
|
|
829
|
+
delete this.query[PROJECTION_MODE_QUERY_KEY];
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
this.$router.push({ query: this.query });
|
|
833
|
+
},
|
|
621
834
|
async openCollectionInfo() {
|
|
622
835
|
this.closeActionsMenu();
|
|
623
836
|
this.shouldShowCollectionInfoModal = true;
|
|
@@ -718,160 +931,401 @@ module.exports = app => app.component('models', {
|
|
|
718
931
|
}
|
|
719
932
|
return formatValue(value / 1000000000, 'B');
|
|
720
933
|
},
|
|
721
|
-
checkIndexLocation(indexName) {
|
|
722
|
-
if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
|
|
723
|
-
return 'text-gray-500';
|
|
724
|
-
} else if (this.schemaIndexes.find(x => x.name == indexName)) {
|
|
725
|
-
return 'text-forest-green-500';
|
|
726
|
-
} else {
|
|
727
|
-
return 'text-valencia-500';
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
934
|
async getDocuments() {
|
|
731
|
-
|
|
732
|
-
this.
|
|
935
|
+
this.loadingMore = false;
|
|
936
|
+
this.status = 'loading';
|
|
937
|
+
try {
|
|
938
|
+
// Track recently viewed model
|
|
939
|
+
this.trackRecentModel(this.currentModel);
|
|
733
940
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
941
|
+
// Clear previous data
|
|
942
|
+
this.documents = [];
|
|
943
|
+
this.schemaPaths = [];
|
|
944
|
+
this.numDocuments = null;
|
|
945
|
+
this.loadedAllDocs = false;
|
|
946
|
+
this.lastSelectedIndex = null;
|
|
740
947
|
|
|
741
|
-
|
|
742
|
-
|
|
948
|
+
let docsCount = 0;
|
|
949
|
+
let schemaPathsReceived = false;
|
|
743
950
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
951
|
+
// Use async generator to stream SSEs
|
|
952
|
+
const params = this.buildDocumentFetchParams();
|
|
953
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
954
|
+
if (event.schemaPaths && !schemaPathsReceived) {
|
|
748
955
|
// Sort schemaPaths with _id first
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
956
|
+
this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
|
|
957
|
+
if (k1 === '_id' && k2 !== '_id') {
|
|
958
|
+
return -1;
|
|
959
|
+
}
|
|
960
|
+
if (k1 !== '_id' && k2 === '_id') {
|
|
961
|
+
return 1;
|
|
962
|
+
}
|
|
963
|
+
return 0;
|
|
964
|
+
}).map(key => event.schemaPaths[key]);
|
|
965
|
+
this.shouldExport = {};
|
|
966
|
+
for (const { path } of this.schemaPaths) {
|
|
967
|
+
this.shouldExport[path] = true;
|
|
752
968
|
}
|
|
753
|
-
|
|
754
|
-
|
|
969
|
+
const shouldUseSavedProjection = this.isProjectionMenuSelected === true;
|
|
970
|
+
const savedPaths = shouldUseSavedProjection ? this.loadProjectionPreference() : null;
|
|
971
|
+
if (savedPaths === null) {
|
|
972
|
+
this.applyDefaultProjection(event.suggestedFields);
|
|
973
|
+
if (shouldUseSavedProjection) {
|
|
974
|
+
this.saveProjectionPreference();
|
|
975
|
+
}
|
|
976
|
+
} else if (Array.isArray(savedPaths) && savedPaths.length === 0) {
|
|
977
|
+
this.filteredPaths = [];
|
|
978
|
+
this.projectionText = '';
|
|
979
|
+
if (shouldUseSavedProjection) {
|
|
980
|
+
this.saveProjectionPreference();
|
|
981
|
+
}
|
|
982
|
+
} else if (savedPaths && savedPaths.length > 0) {
|
|
983
|
+
this.filteredPaths = savedPaths
|
|
984
|
+
.map(path => this.schemaPaths.find(p => p.path === path))
|
|
985
|
+
.filter(Boolean);
|
|
986
|
+
if (this.filteredPaths.length === 0) {
|
|
987
|
+
this.applyDefaultProjection(event.suggestedFields);
|
|
988
|
+
if (shouldUseSavedProjection) {
|
|
989
|
+
this.saveProjectionPreference();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
this.applyDefaultProjection(event.suggestedFields);
|
|
994
|
+
if (shouldUseSavedProjection) {
|
|
995
|
+
this.saveProjectionPreference();
|
|
996
|
+
}
|
|
755
997
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
998
|
+
this.selectedPaths = [...this.filteredPaths];
|
|
999
|
+
this.syncProjectionFromPaths();
|
|
1000
|
+
schemaPathsReceived = true;
|
|
1001
|
+
}
|
|
1002
|
+
if (event.numDocs !== undefined) {
|
|
1003
|
+
this.numDocuments = event.numDocs;
|
|
1004
|
+
}
|
|
1005
|
+
if (event.document) {
|
|
1006
|
+
this.documents.push(event.document);
|
|
1007
|
+
docsCount++;
|
|
1008
|
+
}
|
|
1009
|
+
if (event.message) {
|
|
1010
|
+
throw new Error(event.message);
|
|
761
1011
|
}
|
|
762
|
-
this.filteredPaths = [...this.schemaPaths];
|
|
763
|
-
this.selectedPaths = [...this.schemaPaths];
|
|
764
|
-
schemaPathsReceived = true;
|
|
765
|
-
}
|
|
766
|
-
if (event.numDocs !== undefined) {
|
|
767
|
-
this.numDocuments = event.numDocs;
|
|
768
|
-
}
|
|
769
|
-
if (event.document) {
|
|
770
|
-
this.documents.push(event.document);
|
|
771
|
-
docsCount++;
|
|
772
|
-
}
|
|
773
|
-
if (event.message) {
|
|
774
|
-
this.status = 'loaded';
|
|
775
|
-
throw new Error(event.message);
|
|
776
1012
|
}
|
|
777
|
-
}
|
|
778
1013
|
|
|
779
|
-
|
|
780
|
-
|
|
1014
|
+
if (docsCount < limit) {
|
|
1015
|
+
this.loadedAllDocs = true;
|
|
1016
|
+
}
|
|
1017
|
+
} finally {
|
|
1018
|
+
this.status = 'loaded';
|
|
781
1019
|
}
|
|
1020
|
+
this.$nextTick(() => {
|
|
1021
|
+
this.restoreScrollPosition();
|
|
1022
|
+
if (!this.suppressScrollCheck) {
|
|
1023
|
+
this.checkIfScrolledToBottom();
|
|
1024
|
+
}
|
|
1025
|
+
this.suppressScrollCheck = false;
|
|
1026
|
+
});
|
|
782
1027
|
},
|
|
783
1028
|
async loadMoreDocuments() {
|
|
784
|
-
|
|
785
|
-
|
|
1029
|
+
const isLoadingMore = this.documents.length > 0;
|
|
1030
|
+
if (isLoadingMore) {
|
|
1031
|
+
this.loadingMore = true;
|
|
1032
|
+
}
|
|
1033
|
+
this.status = 'loading';
|
|
1034
|
+
try {
|
|
1035
|
+
let docsCount = 0;
|
|
1036
|
+
let numDocsReceived = false;
|
|
786
1037
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
1038
|
+
// Use async generator to stream SSEs
|
|
1039
|
+
const params = this.buildDocumentFetchParams({ skip: this.documents.length });
|
|
1040
|
+
for await (const event of api.Model.getDocumentsStream(params)) {
|
|
1041
|
+
if (event.numDocs !== undefined && !numDocsReceived) {
|
|
1042
|
+
this.numDocuments = event.numDocs;
|
|
1043
|
+
numDocsReceived = true;
|
|
1044
|
+
}
|
|
1045
|
+
if (event.document) {
|
|
1046
|
+
this.documents.push(event.document);
|
|
1047
|
+
docsCount++;
|
|
1048
|
+
}
|
|
1049
|
+
if (event.message) {
|
|
1050
|
+
throw new Error(event.message);
|
|
1051
|
+
}
|
|
793
1052
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1053
|
+
|
|
1054
|
+
if (docsCount < limit) {
|
|
1055
|
+
this.loadedAllDocs = true;
|
|
797
1056
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1057
|
+
} finally {
|
|
1058
|
+
this.loadingMore = false;
|
|
1059
|
+
this.status = 'loaded';
|
|
1060
|
+
}
|
|
1061
|
+
this.$nextTick(() => this.checkIfScrolledToBottom());
|
|
1062
|
+
},
|
|
1063
|
+
applyDefaultProjection(suggestedFields) {
|
|
1064
|
+
if (Array.isArray(suggestedFields) && suggestedFields.length > 0) {
|
|
1065
|
+
this.filteredPaths = suggestedFields
|
|
1066
|
+
.map(path => this.schemaPaths.find(p => p.path === path))
|
|
1067
|
+
.filter(Boolean);
|
|
1068
|
+
}
|
|
1069
|
+
if (!this.filteredPaths || this.filteredPaths.length === 0) {
|
|
1070
|
+
this.filteredPaths = this.schemaPaths.slice(0, DEFAULT_FIRST_N_FIELDS);
|
|
1071
|
+
}
|
|
1072
|
+
if (this.filteredPaths.length === 0) {
|
|
1073
|
+
this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
|
|
1074
|
+
}
|
|
1075
|
+
},
|
|
1076
|
+
loadProjectionPreference() {
|
|
1077
|
+
if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
|
|
1081
|
+
const stored = window.localStorage.getItem(key);
|
|
1082
|
+
if (stored === null || stored === undefined) {
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
if (stored === '') {
|
|
1086
|
+
return [];
|
|
1087
|
+
}
|
|
1088
|
+
try {
|
|
1089
|
+
const parsed = JSON.parse(stored);
|
|
1090
|
+
if (Array.isArray(parsed)) {
|
|
1091
|
+
return parsed.map(x => String(x).trim()).filter(Boolean);
|
|
801
1092
|
}
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
return null;
|
|
802
1095
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1096
|
+
return null;
|
|
1097
|
+
},
|
|
1098
|
+
saveProjectionPreference() {
|
|
1099
|
+
if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
|
|
1100
|
+
return;
|
|
806
1101
|
}
|
|
1102
|
+
const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
|
|
1103
|
+
const paths = this.filteredPaths.map(p => p.path);
|
|
1104
|
+
window.localStorage.setItem(key, JSON.stringify(paths));
|
|
807
1105
|
},
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1106
|
+
clearProjection() {
|
|
1107
|
+
// Keep current filter input in sync with the URL so projection reset
|
|
1108
|
+
// does not unintentionally wipe the filter on remount.
|
|
1109
|
+
this.syncFilterToQuery();
|
|
1110
|
+
this.filteredPaths = [];
|
|
1111
|
+
this.selectedPaths = [];
|
|
1112
|
+
this.projectionText = '';
|
|
1113
|
+
this.updateProjectionQuery();
|
|
1114
|
+
this.saveProjectionPreference();
|
|
1115
|
+
},
|
|
1116
|
+
resetFilter() {
|
|
1117
|
+
// Reuse the existing "apply filter + update URL" flow.
|
|
1118
|
+
this.search('');
|
|
1119
|
+
},
|
|
1120
|
+
syncFilterToQuery() {
|
|
1121
|
+
if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
|
|
1122
|
+
this.query.search = this.searchText;
|
|
1123
|
+
} else {
|
|
1124
|
+
delete this.query.search;
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
applyDefaultProjectionColumns() {
|
|
1128
|
+
if (!this.schemaPaths || this.schemaPaths.length === 0) return;
|
|
1129
|
+
const pathNames = this.schemaPaths.map(p => p.path);
|
|
1130
|
+
this.applyDefaultProjection(pathNames.slice(0, DEFAULT_FIRST_N_FIELDS));
|
|
1131
|
+
this.selectedPaths = [...this.filteredPaths];
|
|
1132
|
+
this.syncProjectionFromPaths();
|
|
1133
|
+
this.updateProjectionQuery();
|
|
1134
|
+
this.saveProjectionPreference();
|
|
1135
|
+
},
|
|
1136
|
+
initProjection(ev) {
|
|
1137
|
+
if (!this.projectionText || !this.projectionText.trim()) {
|
|
1138
|
+
this.projectionText = '';
|
|
1139
|
+
this.$nextTick(() => {
|
|
1140
|
+
if (ev && ev.target) {
|
|
1141
|
+
ev.target.setSelectionRange(0, 0);
|
|
820
1142
|
}
|
|
821
|
-
|
|
822
|
-
}).map(key => this.selectedPaths[key]);
|
|
1143
|
+
});
|
|
823
1144
|
}
|
|
824
1145
|
},
|
|
825
|
-
|
|
826
|
-
if (this
|
|
827
|
-
this.
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
1146
|
+
syncProjectionFromPaths() {
|
|
1147
|
+
if (this.filteredPaths.length === 0) {
|
|
1148
|
+
this.projectionText = '';
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
// String-only projection syntax: `field1 field2` and `-field` for exclusions.
|
|
1152
|
+
// Since `filteredPaths` represents the final include set, we serialize as space-separated fields.
|
|
1153
|
+
this.projectionText = this.filteredPaths.map(p => p.path).join(' ');
|
|
1154
|
+
},
|
|
1155
|
+
parseProjectionInput(text) {
|
|
1156
|
+
if (!text || typeof text !== 'string') {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
const trimmed = text.trim();
|
|
1160
|
+
if (!trimmed) {
|
|
1161
|
+
return [];
|
|
1162
|
+
}
|
|
1163
|
+
const normalizeKey = (key) => String(key).trim();
|
|
1164
|
+
|
|
1165
|
+
// String-only projection syntax:
|
|
1166
|
+
// name email
|
|
1167
|
+
// -password (exclusion-only)
|
|
1168
|
+
// +email (inclusion-only)
|
|
1169
|
+
//
|
|
1170
|
+
// Brace/object syntax is intentionally NOT supported.
|
|
1171
|
+
if (trimmed.startsWith('{') || trimmed.endsWith('}')) {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
|
|
1176
|
+
if (tokens.length === 0) return [];
|
|
1177
|
+
|
|
1178
|
+
const includeKeys = [];
|
|
1179
|
+
const excludeKeys = [];
|
|
1180
|
+
|
|
1181
|
+
for (const rawToken of tokens) {
|
|
1182
|
+
const token = rawToken.trim();
|
|
1183
|
+
if (!token) continue;
|
|
1184
|
+
|
|
1185
|
+
const prefix = token[0];
|
|
1186
|
+
if (prefix === '-') {
|
|
1187
|
+
const path = token.slice(1).trim();
|
|
1188
|
+
if (!path) return null;
|
|
1189
|
+
excludeKeys.push(path);
|
|
1190
|
+
} else if (prefix === '+') {
|
|
1191
|
+
const path = token.slice(1).trim();
|
|
1192
|
+
if (!path) return null;
|
|
1193
|
+
includeKeys.push(path);
|
|
1194
|
+
} else {
|
|
1195
|
+
includeKeys.push(token);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
if (includeKeys.length > 0 && excludeKeys.length > 0) {
|
|
1200
|
+
// Support subtractive edits on an existing projection string, e.g.
|
|
1201
|
+
// `name email createdAt -email` -> `name createdAt`.
|
|
1202
|
+
const includeSet = new Set(includeKeys.map(normalizeKey));
|
|
1203
|
+
for (const path of excludeKeys) {
|
|
1204
|
+
includeSet.delete(normalizeKey(path));
|
|
1205
|
+
}
|
|
1206
|
+
return Array.from(includeSet);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (excludeKeys.length > 0) {
|
|
1210
|
+
const excludeSet = new Set(excludeKeys.map(normalizeKey));
|
|
1211
|
+
return this.schemaPaths.map(p => p.path).filter(p => !excludeSet.has(p));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
return includeKeys.map(normalizeKey);
|
|
1215
|
+
},
|
|
1216
|
+
applyProjectionFromInput() {
|
|
1217
|
+
const paths = this.parseProjectionInput(this.projectionText);
|
|
1218
|
+
if (paths === null) {
|
|
1219
|
+
this.syncProjectionFromPaths();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
if (paths.length === 0) {
|
|
1223
|
+
this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
|
|
1224
|
+
if (this.filteredPaths.length === 0 && this.schemaPaths.length > 0) {
|
|
1225
|
+
const idPath = this.schemaPaths.find(p => p.path === '_id');
|
|
1226
|
+
this.filteredPaths = idPath ? [idPath] : [this.schemaPaths[0]];
|
|
832
1227
|
}
|
|
833
1228
|
} else {
|
|
834
|
-
this.
|
|
1229
|
+
this.filteredPaths = paths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
|
|
1230
|
+
const validPaths = new Set(this.schemaPaths.map(p => p.path));
|
|
1231
|
+
for (const path of paths) {
|
|
1232
|
+
if (validPaths.has(path) && !this.filteredPaths.find(p => p.path === path)) {
|
|
1233
|
+
this.filteredPaths.push(this.schemaPaths.find(p => p.path === path));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
if (this.filteredPaths.length === 0) {
|
|
1237
|
+
this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
|
|
1238
|
+
}
|
|
835
1239
|
}
|
|
836
|
-
this.
|
|
1240
|
+
this.selectedPaths = [...this.filteredPaths];
|
|
1241
|
+
this.syncProjectionFromPaths();
|
|
1242
|
+
this.updateProjectionQuery();
|
|
1243
|
+
this.saveProjectionPreference();
|
|
837
1244
|
},
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1245
|
+
updateProjectionQuery() {
|
|
1246
|
+
const paths = this.filteredPaths.map(x => x.path).filter(Boolean);
|
|
1247
|
+
if (paths.length > 0) {
|
|
1248
|
+
this.query.fields = JSON.stringify(paths);
|
|
841
1249
|
} else {
|
|
842
|
-
this.
|
|
1250
|
+
delete this.query.fields;
|
|
843
1251
|
}
|
|
844
|
-
this.shouldShowFieldModal = false;
|
|
845
|
-
const selectedParams = this.filteredPaths.map(x => x.path).join(',');
|
|
846
|
-
this.query.fields = selectedParams;
|
|
847
1252
|
this.$router.push({ query: this.query });
|
|
848
1253
|
},
|
|
849
|
-
|
|
850
|
-
this.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1254
|
+
removeField(schemaPath) {
|
|
1255
|
+
if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
|
|
1256
|
+
this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
|
|
1257
|
+
this.suppressScrollCheck = true;
|
|
1258
|
+
// Persist for remount caused by query changes.
|
|
1259
|
+
if (typeof window !== 'undefined') {
|
|
1260
|
+
window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
|
|
1261
|
+
window.__studioModelsSuppressScrollCheck = true;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const index = this.filteredPaths.findIndex(p => p.path === schemaPath.path);
|
|
1265
|
+
if (index !== -1) {
|
|
1266
|
+
this.filteredPaths.splice(index, 1);
|
|
1267
|
+
if (this.filteredPaths.length === 0) {
|
|
1268
|
+
const idPath = this.schemaPaths.find(p => p.path === '_id');
|
|
1269
|
+
this.filteredPaths = idPath ? [idPath] : [];
|
|
1270
|
+
}
|
|
1271
|
+
this.syncProjectionFromPaths();
|
|
1272
|
+
this.updateProjectionQuery();
|
|
1273
|
+
this.saveProjectionPreference();
|
|
1274
|
+
}
|
|
854
1275
|
},
|
|
855
|
-
|
|
856
|
-
this.
|
|
1276
|
+
addField(schemaPath) {
|
|
1277
|
+
if (!this.filteredPaths.find(p => p.path === schemaPath.path)) {
|
|
1278
|
+
if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
|
|
1279
|
+
this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
|
|
1280
|
+
this.suppressScrollCheck = true;
|
|
1281
|
+
// Persist for remount caused by query changes.
|
|
1282
|
+
if (typeof window !== 'undefined') {
|
|
1283
|
+
window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
|
|
1284
|
+
window.__studioModelsSuppressScrollCheck = true;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
this.filteredPaths.push(schemaPath);
|
|
1288
|
+
this.filteredPaths.sort((a, b) => {
|
|
1289
|
+
if (a.path === '_id') return -1;
|
|
1290
|
+
if (b.path === '_id') return 1;
|
|
1291
|
+
return 0;
|
|
1292
|
+
});
|
|
1293
|
+
this.syncProjectionFromPaths();
|
|
1294
|
+
this.updateProjectionQuery();
|
|
1295
|
+
this.saveProjectionPreference();
|
|
1296
|
+
this.showAddFieldDropdown = false;
|
|
1297
|
+
this.addFieldFilterText = '';
|
|
1298
|
+
}
|
|
857
1299
|
},
|
|
858
|
-
|
|
859
|
-
this.
|
|
1300
|
+
restoreScrollPosition() {
|
|
1301
|
+
if (this.outputType !== 'table') return;
|
|
1302
|
+
if (this.scrollTopToRestore == null) return;
|
|
1303
|
+
const container = this.$refs.documentsScrollContainer;
|
|
1304
|
+
if (!container) return;
|
|
1305
|
+
container.scrollTop = this.scrollTopToRestore;
|
|
1306
|
+
this.scrollTopToRestore = null;
|
|
860
1307
|
},
|
|
861
|
-
|
|
862
|
-
|
|
1308
|
+
toggleAddFieldDropdown() {
|
|
1309
|
+
this.showAddFieldDropdown = !this.showAddFieldDropdown;
|
|
1310
|
+
if (this.showAddFieldDropdown) {
|
|
1311
|
+
this.addFieldFilterText = '';
|
|
1312
|
+
this.$nextTick(() => this.$refs.addFieldFilterInput?.focus());
|
|
1313
|
+
}
|
|
863
1314
|
},
|
|
864
1315
|
getComponentForPath(schemaPath) {
|
|
1316
|
+
if (!schemaPath || typeof schemaPath !== 'object') {
|
|
1317
|
+
return 'list-mixed';
|
|
1318
|
+
}
|
|
865
1319
|
if (schemaPath.instance === 'Array') {
|
|
866
1320
|
return 'list-array';
|
|
867
1321
|
}
|
|
868
1322
|
if (schemaPath.instance === 'String') {
|
|
869
1323
|
return 'list-string';
|
|
870
1324
|
}
|
|
871
|
-
if (schemaPath.instance
|
|
1325
|
+
if (schemaPath.instance === 'Embedded') {
|
|
872
1326
|
return 'list-subdocument';
|
|
873
1327
|
}
|
|
874
|
-
if (schemaPath.instance
|
|
1328
|
+
if (schemaPath.instance === 'Mixed') {
|
|
875
1329
|
return 'list-mixed';
|
|
876
1330
|
}
|
|
877
1331
|
return 'list-default';
|
|
@@ -896,6 +1350,31 @@ module.exports = app => app.component('models', {
|
|
|
896
1350
|
this.edittingDoc = null;
|
|
897
1351
|
this.$toast.success('Document updated!');
|
|
898
1352
|
},
|
|
1353
|
+
copyCellValue(value) {
|
|
1354
|
+
const text = value == null ? '' : (typeof value === 'object' ? JSON.stringify(value) : String(value));
|
|
1355
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
|
|
1356
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1357
|
+
this.$toast.success('Copied to clipboard');
|
|
1358
|
+
}).catch(() => {
|
|
1359
|
+
this.fallbackCopyText(text);
|
|
1360
|
+
});
|
|
1361
|
+
} else {
|
|
1362
|
+
this.fallbackCopyText(text);
|
|
1363
|
+
}
|
|
1364
|
+
},
|
|
1365
|
+
fallbackCopyText(text) {
|
|
1366
|
+
try {
|
|
1367
|
+
const el = document.createElement('textarea');
|
|
1368
|
+
el.value = text;
|
|
1369
|
+
document.body.appendChild(el);
|
|
1370
|
+
el.select();
|
|
1371
|
+
document.execCommand('copy');
|
|
1372
|
+
document.body.removeChild(el);
|
|
1373
|
+
this.$toast.success('Copied to clipboard');
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
this.$toast.error('Copy failed');
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
899
1378
|
handleDocumentClick(document, event) {
|
|
900
1379
|
if (this.selectMultiple) {
|
|
901
1380
|
this.handleDocumentSelection(document, event);
|