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