@mongoosejs/studio 0.0.127 → 0.0.129

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.
Files changed (28) hide show
  1. package/backend/actions/Model/addField.js +54 -0
  2. package/backend/actions/Model/exportQueryResults.js +0 -1
  3. package/backend/actions/Model/getDocument.js +3 -1
  4. package/backend/actions/Model/getDocuments.js +14 -5
  5. package/backend/actions/Model/getDocumentsStream.js +14 -5
  6. package/backend/actions/Model/index.js +1 -0
  7. package/backend/actions/Model/updateDocument.js +23 -6
  8. package/backend/actions/Model/updateDocuments.js +23 -5
  9. package/frontend/public/app.js +613 -45
  10. package/frontend/public/tw.css +219 -2
  11. package/frontend/src/api.js +6 -0
  12. package/frontend/src/document/document.css +8 -0
  13. package/frontend/src/document/document.html +102 -8
  14. package/frontend/src/document/document.js +34 -1
  15. package/frontend/src/document-details/document-details.css +98 -0
  16. package/frontend/src/document-details/document-details.html +231 -19
  17. package/frontend/src/document-details/document-details.js +328 -3
  18. package/frontend/src/document-details/document-property/document-property.css +15 -0
  19. package/frontend/src/document-details/document-property/document-property.html +75 -31
  20. package/frontend/src/document-details/document-property/document-property.js +43 -2
  21. package/frontend/src/edit-boolean/edit-boolean.html +47 -0
  22. package/frontend/src/edit-boolean/edit-boolean.js +38 -0
  23. package/frontend/src/models/models.js +79 -30
  24. package/frontend/src/mothership.js +4 -0
  25. package/frontend/src/navbar/navbar.html +1 -1
  26. package/frontend/src/team/team.html +63 -4
  27. package/frontend/src/team/team.js +47 -1
  28. package/package.json +1 -1
@@ -9,7 +9,54 @@ appendCSS(require('./document-details.css'));
9
9
 
