@mongoosejs/studio 0.5.7 → 0.5.9

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.
@@ -273,9 +273,120 @@
273
273
  :changes="changes"
274
274
  :invalid="invalid"
275
275
  :viewMode="viewMode"
276
- :model="model"
277
- @add-field="addField"
278
276
  @view-mode-change="updateViewMode"></document-details>
277
+ <modal v-if="showAddFieldModal">
278
+ <template v-slot:body>
279
+ <div class="modal-exit" @click="closeAddFieldModal">&times;</div>
280
+ <div class="add-field-modal">
281
+ <div class="mb-6">
282
+ <h2 class="text-xl font-semibold text-content mb-2">Add New Field</h2>
283
+ <p class="text-sm text-gray-600">Create a new field for this document</p>
284
+ </div>
285
+
286
+ <form @submit.prevent="handleAddFieldSubmit" class="space-y-4">
287
+ <div>
288
+ <label for="fieldName" class="block text-sm font-medium text-content-secondary mb-1">
289
+ Field Name *
290
+ </label>
291
+ <input
292
+ id="fieldName"
293
+ v-model="fieldData.name"
294
+ type="text"
295
+ required
296
+ placeholder="Enter field name..."
297
+ class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
298
+ :class="{ 'border-red-500': fieldErrors.name }"
299
+ />
300
+ <p v-if="fieldErrors.name" class="mt-1 text-sm text-red-600">{{fieldErrors.name}}</p>
301
+ <p v-if="fieldData.name && getTransformedFieldName() !== fieldData.name.trim()" class="mt-1 text-sm text-blue-600">
302
+ Field name will be: <code class="bg-blue-50 px-1 py-0.5 rounded text-xs">{{getTransformedFieldName()}}</code>
303
+ </p>
304
+ </div>
305
+
306
+ <div>
307
+ <label for="fieldType" class="block text-sm font-medium text-content-secondary mb-1">
308
+ Field Type *
309
+ </label>
310
+ <select
311
+ id="fieldType"
312
+ v-model="fieldData.type"
313
+ required
314
+ class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
315
+ :class="{ 'border-red-500': fieldErrors.type }"
316
+ >
317
+ <option value="">Select field type...</option>
318
+ <option v-for="type in allFieldTypes" :key="type" :value="type">
319
+ {{type}}
320
+ </option>
321
+ </select>
322
+ <p v-if="fieldErrors.type" class="mt-1 text-sm text-red-600">{{fieldErrors.type}}</p>
323
+ </div>
324
+
325
+ <div>
326
+ <label for="fieldValue" class="block text-sm font-medium text-content-secondary mb-1">
327
+ Initial Value
328
+ </label>
329
+
330
+ <custom-date-picker
331
+ v-if="shouldUseDatePicker"
332
+ id="fieldValue"
333
+ mode="date"
334
+ :value="fieldData.value"
335
+ :class="{ 'border-red-500': fieldErrors.value, 'max-w-[400px]': true }"
336
+ :horizontal="true"
337
+ @input="fieldData.value = $event"
338
+ />
339
+
340
+ <input
341
+ v-else-if="!shouldUseAce"
342
+ v-model="fieldData.value"
343
+ type="text"
344
+ id="fieldValue"
345
+ placeholder="Enter initial value (optional)..."
346
+ class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
347
+ :class="{ 'border-red-500': fieldErrors.value }"
348
+ />
349
+
350
+ <ace-editor
351
+ v-else
352
+ id="fieldValue"
353
+ v-model="fieldData.value"
354
+ mode="json"
355
+ :line-numbers="true"
356
+ :min-lines="3"
357
+ class="add-field-ace-editor w-full min-h-[100px]"
358
+ :class="{ 'border-red-500': fieldErrors.value }"
359
+ />
360
+
361
+ <p v-if="fieldErrors.value" class="mt-1 text-sm text-red-600">{{fieldErrors.value}}</p>
362
+ <p class="mt-1 text-xs text-content-tertiary">
363
+ <span v-if="shouldUseDatePicker">Select a date or leave empty for null/undefined values.</span>
364
+ <span v-else-if="shouldUseAce">Leave empty for null/undefined values. Use valid JSON format.</span>
365
+ <span v-else>Leave empty for null/undefined values.</span>
366
+ </p>
367
+ </div>
368
+
369
+ <div class="flex justify-end gap-3 pt-4 border-t border-edge">
370
+ <button
371
+ type="button"
372
+ @click="closeAddFieldModal"
373
+ class="px-4 py-2 text-sm font-medium text-content-secondary bg-surface border border-edge-strong rounded-md hover:bg-page focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
374
+ >
375
+ Cancel
376
+ </button>
377
+ <button
378
+ type="submit"
379
+ :disabled="isSubmittingField"
380
+ class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
381
+ >
382
+ <span v-if="isSubmittingField">Adding...</span>
383
+ <span v-else>Add Field</span>
384
+ </button>
385
+ </div>
386
+ </form>
387
+ </div>
388
+ </template>
389
+ </modal>
279
390
  <modal v-if="shouldShowConfirmModal">
