@mongoosejs/studio 0.0.128 → 0.0.130
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/addField.js +54 -0
- package/backend/actions/Model/getDocument.js +3 -1
- package/backend/actions/Model/getDocuments.js +14 -5
- package/backend/actions/Model/getDocumentsStream.js +14 -5
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/updateDocument.js +23 -6
- package/backend/actions/Model/updateDocuments.js +23 -5
- package/frontend/public/app.js +613 -45
- package/frontend/public/tw.css +219 -2
- package/frontend/src/api.js +6 -0
- package/frontend/src/document/document.css +8 -0
- package/frontend/src/document/document.html +102 -8
- package/frontend/src/document/document.js +34 -1
- package/frontend/src/document-details/document-details.css +98 -0
- package/frontend/src/document-details/document-details.html +231 -19
- package/frontend/src/document-details/document-details.js +328 -3
- package/frontend/src/document-details/document-property/document-property.css +15 -0
- package/frontend/src/document-details/document-property/document-property.html +75 -31
- package/frontend/src/document-details/document-property/document-property.js +43 -2
- package/frontend/src/edit-boolean/edit-boolean.html +47 -0
- package/frontend/src/edit-boolean/edit-boolean.js +38 -0
- package/frontend/src/models/models.js +79 -30
- package/frontend/src/mothership.js +4 -0
- package/frontend/src/navbar/navbar.html +1 -1
- package/frontend/src/team/team.html +63 -4
- package/frontend/src/team/team.js +47 -1
- 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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
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
|
-
|
|
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
|
+
});
|