10
10
  module.exports = app => app.component('document-details', {
11
11
  template,
12
- props: ['document', 'schemaPaths', 'editting', 'changes', 'invalid'],
12
+ props: ['document', 'schemaPaths', 'virtualPaths', 'editting', 'changes', 'invalid', 'viewMode'],
13
+ data() {
14
+ return {
15
+ searchQuery: '',
16
+ selectedType: '',
17
+ collapsedVirtuals: new Set(),
18
+ showAddFieldModal: false,
19
+ fieldData: {
20
+ name: '',
21
+ type: '',
22
+ value: ''
23
+ },
24
+ fieldErrors: {},
25
+ isSubmittingField: false,
26
+ fieldValueEditor: null
27
+ };
28
+ },
29
+ mounted() {
30
+ // Focus on search input when component loads
31
+ this.$nextTick(() => {
32
+ if (this.$refs.searchInput) {
33
+ this.$refs.searchInput.focus();
34
+ }
35
+
36
+ if (this.showAddFieldModal) {
37
+ this.initializeFieldValueEditor();
38
+ }
39
+ });
40
+ },
41
+ beforeDestroy() {
42
+ this.destroyFieldValueEditor();
43
+ },
44
+ watch: {
45
+ 'fieldData.type'(newType, oldType) {
46
+ // When field type changes, we need to handle the transition
47
+ if (newType !== oldType) {
48
+ // Destroy existing CodeMirror if it exists
49
+ this.destroyFieldValueEditor();
50
+
51
+ // If switching to a type that needs CodeMirror, initialize it
52
+ if (this.shouldUseCodeMirror) {
53
+ this.$nextTick(() => {
54
+ this.initializeFieldValueEditor();
55
+ });
56
+ }
57
+ }
58
+ }
59
+ },
13
60
  computed: {
14
61
  virtuals() {
15
62
  if (this.schemaPaths == null) {
@@ -23,11 +70,289 @@ module.exports = app => app.component('document-details', {
23
70
  const result = [];
24
71
  for (let i = 0; i < docKeys.length; i++) {
25
72
  if (!exists.includes(docKeys[i])) {
26
- result.push({ name: docKeys[i], value: this.document[docKeys[i]] });
73
+ const isVirtual = this.virtualPaths && this.virtualPaths.includes(docKeys[i]);
74
+ result.push({
75
+ name: docKeys[i],
76
+ value: this.document[docKeys[i]],
77
+ isVirtual: isVirtual,
78
+ isUserAdded: !isVirtual
79
+ });
27
80
  }
28
81
  }
29
82
 
30
83
  return result;
84
+ },
85
+ availableTypes() {
86
+ if (!this.schemaPaths) return [];
87
+ const types = new Set();
88
+ this.schemaPaths.forEach(path => {
89
+ if (path.instance) {
90
+ types.add(path.instance);
91
+ }
92
+ });
93
+
94
+ // Add virtual field types to the available types
95
+ this.virtuals.forEach(virtual => {
96
+ const virtualType = this.getVirtualFieldType(virtual);
97
+ if (virtualType && virtualType !== 'unknown') {
98
+ types.add(virtualType);
99
+ }
100
+ });
101
+
102
+ return Array.from(types).sort();
103
+ },
104
+ allFieldTypes() {
105
+ const schemaTypes = this.availableTypes;
106
+ const commonTypes = ['String', 'Number', 'Boolean', 'Date', 'Array', 'Object'];
107
+
108
+ // Combine schema types with common types, avoiding duplicates
109
+ const allTypes = new Set([...schemaTypes, ...commonTypes]);
110
+ return Array.from(allTypes).sort();
111
+ },
112
+ shouldUseCodeMirror() {
113
+ return ['Array', 'Object', 'Embedded'].includes(this.fieldData.type);
114
+ },
115
+ shouldUseDatePicker() {
116
+ return this.fieldData.type === 'Date';
117
+ },
118
+ filteredSchemaPaths() {
119
+ let paths = this.schemaPaths || [];
120
+
121
+ // Filter by search query
122
+ if (this.searchQuery.trim()) {
123
+ const query = this.searchQuery.toLowerCase();
124
+ paths = paths.filter(path =>
125
+ path.path.toLowerCase().includes(query)
126
+ );
127
+ }
128
+
129
+ // Filter by data type
130
+ if (this.selectedType) {
131
+ paths = paths.filter(path =>
132
+ path.instance === this.selectedType
133
+ );
134
+ }
135
+
136
+ return paths;
137
+ },
138
+ filteredVirtuals() {
139
+ let virtuals = this.virtuals;
140
+
141
+ // Filter by search query
142
+ if (this.searchQuery.trim()) {
143
+ const query = this.searchQuery.toLowerCase();
144
+ virtuals = virtuals.filter(virtual =>
145
+ virtual.name.toLowerCase().includes(query)
146
+ );
147
+ }
148
+
149
+ // Filter by data type
150
+ if (this.selectedType) {
151
+ virtuals = virtuals.filter(virtual => {
152
+ const virtualType = this.getVirtualFieldType(virtual);
153
+ return virtualType === this.selectedType;
154
+ });
155
+ }
156
+
157
+ return virtuals;
158
+ },
159
+ formattedJson() {
160
+ if (!this.document) {
161
+ return '{}';
162
+ }
163
+ return JSON.stringify(this.document, null, 2);
164
+ }
165
+ },
166
+ methods: {
167
+ toggleVirtualField(fieldName) {
168
+ if (this.collapsedVirtuals.has(fieldName)) {
169
+ this.collapsedVirtuals.delete(fieldName);
170
+ } else {
171
+ this.collapsedVirtuals.add(fieldName);
172
+ }
173
+ },
174
+ isVirtualFieldCollapsed(fieldName) {
175
+ return this.collapsedVirtuals.has(fieldName);
176
+ },
177
+ openAddFieldModal() {
178
+ this.showAddFieldModal = true;
179
+ this.$nextTick(() => {
180
+ if (this.shouldUseCodeMirror) {
181
+ this.initializeFieldValueEditor();
182
+ }
183
+ });
184
+ },
185
+ closeAddFieldModal() {
186
+ this.showAddFieldModal = false;
187
+ this.destroyFieldValueEditor();
188
+ this.resetFieldForm();
189
+ },
190
+ async addNewField(fieldData) {
191
+ // Emit event to parent component to handle the field addition
192
+ this.$emit('add-field', fieldData);
193
+ this.closeAddFieldModal();
194
+ },
195
+ validateFieldForm() {
196
+ this.fieldErrors = {};
197
+
198
+ // Validate field name
199
+ const trimmedName = this.fieldData.name.trim();
200
+ if (!trimmedName) {
201
+ this.fieldErrors.name = 'Field name is required';
202
+ } else {
203
+ const transformedName = this.getTransformedFieldName();
204
+ if (!transformedName) {
205
+ this.fieldErrors.name = 'Field name contains only invalid characters';
206
+ } else if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(transformedName)) {
207
+ this.fieldErrors.name = 'Field name must start with a letter, underscore, or $ and contain only letters, numbers, underscores, and $';
208
+ }
209
+ }
210
+
211
+ // Validate field type
212
+ if (!this.fieldData.type) {
213
+ this.fieldErrors.type = 'Field type is required';
214
+ }
215
+
216
+ // Validate field value if provided
217
+ if (this.fieldData.value && this.fieldData.value.trim()) {
218
+ if (['Object', 'Array'].includes(this.fieldData.type)) {
219
+ try {
220
+ JSON.parse(this.fieldData.value);
221
+ } catch (e) {
222
+ this.fieldErrors.value = 'Invalid JSON format for object/array type';
223
+ }
224
+ } else if (this.fieldData.type === 'Number') {
225
+ if (isNaN(Number(this.fieldData.value))) {
226
+ this.fieldErrors.value = 'Invalid number format';
227
+ }
228
+ } else if (this.fieldData.type === 'Boolean') {
229
+ const lowerValue = this.fieldData.value.toLowerCase();
230
+ if (!['true', 'false', '1', '0', 'yes', 'no'].includes(lowerValue)) {
231
+ this.fieldErrors.value = 'Invalid boolean value (use true/false, 1/0, yes/no)';
232
+ }
233
+ } else if (this.fieldData.type === 'Date') {
234
+ // Date picker provides YYYY-MM-DD format, validate it
235
+ const dateValue = new Date(this.fieldData.value);
236
+ if (isNaN(dateValue.getTime())) {
237
+ this.fieldErrors.value = 'Invalid date format';
238
+ }
239
+ }
240
+ }
241
+
242
+ return Object.keys(this.fieldErrors).length === 0;
243
+ },
244
+ parseFieldValue(value, type) {
245
+ if (!value || !value.trim()) {
246
+ return null;
247
+ }
248
+
249
+ switch (type) {
250
+ case 'Number':
251
+ return Number(value);
252
+ case 'Boolean':
253
+ const lowerValue = value.toLowerCase();
254
+ return ['true', '1', 'yes'].includes(lowerValue);
255
+ case 'Date':
256
+ // For date picker, value is already in YYYY-MM-DD format
257
+ return new Date(value);
258
+ case 'Object':
259
+ case 'Array':
260
+ return JSON.parse(value);
261
+ default:
262
+ return value;
263
+ }
264
+ },
265
+ async handleAddFieldSubmit() {
266
+ if (!this.validateFieldForm()) {
267
+ return;
268
+ }
269
+
270
+ this.isSubmittingField = true;
271
+
272
+ try {
273
+ const fieldData = {
274
+ name: this.getTransformedFieldName(),
275
+ type: this.fieldData.type,
276
+ value: this.parseFieldValue(this.fieldData.value, this.fieldData.type)
277
+ };
278
+
279
+ this.$emit('add-field', fieldData);
280
+ this.closeAddFieldModal();
281
+ } catch (error) {
282
+ console.error('Error adding field:', error);
283
+ this.fieldErrors.value = 'Error processing field value';
284
+ } finally {
285
+ this.isSubmittingField = false;
286
+ }
287
+ },
288
+ resetFieldForm() {
289
+ this.fieldData = {
290
+ name: '',
291
+ type: '',
292
+ value: ''
293
+ };
294
+ this.fieldErrors = {};
295
+ this.isSubmittingField = false;
296
+ // Reset CodeMirror editor if it exists
297
+ if (this.fieldValueEditor) {
298
+ this.fieldValueEditor.setValue('');
299
+ }
300
+ },
301
+ initializeFieldValueEditor() {
302
+ if (this.$refs.fieldValueEditor && !this.fieldValueEditor && this.shouldUseCodeMirror) {
303
+ this.$refs.fieldValueEditor.value = this.fieldData.value || '';
304
+ this.fieldValueEditor = CodeMirror.fromTextArea(this.$refs.fieldValueEditor, {
305
+ mode: 'javascript',
306
+ lineNumbers: true,
307
+ height: 'auto'
308
+ });
309
+ this.fieldValueEditor.on('change', () => {
310
+ this.fieldData.value = this.fieldValueEditor.getValue();
311
+ });
312
+ }
313
+ },
314
+ destroyFieldValueEditor() {
315
+ if (this.fieldValueEditor) {
316
+ this.fieldValueEditor.toTextArea();
317
+ this.fieldValueEditor = null;
318
+ }
319
+ },
320
+ toSnakeCase(str) {
321
+ return str
322
+ .trim()
323
+ .replace(/\s+/g, '_') // Replace spaces with underscores
324
+ .replace(/[^a-zA-Z0-9_$]/g, '') // Remove invalid characters
325
+ .replace(/^[0-9]/, '_$&') // Prefix numbers with underscore
326
+ .toLowerCase();
327
+ },
328
+ getTransformedFieldName() {
329
+ if (!this.fieldData.name) return '';
330
+ return this.toSnakeCase(this.fieldData.name.trim());
331
+ },
332
+ getVirtualFieldType(virtual) {
333
+ const value = virtual.value;
334
+ if (value === null || value === undefined) {
335
+ return 'null';
336
+ }
337
+ if (Array.isArray(value)) {
338
+ return 'Array';
339
+ }
340
+ if (value instanceof Date) {
341
+ return 'Date';
342
+ }
343
+ if (typeof value === 'object') {
344
+ return 'Object';
345
+ }
346
+ if (typeof value === 'number') {
347
+ return 'Number';
348
+ }
349
+ if (typeof value === 'boolean') {
350
+ return 'Boolean';
351
+ }
352
+ if (typeof value === 'string') {
353
+ return 'String';
354
+ }
355
+ return 'unknown';
31
356
  }
32
357
  }
33
- });
358
+ });
@@ -20,4 +20,19 @@
20
20
  .document-details .date-position {
21
21
  float: right;
22
22
  margin-top: -7px;
23
+ }
24
+
25
+ .truncated-value-container,
26
+ .expanded-value-container {
27
+ position: relative;
28
+ }
29
+
30
+ .truncated-value-container button,
31
+ .expanded-value-container button {
32
+ transition: all 0.2s ease;
33
+ }
34
+
35
+ .truncated-value-container button:hover,
36
+ .expanded-value-container button:hover {
37
+ transform: translateX(2px);
23
38
  }
