@mongoosejs/studio 0.5.7 → 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.
@@ -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', 'model'],
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-teal-600' : ''"
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-teal-600' : ''"
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
- <input
2
+ <custom-date-picker
3
3
  v-if="dateSelection == 'picker'"
4
- class="w-64 h-8 border border-edge-strong outline-0"
5
- type="datetime-local"
6
- :value="valueAsLocalString"
7
- @input="$emit('input', $event.target.value)">
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
- <form @submit.prevent="emitSearch" class="relative flex-grow m-0">
2
- <input
3
- ref="searchInput"
4
- 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"
5
- type="text"
6
- placeholder="Filter (supports JS, dates, ObjectIds, and more)"
7
- v-model="searchText"
8
- @click="initFilter"
9
- @input="updateAutocomplete"
10
- @keydown="handleKeyDown"
11
- />
12
- <ul v-if="autocompleteSuggestions.length" class="absolute z-[9999] bg-surface border border-edge-strong rounded mt-1 w-full max-h-40 overflow-y-auto shadow">
13
- <li
14
- v-for="(suggestion, index) in autocompleteSuggestions"
15
- :key="suggestion"
16
- class="px-2 py-1 cursor-pointer"
17
- :class="{ 'bg-primary-subtle': index === autocompleteIndex }"
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
- {{ suggestion }}
21
- </li>
22
- </ul>
23
- </form>
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.5.7",
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": {