280
391
  <template v-slot:body>
281
392
  <div class="modal-exit" @click="shouldShowConfirmModal = false;">&times;</div>
@@ -27,7 +27,15 @@ module.exports = app => app.component('document', {
27
27
  shouldShowDeleteModal: false,
28
28
  shouldShowCloneModal: false,
29
29
  shouldShowValidationModal: false,
30
+ showAddFieldModal: false,
30
31
  validationResult: null,
32
+ fieldData: {
33
+ name: '',
34
+ type: '',
35
+ value: ''
36
+ },
37
+ fieldErrors: {},
38
+ isSubmittingField: false,
31
39
  previousQuery: null,
32
40
  lastUpdatedAt: null,
33
41
  isRefreshing: false,
@@ -89,6 +97,33 @@ module.exports = app => app.component('document', {
89
97
  }
90
98
  return shortcuts;
91
99
  },
100
+ allFieldTypes() {
101
+ const types = new Set(['String', 'Number', 'Boolean', 'Date', 'Array', 'Object']);
102
+
103
+ (this.schemaPaths || []).forEach(path => {
104
+ if (path.instance) {
105
+ types.add(path.instance);
106
+ }
107
+ });
108
+
109
+ Object.keys(this.document || {}).forEach(key => {
110
+ if ((this.schemaPaths || []).some(path => path.path === key)) {
111
+ return;
112
+ }
113
+ const fieldType = this.getFieldType(this.document[key]);
114
+ if (fieldType && fieldType !== 'unknown' && fieldType !== 'null') {
115
+ types.add(fieldType);
116
+ }
117
+ });
118
+
119
+ return Array.from(types).sort();
120
+ },
121
+ shouldUseAce() {
122
+ return ['Array', 'Object', 'Embedded'].includes(this.fieldData.type);
123
+ },
124
+ shouldUseDatePicker() {
125
+ return this.fieldData.type === 'Date';
126
+ },
92
127
  isLambda() {
93
128
  return !!window?.MONGOOSE_STUDIO_CONFIG?.isLambda;
94
129
  }
@@ -285,16 +320,143 @@ module.exports = app => app.component('document', {
285
320
  showClonedDocument(doc) {
286
321
  this.$router.push({ path: `/model/${this.model}/document/${doc._id}` });
287
322
  },
288
- async addField(fieldData) {
289
- const { doc } = await api.Model.addField({
290
- model: this.model,
291
- _id: this.document._id,
292
- fieldName: fieldData.name,
293
- fieldValue: fieldData.value
294
- });
295
- this.document = doc;
323
+ addField() {
324
+ this.showAddFieldModal = true;
325
+ },
326
+ closeAddFieldModal() {
327
+ this.showAddFieldModal = false;
328
+ this.resetFieldForm();
329
+ },
330
+ resetFieldForm() {
331
+ this.fieldData = {
332
+ name: '',
333
+ type: '',
334
+ value: ''
335
+ };
336
+ this.fieldErrors = {};
337
+ this.isSubmittingField = false;
338
+ },
339
+ getTransformedFieldName() {
340
+ const trimmedName = this.fieldData.name.trim();
341
+ return trimmedName
342
+ .replace(/[^a-zA-Z0-9_$]/g, '_')
343
+ .replace(/^[^a-zA-Z_$]+/, '');
344
+ },
345
+ validateFieldForm() {
346
+ this.fieldErrors = {};
347
+
348
+ const trimmedName = this.fieldData.name.trim();
349
+ if (!trimmedName) {
350
+ this.fieldErrors.name = 'Field name is required';
351
+ } else {
352
+ const transformedName = this.getTransformedFieldName();
353
+ if (!transformedName) {
354
+ this.fieldErrors.name = 'Field name contains only invalid characters';
355
+ } else if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(transformedName)) {
356
+ this.fieldErrors.name = 'Field name must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $';
357
+ }
358
+ }
359
+
360
+ if (!this.fieldData.type) {
361
+ this.fieldErrors.type = 'Field type is required';
362
+ }
363
+
364
+ if (this.fieldData.value && this.fieldData.value.trim()) {
365
+ if (['Object', 'Array'].includes(this.fieldData.type)) {
366
+ try {
367
+ JSON.parse(this.fieldData.value);
368
+ } catch (err) {
369
+ this.fieldErrors.value = 'Invalid JSON format for object/array type';
370
+ }
371
+ } else if (this.fieldData.type === 'Number') {
372
+ if (isNaN(Number(this.fieldData.value))) {
373
+ this.fieldErrors.value = 'Invalid number format';
374
+ }
375
+ } else if (this.fieldData.type === 'Boolean') {
376
+ const lowerValue = this.fieldData.value.toLowerCase();
377
+ if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lowerValue)) {
378
+ this.fieldErrors.value = 'Invalid boolean value (use true/false, 1/0, yes/no)';
379
+ }
380
+ } else if (this.fieldData.type === 'Date') {
381
+ const dateValue = new Date(this.fieldData.value);
382
+ if (isNaN(dateValue.getTime())) {
383
+ this.fieldErrors.value = 'Invalid date format';
384
+ }
385
+ }
386
+ }
387
+
388
+ return Object.keys(this.fieldErrors).length === 0;
389
+ },
390
+ parseFieldValue(value, type) {
391
+ if (!value || !value.trim()) {
392
+ return null;
393
+ }
394
+
395
+ switch (type) {
396
+ case 'Number':
397
+ return Number(value);
398
+ case 'Boolean':
399
+ return ['true', '1', 'yes'].includes(value.toLowerCase());
400
+ case 'Date':
401
+ return new Date(value);
402
+ case 'Object':
403
+ case 'Array':
404
+ return JSON.parse(value);
405
+ default:
406
+ return value;
407
+ }
408
+ },
409
+ async handleAddFieldSubmit() {
410
+ if (!this.validateFieldForm()) {
411
+ return;
412
+ }
413
+
414
+ this.isSubmittingField = true;
415
+
416
+ try {
417
+ const fieldName = this.getTransformedFieldName();
418
+ const fieldValue = this.parseFieldValue(this.fieldData.value, this.fieldData.type);
419
+
420
+ const { doc } = await api.Model.addField({
421
+ model: this.model,
422
+ _id: this.document._id,
423
+ fieldName,
424
+ fieldValue
425
+ });
426
+ this.document = doc;
427
+ this.closeAddFieldModal();
296
428
 
297
- this.$toast.success(`Field added! Field "${fieldData.name}" has been added to the document`);
429
+ this.$toast.success(`Field added! Field "${fieldName}" has been added to the document`);
430
+ } catch (error) {
431
+ console.error('Error adding field:', error);
432
+ this.fieldErrors.value = 'Error adding field';
433
+ } finally {
434
+ this.isSubmittingField = false;
435
+ }
436
+ },
437
+ getFieldType(value) {
438
+ if (value === null || value === undefined) {
439
+ return 'null';
440
+ }
441
+ if (Array.isArray(value)) {
442
+ return 'Array';
443
+ }
444
+ if (value instanceof Date) {
445
+ return 'Date';
446
+ }
447
+ if (typeof value === 'object') {
448
+ return 'Object';
449
+ }
450
+ if (typeof value === 'number') {
451
+ return 'Number';
452
+ }
453
+ if (typeof value === 'boolean') {
454
+ return 'Boolean';
455
+ }
456
+ if (typeof value === 'string') {
457
+ return 'String';
458
+ }
459
+ return 'unknown';
298
460
  },
299
461
  updateViewMode(mode) {
300
462
  this.viewMode = mode;
@@ -2,6 +2,7 @@
2
2
  <select
3
3
  :value="format"
4
4
  @input="onFormatChange($event.target.value)"
5
+ :disabled="editting"
5
6
  class="text-xs border border-edge rounded-md bg-surface px-2 py-1"
6
7
  >
7
8
  <option value="utc_iso">UTC (ISO)</option>
@@ -4,7 +4,7 @@ const template = require('./date-view-mode-picker.html');
4
4
 
5
5
  module.exports = app => app.component('date-view-mode-picker', {
6
6
  template,
7
- props: ['viewMode', 'path'],
7
+ props: ['viewMode', 'path', 'editting'],
8
8
  emits: ['update:viewMode'],
9
9
  computed: {
10
10
  format() {
@@ -22,68 +22,6 @@
22
22
  margin-top: -7px;
23
23
  }
24
24
 
25
- /* Add Field Modal Styles */
26
- .add-field-modal {
27
- max-width: 500px;
28
- width: 100%;
29
- }
30
-
31
- .add-field-modal .modal-exit {
32
- position: absolute;
33
- top: 15px;
34
- right: 20px;
35
- font-size: 24px;
36
- cursor: pointer;
37
- color: #6b7280;
38
- z-index: 10;
39
- }
40
-
41
- .add-field-modal .modal-exit:hover {
42
- color: #374151;
43
- }
44
-
45
- .add-field-modal form {
46
- max-height: 70vh;
47
- overflow-y: auto;
48
- }
49
-
50
- .add-field-modal input[type="text"],
51
- .add-field-modal input[type="email"],
52
- .add-field-modal input[type="password"],
53
- .add-field-modal select,
54
- .add-field-modal textarea {
55
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
56
- }
57
-
58
- .add-field-modal input:focus,
59
- .add-field-modal select:focus,
60
- .add-field-modal textarea:focus {
61
- border-color: #3b82f6;
62
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
63
- }
64
-
65
- .add-field-modal .border-red-500 {
66
- border-color: #ef4444 !important;
67
- }
68
-
69
- .add-field-modal .border-red-500:focus {
70
- box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1) !important;
71
- }
72
-
73
- /* Ace editor styling in modal */
74
- .add-field-modal .add-field-ace-editor.ace_editor {
75
- border: 1px solid #d1d5db;
76
- border-radius: 0.375rem;
77
- font-size: 14px;
78
- min-height: 100px;
79
- }
80
-
81
- .add-field-modal .add-field-ace-editor:focus-within.ace_editor,
82
- .add-field-modal .add-field-ace-editor.ace_focus {
83
- border-color: #3b82f6;
84
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
85
- }
86
-
87
25
  /* JSON View Styles */
88
26
  .json-view {
89
27
  width: 100%;
@@ -113,4 +51,4 @@
113
51
 
114
52
  .json-view pre::-webkit-scrollbar-thumb:hover {
115
53
  background: #555;
116
- }
54
+ }
@@ -172,126 +172,4 @@
172
172
  <pre class="text-sm font-mono text-gray-800 whitespace-pre">{{formattedJson}}</pre>
173
173
  </div>
174
174
  </div>
175
-
176
- <!-- Add Field Modal -->
177
- <modal v-if="showAddFieldModal">
178
- <template v-slot:body>
179
- <div class="modal-exit" @click="closeAddFieldModal">&times;</div>
180
- <div class="add-field-modal">
181
- <div class="mb-6">
182
- <h2 class="text-xl font-semibold text-content mb-2">Add New Field</h2>
183
- <p class="text-sm text-gray-600">Create a new field for this document</p>
184
- </div>
185
-
186
- <form @submit.prevent="handleAddFieldSubmit" class="space-y-4">
187
- <!-- Field Name -->
188
- <div>
189
- <label for="fieldName" class="block text-sm font-medium text-content-secondary mb-1">
190
- Field Name *
191
- </label>
192
- <input
193
- id="fieldName"
194
- v-model="fieldData.name"
195
- type="text"
196
- required
197
- placeholder="Enter field name..."
198
- class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
199
- :class="{ 'border-red-500': fieldErrors.name }"
200
- />
201
- <p v-if="fieldErrors.name" class="mt-1 text-sm text-red-600">{{fieldErrors.name}}</p>
202
- <p v-if="fieldData.name && getTransformedFieldName() !== fieldData.name.trim()" class="mt-1 text-sm text-blue-600">
203
- Field name will be: <code class="bg-blue-50 px-1 py-0.5 rounded text-xs">{{getTransformedFieldName()}}</code>
204
- </p>
205
- </div>
206
-
207
- <!-- Field Type -->
208
- <div>
209
- <label for="fieldType" class="block text-sm font-medium text-content-secondary mb-1">
210
- Field Type *
211
- </label>
212
- <select
213
- id="fieldType"
214
- v-model="fieldData.type"
215
- required
216
- class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
217
- :class="{ 'border-red-500': fieldErrors.type }"
218
- >
219
- <option value="">Select field type...</option>
220
- <option v-for="type in allFieldTypes" :key="type" :value="type">
221
- {{type}}
222
- </option>
223
- </select>
224
- <p v-if="fieldErrors.type" class="mt-1 text-sm text-red-600">{{fieldErrors.type}}</p>
225
- </div>
226
-
227
- <!-- Field Value -->
228
- <div>
229
- <label for="fieldValue" class="block text-sm font-medium text-content-secondary mb-1">
230
- Initial Value
231
- </label>
232
-
233
- <!-- Date picker for Date type -->
234
- <input
235
- v-if="shouldUseDatePicker"
236
- v-model="fieldData.value"
237
- type="date"
238
- id="fieldValue"
239
- class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
240
- :class="{ 'border-red-500': fieldErrors.value }"
241
- />
242
-
243
- <!-- Simple input for basic types -->
244
- <input
245
- v-else-if="!shouldUseAce"
246
- v-model="fieldData.value"
247
- type="text"
248
- id="fieldValue"
249
- placeholder="Enter initial value (optional)..."
250
- class="w-full px-3 py-2 border border-edge-strong rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
251
- :class="{ 'border-red-500': fieldErrors.value }"
252
- />
253
-
254
- <!-- Ace editor for complex types (Array/Object/Embedded) -->
255
- <ace-editor
256
- v-else
257
- id="fieldValue"
258
- v-model="fieldData.value"
259
- mode="json"
260
- :line-numbers="true"
261
- :min-lines="3"
262
- class="add-field-ace-editor w-full min-h-[100px]"
263
- :class="{ 'border-red-500': fieldErrors.value }"
264
- />
265
-
266
- <p v-if="fieldErrors.value" class="mt-1 text-sm text-red-600">{{fieldErrors.value}}</p>
267
- <p class="mt-1 text-xs text-content-tertiary">
268
- <span v-if="shouldUseDatePicker">Select a date or leave empty for null/undefined values.</span>
269
- <span v-else-if="shouldUseAce">Leave empty for null/undefined values. Use valid JSON format.</span>
270
- <span v-else>Leave empty for null/undefined values.</span>
271
- </p>
272
- </div>
273
-
274
-
275
- <!-- Action Buttons -->
276
- <div class="flex justify-end gap-3 pt-4 border-t border-edge">
277
- <button
278
- type="button"
279
- @click="closeAddFieldModal"
280
- class="px-4 py-2 text-sm font-medium text-content-secondary bg-surface border border-edge-strong rounded-md hover:bg-page focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
281
- >
282
- Cancel
283
- </button>
284
- <button
285
- type="submit"
286
- :disabled="isSubmittingField"
287
- class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
288
- >
289
- <span v-if="isSubmittingField">Adding...</span>
290
- <span v-else>Add Field</span>
291
- </button>
292
- </div>
293
- </form>
294
- </div>
295
- </template>
296
- </modal>
297
175
  </div>