@@ -1,22 +1,37 @@
1
- <div>
2
- <div class="relative path-key p-1 flex">
3
- <div class="grow flex justify-between items-center">
4
- <div>
5
- {{path.path}}
6
- <span class="path-type">
7
- ({{(path.instance || 'unknown').toLowerCase()}})
8
- </span>
9
- </div>
10
- <div>
11
- <router-link
12
- v-if="path.ref && getValueForPath(path.path)"
13
- :to="`/model/${path.ref}/document/${getValueForPath(path.path)}`"
14
- class="bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm mr-1 rounded-md"
15
- >View Document
16
- </router-link>
17
- </div>
1
+ <div class="border border-gray-200 rounded-lg mb-2">
2
+ <!-- Collapsible Header -->
3
+ <div
4
+ @click="toggleCollapse"
5
+ class="p-3 bg-gray-50 hover:bg-gray-100 cursor-pointer flex items-center justify-between border-b border-gray-200"
6
+ >
7
+ <div class="flex items-center">
8
+ <svg
9
+ :class="isCollapsed ? 'rotate-0' : 'rotate-90'"
10
+ class="w-4 h-4 text-gray-500 mr-2 transition-transform duration-200"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ viewBox="0 0 24 24"
14
+ >
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
16
+ </svg>
17
+ <span class="font-medium text-gray-900">{{path.path}}</span>
18
+ <span class="ml-2 text-sm text-gray-500">({{(path.instance || 'unknown').toLowerCase()}})</span>
19
+ </div>
20
+ <div class="flex items-center gap-2">
21
+ <router-link
22
+ v-if="path.ref && getValueForPath(path.path)"
23
+ :to="`/model/${path.ref}/document/${getValueForPath(path.path)}`"
24
+ class="bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md"
25
+ @click.stop
26
+ >View Document
27
+ </router-link>
18
28
  </div>
