@mongoosejs/studio 0.5.6 → 0.5.8
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/README.md +35 -3
- package/express.js +8 -1
- package/frontend/public/app.js +988 -216
- package/frontend/public/tw.css +113 -0
- package/frontend/src/_util/calendar.js +155 -0
- package/frontend/src/_util/document-search-autocomplete.js +105 -23
- package/frontend/src/custom-date-picker/custom-date-picker.html +183 -0
- package/frontend/src/custom-date-picker/custom-date-picker.js +407 -0
- package/frontend/src/document/document.css +62 -0
- package/frontend/src/document/document.html +113 -2
- package/frontend/src/document/document.js +171 -9
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +1 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +1 -1
- package/frontend/src/document-details/document-details.css +1 -63
- package/frontend/src/document-details/document-details.html +0 -122
- package/frontend/src/document-details/document-details.js +2 -138
- package/frontend/src/document-details/document-property/document-property.html +3 -2
- package/frontend/src/edit-date/edit-date.html +8 -6
- package/frontend/src/edit-date/edit-date.js +0 -20
- package/frontend/src/models/document-search/document-search.html +78 -22
- package/frontend/src/models/document-search/document-search.js +64 -2
- package/nest.d.ts +27 -0
- package/nest.js +134 -0
- package/package.json +8 -1
|
@@ -9,20 +9,12 @@ appendCSS(require('./document-details.css'));
|
|
|
9
9
|
|
|
10
10
|
module.exports = app => app.component('document-details', {
|
|
11
11
|
template,
|
|
12
|
-
props: ['document', 'schemaPaths', 'virtualPaths', 'editting', 'changes', 'invalid', 'viewMode'
|
|
12
|
+
props: ['document', 'schemaPaths', 'virtualPaths', 'editting', 'changes', 'invalid', 'viewMode'],
|
|
13
13
|
data() {
|
|
14
14
|
return {
|
|
15
15
|
searchQuery: '',
|
|
16
16
|
selectedType: '',
|
|
17
|
-
collapsedVirtuals: new Set()
|
|
18
|
-
showAddFieldModal: false,
|
|
19
|
-
fieldData: {
|
|
20
|
-
name: '',
|
|
21
|
-
type: '',
|
|
22
|
-
value: ''
|
|
23
|
-
},
|
|
24
|
-
fieldErrors: {},
|
|
25
|
-
isSubmittingField: false
|
|
17
|
+
collapsedVirtuals: new Set()
|
|
26
18
|
};
|
|
27
19
|
},
|
|
28
20
|
mounted() {
|
|
@@ -92,20 +84,6 @@ module.exports = app => app.component('document-details', {
|
|
|
92
84
|
|
|
93
85
|
return Array.from(types).sort();
|
|
94
86
|
},
|
|
95
|
-
allFieldTypes() {
|
|
96
|
-
const schemaTypes = this.availableTypes;
|
|
97
|
-
const commonTypes = ['String', 'Number', 'Boolean', 'Date', 'Array', 'Object'];
|
|
98
|
-
|
|
99
|
-
// Combine schema types with common types, avoiding duplicates
|
|
100
|
-
const allTypes = new Set([...schemaTypes, ...commonTypes]);
|
|
101
|
-
return Array.from(allTypes).sort();
|
|
102
|
-
},
|
|
103
|
-
shouldUseAce() {
|
|
104
|
-
return ['Array', 'Object', 'Embedded'].includes(this.fieldData.type);
|
|
105
|
-
},
|
|
106
|
-
shouldUseDatePicker() {
|
|
107
|
-
return this.fieldData.type === 'Date';
|
|
108
|
-
},
|
|
109
87
|
typeFilteredSchemaPaths() {
|
|
110
88
|
let paths = this.schemaPaths || [];
|
|
111
89
|
|
|
@@ -317,120 +295,6 @@ module.exports = app => app.component('document-details', {
|
|
|
317
295
|
isVirtualFieldCollapsed(fieldName) {
|
|
318
296
|
return this.collapsedVirtuals.has(fieldName);
|
|
319
297
|
},
|
|
320
|
-
openAddFieldModal() {
|
|
321
|
-
this.showAddFieldModal = true;
|
|
322
|
-
},
|
|
323
|
-
closeAddFieldModal() {
|
|
324
|
-
this.showAddFieldModal = false;
|
|
325
|
-
this.resetFieldForm();
|
|
326
|
-
},
|
|
327
|
-
async addNewField(fieldData) {
|
|
328
|
-
// Emit event to parent component to handle the field addition
|
|
329
|
-
this.$emit('add-field', fieldData);
|
|
330
|
-
this.closeAddFieldModal();
|
|
331
|
-
},
|
|
332
|
-
validateFieldForm() {
|
|
333
|
-
this.fieldErrors = {};
|
|
334
|
-
|
|
335
|
-
// Validate field name
|
|
336
|
-
const trimmedName = this.fieldData.name.trim();
|
|
337
|
-
if (!trimmedName) {
|
|
338
|
-
this.fieldErrors.name = 'Field name is required';
|
|
339
|
-
} else {
|
|
340
|
-
const transformedName = this.getTransformedFieldName();
|
|
341
|
-
if (!transformedName) {
|
|
342
|
-
this.fieldErrors.name = 'Field name contains only invalid characters';
|
|
343
|
-
} else if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(transformedName)) {
|
|
344
|
-
this.fieldErrors.name = 'Field name must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $';
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Validate field type
|
|
349
|
-
if (!this.fieldData.type) {
|
|
350
|
-
this.fieldErrors.type = 'Field type is required';
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Validate field value if provided
|
|
354
|
-
if (this.fieldData.value && this.fieldData.value.trim()) {
|
|
355
|
-
if (['Object', 'Array'].includes(this.fieldData.type)) {
|
|
356
|
-
try {
|
|
357
|
-
JSON.parse(this.fieldData.value);
|
|
358
|
-
} catch (e) {
|
|
359
|
-
this.fieldErrors.value = 'Invalid JSON format for object/array type';
|
|
360
|
-
}
|
|
361
|
-
} else if (this.fieldData.type === 'Number') {
|
|
362
|
-
if (isNaN(Number(this.fieldData.value))) {
|
|
363
|
-
this.fieldErrors.value = 'Invalid number format';
|
|
364
|
-
}
|
|
365
|
-
} else if (this.fieldData.type === 'Boolean') {
|
|
366
|
-
const lowerValue = this.fieldData.value.toLowerCase();
|
|
367
|
-
if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lowerValue)) {
|
|
368
|
-
this.fieldErrors.value = 'Invalid boolean value (use true/false, 1/0, yes/no)';
|
|
369
|
-
}
|
|
370
|
-
} else if (this.fieldData.type === 'Date') {
|
|
371
|
-
// Date picker provides YYYY-MM-DD format, validate it
|
|
372
|
-
const dateValue = new Date(this.fieldData.value);
|
|
373
|
-
if (isNaN(dateValue.getTime())) {
|
|
374
|
-
this.fieldErrors.value = 'Invalid date format';
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
return Object.keys(this.fieldErrors).length === 0;
|
|
380
|
-
},
|
|
381
|
-
parseFieldValue(value, type) {
|
|
382
|
-
if (!value || !value.trim()) {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
switch (type) {
|
|
387
|
-
case 'Number':
|
|
388
|
-
return Number(value);
|
|
389
|
-
case 'Boolean':
|
|
390
|
-
const lowerValue = value.toLowerCase();
|
|
391
|
-
return ['true', '1', 'yes'].includes(lowerValue);
|
|
392
|
-
case 'Date':
|
|
393
|
-
// For date picker, value is already in YYYY-MM-DD format
|
|
394
|
-
return new Date(value);
|
|
395
|
-
case 'Object':
|
|
396
|
-
case 'Array':
|
|
397
|
-
return JSON.parse(value);
|
|
398
|
-
default:
|
|
399
|
-
return value;
|
|
400
|
-
}
|
|
401
|
-
},
|
|
402
|
-
async handleAddFieldSubmit() {
|
|
403
|
-
if (!this.validateFieldForm()) {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
this.isSubmittingField = true;
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
const fieldData = {
|
|
411
|
-
name: this.fieldData.name,
|
|
412
|
-
type: this.fieldData.type,
|
|
413
|
-
value: this.parseFieldValue(this.fieldData.value, this.fieldData.type)
|
|
414
|
-
};
|
|
415
|
-
|
|
416
|
-
this.$emit('add-field', fieldData);
|
|
417
|
-
this.closeAddFieldModal();
|
|
418
|
-
} catch (error) {
|
|
419
|
-
console.error('Error adding field:', error);
|
|
420
|
-
this.fieldErrors.value = 'Error processing field value';
|
|
421
|
-
} finally {
|
|
422
|
-
this.isSubmittingField = false;
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
resetFieldForm() {
|
|
426
|
-
this.fieldData = {
|
|
427
|
-
name: '',
|
|
428
|
-
type: '',
|
|
429
|
-
value: ''
|
|
430
|
-
};
|
|
431
|
-
this.fieldErrors = {};
|
|
432
|
-
this.isSubmittingField = false;
|
|
433
|
-
},
|
|
434
298
|
getVirtualFieldType(virtual) {
|
|
435
299
|
const value = virtual.value;
|
|
436
300
|
if (value === null || value === undefined) {
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
v-if="isDatePath"
|
|
95
95
|
:viewMode="dateViewMode"
|
|
96
96
|
:path="path"
|
|
97
|
+
:editting="editting"
|
|
97
98
|
@update:viewMode="dateViewMode = $event"
|
|
98
99
|
></date-view-mode-picker>
|
|
99
100
|
<objectid-view-mode-picker
|
|
@@ -129,7 +130,7 @@
|
|
|
129
130
|
<div v-if="editting && path.instance === 'Date'" class="mb-3 flex gap-1.5">
|
|
130
131
|
<div
|
|
131
132
|
@click="dateType = 'picker'"
|
|
132
|
-
:class="dateType === 'picker' ? 'bg-
|
|
133
|
+
:class="dateType === 'picker' ? 'bg-merlot-700' : ''"
|
|
133
134
|
class="self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer">
|
|
134
135
|
<div
|
|
135
136
|
:class="dateType === 'picker' ? 'text-white' : ''"
|
|
@@ -139,7 +140,7 @@
|
|
|
139
140
|
</div>
|
|
140
141
|
<div
|
|
141
142
|
@click="dateType = 'iso'"
|
|
142
|
-
:class="dateType === 'iso' ? 'bg-
|
|
143
|
+
:class="dateType === 'iso' ? 'bg-merlot-700' : ''"
|
|
143
144
|
class="self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer">
|
|
144
145
|
<div
|
|
145
146
|
:class="dateType === 'iso' ? 'text-white' : ''"
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
<div>
|
|
2
|
-
<
|
|
2
|
+
<custom-date-picker
|
|
3
3
|
v-if="dateSelection == 'picker'"
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
:
|
|
7
|
-
|
|
4
|
+
:value="value"
|
|
5
|
+
mode="datetime"
|
|
6
|
+
:horizontal="true"
|
|
7
|
+
class="max-w-[420px]"
|
|
8
|
+
@input="$emit('input', $event)"
|
|
9
|
+
/>
|
|
8
10
|
<input
|
|
9
11
|
v-if="dateSelection == 'iso'"
|
|
10
12
|
type="text"
|
|
11
13
|
class="w-64 h-8 border border-edge-strong outline-0"
|
|
12
14
|
:value="valueAsISOString"
|
|
13
15
|
@input="updateFromISO">
|
|
14
|
-
</div>
|
|
16
|
+
</div>
|
|
@@ -6,9 +6,6 @@ module.exports = app => app.component('edit-date', {
|
|
|
6
6
|
template: template,
|
|
7
7
|
props: ['value', 'format'],
|
|
8
8
|
emits: ['input'],
|
|
9
|
-
data: () => ({
|
|
10
|
-
inputType: ''
|
|
11
|
-
}),
|
|
12
9
|
methods: {
|
|
13
10
|
updateFromISO($event) {
|
|
14
11
|
const value = $event.target.value;
|
|
@@ -28,23 +25,6 @@ module.exports = app => app.component('edit-date', {
|
|
|
28
25
|
}
|
|
29
26
|
},
|
|
30
27
|
computed: {
|
|
31
|
-
valueAsLocalString() {
|
|
32
|
-
if (this.value == null) {
|
|
33
|
-
return this.value;
|
|
34
|
-
}
|
|
35
|
-
const date = new Date(this.value);
|
|
36
|
-
return [
|
|
37
|
-
date.getFullYear(),
|
|
38
|
-
'-',
|
|
39
|
-
(date.getMonth() + 1).toString().padStart(2, '0'),
|
|
40
|
-
'-',
|
|
41
|
-
date.getDate().toString().padStart(2, '0'),
|
|
42
|
-
'T',
|
|
43
|
-
date.getHours().toString().padStart(2, '0'),
|
|
44
|
-
':',
|
|
45
|
-
date.getMinutes().toString().padStart(2, '0')
|
|
46
|
-
].join('');
|
|
47
|
-
},
|
|
48
28
|
valueAsISOString() {
|
|
49
29
|
if (this.value == null) {
|
|
50
30
|
return '' + this.value;
|
|
@@ -1,23 +1,79 @@
|
|
|
1
|
-
<
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@mousedown.prevent="applySuggestion(index)"
|
|
1
|
+
<div class="relative flex-grow m-0" @focusout="onWrapperFocusOut">
|
|
2
|
+
<form @submit.prevent="emitSearch" class="w-full mb-0">
|
|
3
|
+
<input
|
|
4
|
+
ref="searchInput"
|
|
5
|
+
class="w-full font-mono rounded-md p-1 border border-edge-strong outline-edge-strong text-lg focus:ring-1 focus:ring-primary-subtle focus:ring-offset-0 focus:outline-none"
|
|
6
|
+
type="text"
|
|
7
|
+
placeholder="Filter (supports JS, dates, ObjectIds, and more)"
|
|
8
|
+
v-model="searchText"
|
|
9
|
+
@click="initFilter($event); updateAutocomplete()"
|
|
10
|
+
@input="updateAutocomplete"
|
|
11
|
+
@keyup="onSearchKeyup"
|
|
12
|
+
@keydown="handleKeyDown"
|
|
13
|
+
/>
|
|
14
|
+
<div
|
|
15
|
+
v-if="autocompleteSuggestions.length"
|
|
16
|
+
ref="autocompleteDropdown"
|
|
17
|
+
class="absolute z-[9999] bg-surface border border-edge-strong rounded mt-1 w-full shadow"
|
|
19
18
|
>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
<ul class="max-h-40 overflow-y-auto">
|
|
20
|
+
<li
|
|
21
|
+
v-for="(suggestion, index) in autocompleteSuggestions"
|
|
22
|
+
:key="suggestion"
|
|
23
|
+
class="px-2 py-1 cursor-pointer"
|
|
24
|
+
:class="{ 'bg-primary-subtle': index === autocompleteIndex }"
|
|
25
|
+
@mousedown.prevent="applySuggestion(index)"
|
|
26
|
+
>
|
|
27
|
+
{{ suggestion }}
|
|
28
|
+
</li>
|
|
29
|
+
</ul>
|
|
30
|
+
</div>
|
|
31
|
+
</form>
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
v-if="datePickerContext"
|
|
35
|
+
class="absolute top-full left-0 z-[10000] mt-0 w-[400px] max-w-[calc(100vw-2rem)] bg-surface border border-edge-strong rounded-lg shadow-xl"
|
|
36
|
+
role="dialog"
|
|
37
|
+
aria-label="Date picker"
|
|
38
|
+
@mousedown.stop
|
|
39
|
+
>
|
|
40
|
+
<div class="flex items-center justify-between gap-2 px-3 py-2 border-b border-edge-strong">
|
|
41
|
+
<span class="text-sm font-medium text-content-primary">Insert date into filter</span>
|
|
42
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
43
|
+
<select
|
|
44
|
+
v-if="!datePickerMinimized"
|
|
45
|
+
v-model="dateInsertFormat"
|
|
46
|
+
class="text-xs border border-edge rounded-md bg-surface px-2 py-1"
|
|
47
|
+
@mousedown.stop
|
|
48
|
+
>
|
|
49
|
+
<option value="timestamp">Milliseconds</option>
|
|
50
|
+
<option value="quoted">Date string</option>
|
|
51
|
+
</select>
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
class="flex items-center justify-center w-5 h-5 rounded hover:bg-surface-hover text-content-secondary"
|
|
55
|
+
:aria-label="datePickerMinimized ? 'Expand date picker' : 'Minimize date picker'"
|
|
56
|
+
@click.stop="datePickerMinimized = !datePickerMinimized"
|
|
57
|
+
>
|
|
58
|
+
<svg v-if="datePickerMinimized" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
|
60
|
+
</svg>
|
|
61
|
+
<svg v-else xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-3.5 h-3.5">
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div v-if="!datePickerMinimized" class="p-2">
|
|
68
|
+
<custom-date-picker
|
|
69
|
+
compact
|
|
70
|
+
horizontal
|
|
71
|
+
embedded
|
|
72
|
+
:immediate="false"
|
|
73
|
+
mode="datetime"
|
|
74
|
+
:value="datePickerLocalValue"
|
|
75
|
+
@commit="applyDateFromPicker"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
@@ -4,7 +4,10 @@ const template = require('./document-search.html');
|
|
|
4
4
|
const {
|
|
5
5
|
buildAutocompleteTrie,
|
|
6
6
|
getAutocompleteSuggestions,
|
|
7
|
-
applySuggestion
|
|
7
|
+
applySuggestion,
|
|
8
|
+
getDatePickerInsertionRange,
|
|
9
|
+
dateArgumentSliceToDatetimeLocal,
|
|
10
|
+
insertDateInDateArgument
|
|
8
11
|
} = require('../../_util/document-search-autocomplete');
|
|
9
12
|
|
|
10
13
|
module.exports = app => app.component('document-search', {
|
|
@@ -24,7 +27,11 @@ module.exports = app => app.component('document-search', {
|
|
|
24
27
|
autocompleteSuggestions: [],
|
|
25
28
|
autocompleteIndex: 0,
|
|
26
29
|
autocompleteTrie: null,
|
|
27
|
-
searchText: this.value || ''
|
|
30
|
+
searchText: this.value || '',
|
|
31
|
+
datePickerContext: null,
|
|
32
|
+
datePickerLocalValue: '',
|
|
33
|
+
dateInsertFormat: 'quoted', // 'timestamp' | 'quoted'
|
|
34
|
+
datePickerMinimized: false
|
|
28
35
|
};
|
|
29
36
|
},
|
|
30
37
|
watch: {
|
|
@@ -66,10 +73,31 @@ module.exports = app => app.component('document-search', {
|
|
|
66
73
|
});
|
|
67
74
|
}
|
|
68
75
|
},
|
|
76
|
+
onSearchKeyup(ev) {
|
|
77
|
+
if (
|
|
78
|
+
this.autocompleteSuggestions.length > 0 &&
|
|
79
|
+
(ev.key === 'ArrowUp' || ev.key === 'ArrowDown')
|
|
80
|
+
) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.updateAutocomplete();
|
|
84
|
+
},
|
|
69
85
|
updateAutocomplete() {
|
|
70
86
|
const input = this.$refs.searchInput;
|
|
71
87
|
const cursorPos = input ? input.selectionStart : 0;
|
|
72
88
|
|
|
89
|
+
const dateRange = getDatePickerInsertionRange(this.searchText, cursorPos);
|
|
90
|
+
if (dateRange && !this.datePickerContext) {
|
|
91
|
+
this.datePickerMinimized = false;
|
|
92
|
+
}
|
|
93
|
+
this.datePickerContext = dateRange;
|
|
94
|
+
if (dateRange) {
|
|
95
|
+
const argSlice = this.searchText.slice(dateRange.innerStart, dateRange.innerEnd);
|
|
96
|
+
this.datePickerLocalValue = dateArgumentSliceToDatetimeLocal(argSlice);
|
|
97
|
+
} else {
|
|
98
|
+
this.datePickerLocalValue = '';
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
if (this.autocompleteTrie) {
|
|
74
102
|
this.autocompleteSuggestions = getAutocompleteSuggestions(
|
|
75
103
|
this.autocompleteTrie,
|
|
@@ -82,6 +110,34 @@ module.exports = app => app.component('document-search', {
|
|
|
82
110
|
this.autocompleteSuggestions = [];
|
|
83
111
|
}
|
|
84
112
|
},
|
|
113
|
+
applyDateFromPicker(localDateTime) {
|
|
114
|
+
if (!localDateTime || !this.datePickerContext) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const picked = new Date(localDateTime);
|
|
118
|
+
if (Number.isNaN(picked.getTime())) {
|
|
119
|
+
if (this.$toast) {
|
|
120
|
+
this.$toast.error('Invalid date or time. Enter a valid date and time, then try again.');
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const result = insertDateInDateArgument(
|
|
125
|
+
this.searchText,
|
|
126
|
+
this.datePickerContext,
|
|
127
|
+
picked,
|
|
128
|
+
this.dateInsertFormat
|
|
129
|
+
);
|
|
130
|
+
const input = this.$refs.searchInput;
|
|
131
|
+
this.searchText = result.text;
|
|
132
|
+
this.autocompleteSuggestions = [];
|
|
133
|
+
this.$nextTick(() => {
|
|
134
|
+
if (input) {
|
|
135
|
+
input.focus();
|
|
136
|
+
input.setSelectionRange(result.newCursorPos, result.newCursorPos);
|
|
137
|
+
}
|
|
138
|
+
this.updateAutocomplete();
|
|
139
|
+
});
|
|
140
|
+
},
|
|
85
141
|
handleKeyDown(ev) {
|
|
86
142
|
if (this.autocompleteSuggestions.length === 0) {
|
|
87
143
|
return;
|
|
@@ -116,6 +172,12 @@ module.exports = app => app.component('document-search', {
|
|
|
116
172
|
});
|
|
117
173
|
this.autocompleteSuggestions = [];
|
|
118
174
|
},
|
|
175
|
+
onWrapperFocusOut(ev) {
|
|
176
|
+
if (!this.$el.contains(ev.relatedTarget)) {
|
|
177
|
+
this.datePickerContext = null;
|
|
178
|
+
this.datePickerMinimized = false;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
119
181
|
addPathFilter(path) {
|
|
120
182
|
if (this.searchText) {
|
|
121
183
|
if (this.searchText.endsWith('}')) {
|
package/nest.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
declare module '@mongoosejs/studio/nest' {
|
|
2
|
+
import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
3
|
+
import { Connection, Mongoose } from 'mongoose';
|
|
4
|
+
|
|
5
|
+
export interface MongooseStudioModuleOptions {
|
|
6
|
+
path?: string;
|
|
7
|
+
apiPath?: string;
|
|
8
|
+
connection?: Connection | Mongoose;
|
|
9
|
+
connectionToken?: string | symbol | Function;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
bindIp?: string | string[] | null;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MongooseStudioModuleAsyncOptions {
|
|
16
|
+
imports?: any[];
|
|
17
|
+
inject?: any[];
|
|
18
|
+
connectionToken?: string | symbol | Function;
|
|
19
|
+
useFactory: (...args: any[]) => Promise<MongooseStudioModuleOptions> | MongooseStudioModuleOptions;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class MongooseStudioModule implements NestModule {
|
|
23
|
+
configure(consumer: MiddlewareConsumer): void;
|
|
24
|
+
static forRoot(options?: MongooseStudioModuleOptions): DynamicModule;
|
|
25
|
+
static forRootAsync(options: MongooseStudioModuleAsyncOptions): DynamicModule;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/nest.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Inject, Module, RequestMethod } = require('@nestjs/common');
|
|
4
|
+
const mongoose = require('mongoose');
|
|
5
|
+
const createExpressRouter = require('./express');
|
|
6
|
+
|
|
7
|
+
const MONGOOSE_STUDIO_OPTIONS = Symbol('MONGOOSE_STUDIO_OPTIONS');
|
|
8
|
+
const MONGOOSE_STUDIO_CONNECTION = Symbol('MONGOOSE_STUDIO_CONNECTION');
|
|
9
|
+
const MONGOOSE_STUDIO_EXPRESS_ROUTER = Symbol('MONGOOSE_STUDIO_EXPRESS_ROUTER');
|
|
10
|
+
|
|
11
|
+
class MongooseStudioModule {
|
|
12
|
+
constructor(options, expressRouter) {
|
|
13
|
+
this.options = options || {};
|
|
14
|
+
this.expressRouter = expressRouter;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
configure(consumer) {
|
|
18
|
+
const mountPath = this.options.path || '/studio';
|
|
19
|
+
|
|
20
|
+
consumer
|
|
21
|
+
.apply((req, res, next) => {
|
|
22
|
+
if (!isMountedStudioRequest(req, mountPath)) {
|
|
23
|
+
return next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const originalUrl = req.url;
|
|
27
|
+
req.url = req.url.slice(normalizeExpressMountPath(mountPath).length) || '/';
|
|
28
|
+
try {
|
|
29
|
+
return this.expressRouter(req, res, err => {
|
|
30
|
+
req.url = originalUrl;
|
|
31
|
+
next(err);
|
|
32
|
+
});
|
|
33
|
+
} finally {
|
|
34
|
+
req.url = originalUrl;
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.forRoutes({
|
|
38
|
+
path: '*path',
|
|
39
|
+
method: RequestMethod.ALL
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static forRoot(options) {
|
|
44
|
+
options = options || {};
|
|
45
|
+
const providers = [
|
|
46
|
+
{ provide: MONGOOSE_STUDIO_OPTIONS, useValue: options },
|
|
47
|
+
createExpressRouterProvider()
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (options.connectionToken) {
|
|
51
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useExisting: options.connectionToken });
|
|
52
|
+
} else if (options.connection) {
|
|
53
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: options.connection });
|
|
54
|
+
} else {
|
|
55
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: mongoose });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
module: MongooseStudioModule,
|
|
60
|
+
providers,
|
|
61
|
+
exports: providers
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static forRootAsync(options) {
|
|
66
|
+
options = options || {};
|
|
67
|
+
const providers = [
|
|
68
|
+
{
|
|
69
|
+
provide: MONGOOSE_STUDIO_OPTIONS,
|
|
70
|
+
useFactory: options.useFactory,
|
|
71
|
+
inject: options.inject || []
|
|
72
|
+
},
|
|
73
|
+
createExpressRouterProvider()
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
if (options.connectionToken) {
|
|
77
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useExisting: options.connectionToken });
|
|
78
|
+
} else if (options.connection) {
|
|
79
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: options.connection });
|
|
80
|
+
} else {
|
|
81
|
+
providers.push({ provide: MONGOOSE_STUDIO_CONNECTION, useValue: mongoose });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
module: MongooseStudioModule,
|
|
86
|
+
imports: options.imports || [],
|
|
87
|
+
providers,
|
|
88
|
+
exports: providers
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createExpressRouterProvider() {
|
|
94
|
+
return {
|
|
95
|
+
provide: MONGOOSE_STUDIO_EXPRESS_ROUTER,
|
|
96
|
+
useFactory: async(options, connection) => {
|
|
97
|
+
options = options || {};
|
|
98
|
+
const mountPath = options.path || '/studio';
|
|
99
|
+
const apiPath = options.apiPath || `${mountPath}/api`;
|
|
100
|
+
const studioOptions = { ...options };
|
|
101
|
+
delete studioOptions.path;
|
|
102
|
+
delete studioOptions.apiPath;
|
|
103
|
+
delete studioOptions.connection;
|
|
104
|
+
delete studioOptions.connectionToken;
|
|
105
|
+
|
|
106
|
+
return createExpressRouter(apiPath, options.connection || connection || mongoose, studioOptions);
|
|
107
|
+
},
|
|
108
|
+
inject: [MONGOOSE_STUDIO_OPTIONS, MONGOOSE_STUDIO_CONNECTION]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isMountedStudioRequest(req, mountPath) {
|
|
113
|
+
const normalizedMountPath = normalizeExpressMountPath(mountPath);
|
|
114
|
+
return req.url === normalizedMountPath ||
|
|
115
|
+
req.url.startsWith(`${normalizedMountPath}/`) ||
|
|
116
|
+
req.url.startsWith(`${normalizedMountPath}?`) ||
|
|
117
|
+
req.url.startsWith(`${normalizedMountPath}#`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function normalizeExpressMountPath(path) {
|
|
121
|
+
path = `/${path.replace(/^\/+/, '').replace(/\/+$/, '')}`;
|
|
122
|
+
return path === '/' ? '' : path;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Inject(MONGOOSE_STUDIO_OPTIONS)(MongooseStudioModule, undefined, 0);
|
|
126
|
+
Inject(MONGOOSE_STUDIO_EXPRESS_ROUTER)(MongooseStudioModule, undefined, 1);
|
|
127
|
+
Module({})(MongooseStudioModule);
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
MONGOOSE_STUDIO_CONNECTION,
|
|
131
|
+
MONGOOSE_STUDIO_EXPRESS_ROUTER,
|
|
132
|
+
MONGOOSE_STUDIO_OPTIONS,
|
|
133
|
+
MongooseStudioModule
|
|
134
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.8",
|
|
4
4
|
"description": "AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.",
|
|
5
5
|
"homepage": "https://mongoosestudio.app/",
|
|
6
6
|
"repository": {
|
|
@@ -31,8 +31,14 @@
|
|
|
31
31
|
"xss": "^1.0.15"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
+
"@nestjs/common": "10.x || 11.x",
|
|
34
35
|
"mongoose": "7.x || 8.x || ^9.0.0"
|
|
35
36
|
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@nestjs/common": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
36
42
|
"optionalPeerDependencies": {
|
|
37
43
|
"@mongoosejs/task": "0.5.x || 0.6.x"
|
|
38
44
|
},
|
|
@@ -41,6 +47,7 @@
|
|
|
41
47
|
"axios": "1.2.2",
|
|
42
48
|
"dedent": "^1.6.0",
|
|
43
49
|
"eslint": "9.30.0",
|
|
50
|
+
"@nestjs/common": "^11.1.9",
|
|
44
51
|
"express": "4.x",
|
|
45
52
|
"mocha": "10.2.0",
|
|
46
53
|
"mongodb-memory-server": "^11.0.1",
|