@mongoosejs/studio 0.3.2 → 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/ChatMessage/executeScript.js +1 -4
- 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/constants/mongooseStudioChartColors.js +6 -0
- package/backend/db/dashboardSchema.js +7 -1
- package/backend/helpers/getSuggestedProjection.js +37 -0
- package/backend/helpers/parseFieldsParam.js +36 -0
- package/frontend/public/app.js +701 -178
- 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 +621 -133
- 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
- package/seed/connect.js +5 -0
- package/seed/index.js +20 -0
|
@@ -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,15 +700,22 @@ 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;
|
|
540
708
|
const value = mpath.get(path, doc);
|
|
541
|
-
mpath.set(path, value, filteredDoc)
|
|
709
|
+
mpath.set(path, value, filteredDoc, function(cur, path, val) {
|
|
710
|
+
if (arguments.length === 2) {
|
|
711
|
+
if (cur[path] == null) {
|
|
712
|
+
cur[path] = {};
|
|
713
|
+
}
|
|
714
|
+
return cur[path];
|
|
715
|
+
}
|
|
716
|
+
cur[path] = val;
|
|
717
|
+
return val;
|
|
718
|
+
});
|
|
542
719
|
}
|
|
543
720
|
return filteredDoc;
|
|
544
721
|
},
|
|
@@ -546,23 +723,47 @@ module.exports = app => app.component('models', {
|
|
|
546
723
|
if (this.status === 'loading' || this.loadedAllDocs) {
|
|
547
724
|
return;
|
|
548
725
|
}
|
|
549
|
-
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
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 });
|
|
553
749
|
const { docs } = await api.Model.getDocuments(params);
|
|
554
750
|
if (docs.length < limit) {
|
|
555
751
|
this.loadedAllDocs = true;
|
|
556
752
|
}
|
|
557
753
|
this.documents.push(...docs);
|
|
754
|
+
} finally {
|
|
755
|
+
this.loadingMore = false;
|
|
558
756
|
this.status = 'loaded';
|
|
559
757
|
}
|
|
758
|
+
this.$nextTick(() => this.checkIfScrolledToBottom());
|
|
560
759
|
},
|
|
561
760
|
async sortDocs(num, path) {
|
|
562
761
|
let sorted = false;
|
|
563
762
|
if (this.sortBy[path] == num) {
|
|
564
763
|
sorted = true;
|
|
565
764
|
delete this.query.sort;
|
|
765
|
+
delete this.query.sortKey;
|
|
766
|
+
delete this.query.sortDirection;
|
|
566
767
|
this.$router.push({ query: this.query });
|
|
567
768
|
}
|
|
568
769
|
for (const key in this.sortBy) {
|
|
@@ -570,9 +771,13 @@ module.exports = app => app.component('models', {
|
|
|
570
771
|
}
|
|
571
772
|
if (!sorted) {
|
|
572
773
|
this.sortBy[path] = num;
|
|
573
|
-
this.query.
|
|
774
|
+
this.query.sortKey = path;
|
|
775
|
+
this.query.sortDirection = num;
|
|
776
|
+
delete this.query.sort;
|
|
574
777
|
this.$router.push({ query: this.query });
|
|
575
778
|
}
|
|
779
|
+
this.documents = [];
|
|
780
|
+
this.loadedAllDocs = false;
|
|
576
781
|
await this.loadMoreDocuments();
|
|
577
782
|
},
|
|
578
783
|
async search(searchText) {
|
|
@@ -609,6 +814,23 @@ module.exports = app => app.component('models', {
|
|
|
609
814
|
closeActionsMenu() {
|
|
610
815
|
this.showActionsMenu = false;
|
|
611
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
|
+
},
|
|
612
834
|
async openCollectionInfo() {
|
|
613
835
|
this.closeActionsMenu();
|
|
614
836
|
this.shouldShowCollectionInfoModal = true;
|
|
@@ -709,160 +931,401 @@ module.exports = app => app.component('models', {
|
|
|
709
931
|
}
|
|
710
932
|
return formatValue(value / 1000000000, 'B');
|
|
711
933
|
},
|
|
712
|
-
checkIndexLocation(indexName) {
|
|
713
|
-
if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
|
|
714
|
-
return 'text-gray-500';
|
|
715
|
-
} else if (this.schemaIndexes.find(x => x.name == indexName)) {
|
|
716
|
-
return 'text-forest-green-500';
|
|
717
|
-
} else {
|
|
718
|
-
return 'text-valencia-500';
|
|
719
|
-
}
|
|
720
|
-
},
|
|
721
934
|
async getDocuments() {
|
|
722
|
-
|
|
723
|
-
this.
|
|
935
|
+
this.loadingMore = false;
|
|
936
|
+
this.status = 'loading';
|
|
937
|
+
try {
|
|
938
|
+
// Track recently viewed model
|
|
939
|
+
this.trackRecentModel(this.currentModel);
|
|
724
940
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
941
|
+
// Clear previous data
|
|
942
|
+
this.documents = [];
|
|
943
|
+
this.schemaPaths = [];
|
|
944
|
+
this.numDocuments = null;
|
|
945
|
+
this.loadedAllDocs = false;
|
|
946
|
+
this.lastSelectedIndex = null;
|
|
731
947
|
|
|
732
|
-
|
|
733
|
-
|
|
948
|
+
let docsCount = 0;
|
|
949
|
+
let schemaPathsReceived = false;
|
|
734
950
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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) {
|
|
739
955
|
// Sort schemaPaths with _id first
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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;
|
|
743
968
|
}
|
|
744
|
-
|
|
745
|
-
|
|
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
|
+
}
|
|
746
997
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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);
|
|
752
1011
|
}
|
|
753
|
-
this.filteredPaths = [...this.schemaPaths];
|
|
754
|
-
this.selectedPaths = [...this.schemaPaths];
|
|
755
|
-
schemaPathsReceived = true;
|
|
756
|
-
}
|
|
757
|
-
if (event.numDocs !== undefined) {
|
|
758
|
-
this.numDocuments = event.numDocs;
|
|
759
|
-
}
|
|
760
|
-
if (event.document) {
|
|
761
|
-
this.documents.push(event.document);
|
|
762
|
-
docsCount++;
|
|
763
|
-
}
|
|
764
|
-
if (event.message) {
|
|
765
|
-
this.status = 'loaded';
|
|
766
|
-
throw new Error(event.message);
|
|
767
1012
|
}
|
|
768
|
-
}
|
|
769
1013
|
|
|
770
|
-
|
|
771
|
-
|
|
1014
|
+
if (docsCount < limit) {
|
|
1015
|
+
this.loadedAllDocs = true;
|
|
1016
|
+
}
|
|
1017
|
+
} finally {
|
|
1018
|
+
this.status = 'loaded';
|
|
772
1019
|
}
|
|
1020
|
+
this.$nextTick(() => {
|
|
1021
|
+
this.restoreScrollPosition();
|
|
1022
|
+
if (!this.suppressScrollCheck) {
|
|
1023
|
+
this.checkIfScrolledToBottom();
|
|
1024
|
+
}
|
|
1025
|
+
this.suppressScrollCheck = false;
|
|
1026
|
+
});
|
|
773
1027
|
},
|
|
774
1028
|
async loadMoreDocuments() {
|
|
775
|
-
|
|
776
|
-
|
|
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;
|
|
777
1037
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
+
}
|
|
784
1052
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1053
|
+
|
|
1054
|
+
if (docsCount < limit) {
|
|
1055
|
+
this.loadedAllDocs = true;
|
|
788
1056
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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);
|
|
792
1092
|
}
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
return null;
|
|
793
1095
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1096
|
+
return null;
|
|
1097
|
+
},
|
|
1098
|
+
saveProjectionPreference() {
|
|
1099
|
+
if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
|
|
1100
|
+
return;
|
|
797
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));
|
|
798
1105
|
},
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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);
|
|
811
1142
|
}
|
|
812
|
-
|
|
813
|
-
}).map(key => this.selectedPaths[key]);
|
|
1143
|
+
});
|
|
814
1144
|
}
|
|
815
1145
|
},
|
|
816
|
-
|
|
817
|
-
if (this
|
|
818
|
-
this.
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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]];
|
|
823
1227
|
}
|
|
824
1228
|
} else {
|
|
825
|
-
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
|
+
}
|
|
826
1239
|
}
|
|
827
|
-
this.
|
|
1240
|
+
this.selectedPaths = [...this.filteredPaths];
|
|
1241
|
+
this.syncProjectionFromPaths();
|
|
1242
|
+
this.updateProjectionQuery();
|
|
1243
|
+
this.saveProjectionPreference();
|
|
828
1244
|
},
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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);
|
|
832
1249
|
} else {
|
|
833
|
-
this.
|
|
1250
|
+
delete this.query.fields;
|
|
834
1251
|
}
|
|
835
|
-
this.shouldShowFieldModal = false;
|
|
836
|
-
const selectedParams = this.filteredPaths.map(x => x.path).join(',');
|
|
837
|
-
this.query.fields = selectedParams;
|
|
838
1252
|
this.$router.push({ query: this.query });
|
|
839
1253
|
},
|
|
840
|
-
|
|
841
|
-
this.
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
+
}
|
|
845
1275
|
},
|
|
846
|
-
|
|
847
|
-
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
|
+
}
|
|
848
1299
|
},
|
|
849
|
-
|
|
850
|
-
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;
|
|
851
1307
|
},
|
|
852
|
-
|
|
853
|
-
|
|
1308
|
+
toggleAddFieldDropdown() {
|
|
1309
|
+
this.showAddFieldDropdown = !this.showAddFieldDropdown;
|
|
1310
|
+
if (this.showAddFieldDropdown) {
|
|
1311
|
+
this.addFieldFilterText = '';
|
|
1312
|
+
this.$nextTick(() => this.$refs.addFieldFilterInput?.focus());
|
|
1313
|
+
}
|
|
854
1314
|
},
|
|
855
1315
|
getComponentForPath(schemaPath) {
|
|
1316
|
+
if (!schemaPath || typeof schemaPath !== 'object') {
|
|
1317
|
+
return 'list-mixed';
|
|
1318
|
+
}
|
|
856
1319
|
if (schemaPath.instance === 'Array') {
|
|
857
1320
|
return 'list-array';
|
|
858
1321
|
}
|
|
859
1322
|
if (schemaPath.instance === 'String') {
|
|
860
1323
|
return 'list-string';
|
|
861
1324
|
}
|
|
862
|
-
if (schemaPath.instance
|
|
1325
|
+
if (schemaPath.instance === 'Embedded') {
|
|
863
1326
|
return 'list-subdocument';
|
|
864
1327
|
}
|
|
865
|
-
if (schemaPath.instance
|
|
1328
|
+
if (schemaPath.instance === 'Mixed') {
|
|
866
1329
|
return 'list-mixed';
|
|
867
1330
|
}
|
|
868
1331
|
return 'list-default';
|
|
@@ -887,6 +1350,31 @@ module.exports = app => app.component('models', {
|
|
|
887
1350
|
this.edittingDoc = null;
|
|
888
1351
|
this.$toast.success('Document updated!');
|
|
889
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
|
+
},
|
|
890
1378
|
handleDocumentClick(document, event) {
|
|
891
1379
|
if (this.selectMultiple) {
|
|
892
1380
|
this.handleDocumentSelection(document, event);
|