19
- <div v-if="editting && path.instance === 'Date'" class="flex gap-1.5">
29
+ </div>
30
+
31
+ <!-- Collapsible Content -->
32
+ <div v-if="!isCollapsed" class="p-3">
33
+ <!-- Date Type Selector (when editing dates) -->
34
+ <div v-if="editting && path.instance === 'Date'" class="mb-3 flex gap-1.5">
20
35
  <div
21
36
  @click="dateType = 'picker'"
22
37
  :class="dateType === 'picker' ? 'bg-teal-600' : ''"
@@ -38,18 +53,47 @@
38
53
  </div>
39
54
  </div>
40
55
  </div>
41
- </div>
42
- <div v-if="editting && path.path !== '_id'" class="pl-1">
43
- <component
44
- :is="getEditComponentForPath(path)"
45
- :value="getEditValueForPath(path)"
46
- :format="dateType"
47
- @input="changes[path.path] = $event; delete invalid[path.path];"
48
- @error="invalid[path.path] = $event;"
49
- >
50
- </component>
51
- </div>
52
- <div v-else class="pl-1">
53
- <component :is="getComponentForPath(path)" :value="getValueForPath(path.path)"></component>
56
+
57
+ <!-- Field Content -->
58
+ <div v-if="editting && path.path !== '_id'">
59
+ <component
60
+ :is="getEditComponentForPath(path)"
61
+ :value="getEditValueForPath(path)"
62
+ :format="dateType"
63
+ @input="changes[path.path] = $event; delete invalid[path.path];"
64
+ @error="invalid[path.path] = $event;"
65
+ >
66
+ </component>
67
+ </div>
68
+ <div v-else>
69
+ <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->
70
+ <div v-if="needsTruncation && !isValueExpanded" class="truncated-value-container">
71
+ <div class="text-gray-700 whitespace-pre-wrap break-words font-mono text-sm">{{truncatedString}}</div>
72
+ <button
73
+ @click="toggleValueExpansion"
74
+ class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
75
+ >
76
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
77
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
78
+ </svg>
79
+ Show more ({{valueAsString.length}} characters)
80
+ </button>
81
+ </div>
82
+ <div v-else-if="needsTruncation && isValueExpanded" class="expanded-value-container">
83
+ <component :is="getComponentForPath(path)" :value="getValueForPath(path.path)"></component>
84
+ <button
85
+ @click="toggleValueExpansion"
86
+ class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
87
+ >
88
+ <svg class="w-4 h-4 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
89
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
90
+ </svg>
91
+ Show less
92
+ </button>
93
+ </div>
94
+ <div v-else>
95
+ <component :is="getComponentForPath(path)" :value="getValueForPath(path.path)"></component>
96
+ </div>
97
+ </div>
54
98
  </div>
55
99
  </div>
@@ -11,10 +11,41 @@ module.exports = app => app.component('document-property', {
11
11
  template,
12
12
  data: function() {
13
13
  return {
14
- dateType: 'picker' // picker, iso
14
+ dateType: 'picker', // picker, iso
15
+ isCollapsed: false, // Start uncollapsed by default
16
+ isValueExpanded: false // Track if the value is expanded
15
17
  };
16
18
  },
17
19
  props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid'],
20
+ computed: {
21
+ valueAsString() {
22
+ const value = this.getValueForPath(this.path.path);
23
+ if (value == null) {
24
+ return String(value);
25
+ }
26
+ if (typeof value === 'object') {
27
+ return JSON.stringify(value, null, 2);
28
+ }
29
+ return String(value);
30
+ },
31
+ needsTruncation() {
32
+ // Truncate if value is longer than 200 characters
33
+ return this.valueAsString.length > 200;
34
+ },
35
+ displayValue() {
36
+ if (!this.needsTruncation || this.isValueExpanded) {
37
+ return this.getValueForPath(this.path.path);
38
+ }
39
+ // Return truncated value - we'll handle this in the template
40
+ return this.getValueForPath(this.path.path);
41
+ },
42
+ truncatedString() {
43
+ if (this.needsTruncation && !this.isValueExpanded) {
44
+ return this.valueAsString.substring(0, 200) + '...';
45
+ }
46
+ return this.valueAsString;
47
+ }
48
+ },
18
49
  methods: {
19
50
  getComponentForPath(schemaPath) {
20
51
  if (schemaPath.instance === 'Array') {
@@ -35,6 +66,9 @@ module.exports = app => app.component('document-property', {
35
66
  if (path.instance === 'Embedded') {
36
67
  return 'edit-subdocument';
37
68
  }
69
+ if (path.instance === 'Boolean') {
70
+ return 'edit-boolean';
71
+ }
38
72
  return 'edit-default';
39
73
  },
40
74
  getValueForPath(path) {
@@ -50,7 +84,14 @@ module.exports = app => app.component('document-property', {
50
84
  if (!this.document) {
51
85
  return;
52
86
  }
53
- return path in this.changes ? this.changes[path] : mpath.get(path, this.document);
87
+ const documentValue = mpath.get(path, this.document);
88
+ return documentValue;
89
+ },
90
+ toggleCollapse() {
91
+ this.isCollapsed = !this.isCollapsed;
92
+ },
93
+ toggleValueExpansion() {
94
+ this.isValueExpanded = !this.isValueExpanded;
54
95
  }
55
96
  }
56
97
  });
@@ -0,0 +1,47 @@
1
+ <div class="edit-boolean">
2
+ <div class="flex flex-wrap gap-2">
3
+ <label class="flex items-center gap-2 px-3 py-2 border rounded cursor-pointer hover:bg-gray-50"
4
+ :class="selectedValue === true ? 'bg-blue-100 border-blue-300 text-blue-800' : 'border-gray-300 text-gray-700'">
5
+ <input
6
+ type="radio"
7
+ :checked="selectedValue === true"
8
+ @change="selectValue(true)"
9
+ class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
10
+ />
11
+ <span class="text-sm font-medium">true</span>
12
+ </label>
13
+
14
+ <label class="flex items-center gap-2 px-3 py-2 border rounded cursor-pointer hover:bg-gray-50"
15
+ :class="selectedValue === false ? 'bg-blue-100 border-blue-300 text-blue-800' : 'border-gray-300 text-gray-700'">
16
+ <input
17
+ type="radio"
18
+ :checked="selectedValue === false"
19
+ @change="selectValue(false)"
20
+ class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
21
+ />
22
+ <span class="text-sm font-medium">false</span>
23
+ </label>
24
+
25
+ <label class="flex items-center gap-2 px-3 py-2 border rounded cursor-pointer hover:bg-gray-50"
26
+ :class="selectedValue === null ? 'bg-blue-100 border-blue-300 text-blue-800' : 'border-gray-300 text-gray-700'">
27
+ <input
28
+ type="radio"
29
+ :checked="selectedValue === null"
30
+ @change="selectValue(null)"
31
+ class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
32
+ />
33
+ <span class="text-sm font-medium">null</span>
34
+ </label>
35
+
36
+ <label class="flex items-center gap-2 px-3 py-2 border rounded cursor-pointer hover:bg-gray-50"
37
+ :class="selectedValue === undefined ? 'bg-blue-100 border-blue-300 text-blue-800' : 'border-gray-300 text-gray-700'">
38
+ <input
39
+ type="radio"
40
+ :checked="selectedValue === undefined"
41
+ @change="selectValue(undefined)"
42
+ class="w-4 h-4 text-blue-600 border-gray-300 focus:ring-blue-500"
43
+ />
44
+ <span class="text-sm font-medium">undefined</span>
45
+ </label>
46
+ </div>
47
+ </div>
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const template = require('./edit-boolean.html');
4
+
5
+ module.exports = app => app.component('edit-boolean', {
6
+ template: template,
7
+ props: ['value'],
8
+ emits: ['input'],
9
+ data() {
10
+ return {
11
+ selectedValue: null
12
+ };
13
+ },
14
+ mounted() {
15
+ this.selectedValue = this.value;
16
+ },
17
+ watch: {
18
+ value(newValue) {
19
+ this.selectedValue = newValue;
20
+ },
21
+ selectedValue(newValue) {
22
+ // Convert null/undefined to strings for proper backend serialization
23
+ const emitValue = this.convertValueToString(newValue);
24
+ this.$emit('input', emitValue);
25
+ }
26
+ },
27
+ methods: {
28
+ selectValue(value) {
29
+ this.selectedValue = value;
30
+ },
31
+ convertValueToString(value) {
32
+ // Convert null/undefined to strings for proper backend serialization
33
+ if (value === null) return 'null';
34
+ if (typeof value === 'undefined') return 'undefined';
35
+ return value;
36
+ }
37
+ }
38
+ });