@mongoosejs/studio 0.2.2 → 0.2.4

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.
@@ -16,13 +16,16 @@ const UpdateDocumentsParams = new Archetype({
16
16
  $type: Object,
17
17
  $required: true
18
18
  },
19
+ unset: {
20
+ $type: Object
21
+ },
19
22
  roles: {
20
23
  $type: ['string']
21
24
  }
22
25
  }).compile('UpdateDocumentsParams');
23
26
 
24
27
  module.exports = ({ db }) => async function updateDocument(params) {
25
- const { model, _id, update, roles } = new UpdateDocumentsParams(params);
28
+ const { model, _id, update, unset, roles } = new UpdateDocumentsParams(params);
26
29
 
27
30
  await authorize('Model.updateDocument', roles);
28
31
 
@@ -32,16 +35,11 @@ module.exports = ({ db }) => async function updateDocument(params) {
32
35
  }
33
36
 
34
37
  const setFields = {};
35
- const unsetFields = {};
36
-
38
+ const unsetFields = unset || {};
39
+
37
40
  if (Object.keys(update).length > 0) {
38
41
  Object.entries(update).forEach(([key, value]) => {
39
- if (value === 'null') {
40
- setFields[key] = null;
41
- } else if (value === 'undefined') {
42
- // Use $unset to remove the field for undefined values
43
- unsetFields[key] = 1;
44
- } else if (value === '') {
42
+ if (value === '') {
45
43
  // Treat empty strings as undefined - unset the field
46
44
  unsetFields[key] = 1;
47
45
  } else {
@@ -9,6 +9,10 @@
9
9
 
10
10
  var map = {
11
11
  "./": "./frontend/src/index.js",
12
+ "./_util/baseComponent": "./frontend/src/_util/baseComponent.js",
13
+ "./_util/baseComponent.js": "./frontend/src/_util/baseComponent.js",
14
+ "./_util/deepEqual": "./frontend/src/_util/deepEqual.js",
15
+ "./_util/deepEqual.js": "./frontend/src/_util/deepEqual.js",
12
16
  "./_util/document-search-autocomplete": "./frontend/src/_util/document-search-autocomplete.js",
13
17
  "./_util/document-search-autocomplete.js": "./frontend/src/_util/document-search-autocomplete.js",
14
18
  "./api": "./frontend/src/api.js",
@@ -208,6 +212,100 @@ webpackContext.resolve = webpackContextResolve;
208
212
  module.exports = webpackContext;
209
213
  webpackContext.id = "./frontend/src sync recursive ^\\.\\/.*$";
210
214
 
215
+ /***/ },
216
+
217
+ /***/ "./frontend/src/_util/baseComponent.js"
218
+ /*!*********************************************!*\
219
+ !*** ./frontend/src/_util/baseComponent.js ***!
220
+ \*********************************************/
221
+ (module) {
222
+
223
+ "use strict";
224
+
225
+
226
+ module.exports = {
227
+ destroyed() {
228
+ this.$parent.$options.$children = this.$parent.$options.$children.filter(
229
+ (el) => el !== this
230
+ );
231
+ },
232
+ created() {
233
+ this.$parent.$options.$children = this.$parent.$options.$children || [];
234
+ this.$parent.$options.$children.push(this);
235
+ },
236
+ mounted() {
237
+ this.isMounted = true;
238
+ },
239
+ unmounted() {
240
+ this.isMounted = false;
241
+ }
242
+ };
243
+
244
+
245
+ /***/ },
246
+
247
+ /***/ "./frontend/src/_util/deepEqual.js"
248
+ /*!*****************************************!*\
249
+ !*** ./frontend/src/_util/deepEqual.js ***!
250
+ \*****************************************/
251
+ (module) {
252
+
253
+ "use strict";
254
+
255
+
256
+ /**
257
+ * Deep equality check for values (handles primitives, arrays, objects, dates, etc.)
258
+ * @param {*} a - First value to compare
259
+ * @param {*} b - Second value to compare
260
+ * @returns {boolean} - True if values are deeply equal
261
+ */
262
+ function deepEqual(a, b) {
263
+ // Handle primitives and same reference
264
+ if (a === b) return true;
265
+
266
+ // Handle null and undefined
267
+ if (a == null || b == null) return a === b;
268
+
269
+ // Handle different types
270
+ if (typeof a !== typeof b) return false;
271
+
272
+ // Handle dates
273
+ if (a instanceof Date && b instanceof Date) {
274
+ return a.getTime() === b.getTime();
275
+ }
276
+
277
+ // Handle arrays - must both be arrays
278
+ if (Array.isArray(a) || Array.isArray(b)) {
279
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
280
+ if (a.length !== b.length) return false;
281
+ for (let i = 0; i < a.length; i++) {
282
+ if (!deepEqual(a[i], b[i])) return false;
283
+ }
284
+ return true;
285
+ }
286
+
287
+ // Handle objects (non-arrays)
288
+ if (typeof a === 'object' && typeof b === 'object') {
289
+ const keysA = Object.keys(a);
290
+ const keysB = Object.keys(b);
291
+
292
+ if (keysA.length !== keysB.length) return false;
293
+
294
+ for (const key of keysA) {
295
+ if (!keysB.includes(key)) return false;
296
+ if (!deepEqual(a[key], b[key])) return false;
297
+ }
298
+
299
+ return true;
300
+ }
301
+
302
+ // Fallback for primitives (strings, numbers, booleans)
303
+ return false;
304
+ }
305
+
306
+ module.exports = deepEqual;
307
+
308
+
211
309
  /***/ },
212
310
 
213
311
  /***/ "./frontend/src/_util/document-search-autocomplete.js"
@@ -855,6 +953,7 @@ module.exports = function appendCSS(css) {
855
953
 
856
954
 
857
955
  const { inspect } = __webpack_require__(/*! node-inspect-extracted */ "./node_modules/node-inspect-extracted/dist/inspect.js");
956
+ const deepEqual = __webpack_require__(/*! ./_util/deepEqual */ "./frontend/src/_util/deepEqual.js");
858
957
 
859
958
  /**
860
959
  * Format a value for display in array views
@@ -2254,10 +2353,12 @@ module.exports = "<div class=\"dashboard px-1\">\n <div v-if=\"status === 'load
2254
2353
 
2255
2354
 
2256
2355
  const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
2356
+ const baseComponent = __webpack_require__(/*! ../_util/baseComponent */ "./frontend/src/_util/baseComponent.js");
2257
2357
  const template = __webpack_require__(/*! ./dashboard.html */ "./frontend/src/dashboard/dashboard.html");
2258
2358
 
2259
- module.exports = app => app.component('dashboard', {
2359
+ module.exports = {
2260
2360
  template: template,
2361
+ extends: baseComponent,
2261
2362
  props: ['dashboardId'],
2262
2363
  data: function() {
2263
2364
  return {
@@ -2301,7 +2402,7 @@ module.exports = app => app.component('dashboard', {
2301
2402
  }
2302
2403
  },
2303
2404
  shouldEvaluateDashboard() {
2304
- if (this.dashboardResults.length === 0) {
2405
+ if (!this.dashboardResults || this.dashboardResults.length === 0) {
2305
2406
  return true;
2306
2407
  }
2307
2408
 
@@ -2368,6 +2469,22 @@ module.exports = app => app.component('dashboard', {
2368
2469
  this.startingChat = false;
2369
2470
  this.showActionsMenu = false;
2370
2471
  }
2472
+ },
2473
+ async loadInitial() {
2474
+ const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
2475
+ if (!dashboard) {
2476
+ return;
2477
+ }
2478
+ this.dashboard = dashboard;
2479
+ this.code = this.dashboard.code;
2480
+ this.title = this.dashboard.title;
2481
+ this.description = this.dashboard.description ?? '';
2482
+ this.dashboardResults = dashboardResults;
2483
+ if (this.shouldEvaluateDashboard()) {
2484
+ await this.evaluateDashboard();
2485
+ return;
2486
+ }
2487
+ this.status = 'loaded';
2371
2488
  }
2372
2489
  },
2373
2490
  computed: {
@@ -2378,25 +2495,12 @@ module.exports = app => app.component('dashboard', {
2378
2495
  mounted: async function() {
2379
2496
  document.addEventListener('click', this.handleDocumentClick);
2380
2497
  this.showEditor = this.$route.query.edit;
2381
- const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
2382
- if (!dashboard) {
2383
- return;
2384
- }
2385
- this.dashboard = dashboard;
2386
- this.code = this.dashboard.code;
2387
- this.title = this.dashboard.title;
2388
- this.description = this.dashboard.description ?? '';
2389
- this.dashboardResults = dashboardResults;
2390
- if (this.shouldEvaluateDashboard()) {
2391
- await this.evaluateDashboard();
2392
- return;
2393
- }
2394
- this.status = 'loaded';
2498
+ await this.loadInitial();
2395
2499
  },
2396
2500
  beforeDestroy() {
2397
2501
  document.removeEventListener('click', this.handleDocumentClick);
2398
2502
  }
2399
- });
2503
+ };
2400
2504
 
2401
2505
 
2402
2506
  /***/ },
@@ -3181,7 +3285,7 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
3181
3285
  (module) {
3182
3286
 
3183
3287
  "use strict";
3184
- module.exports = "<div class=\"border border-gray-200 bg-white rounded-lg mb-2\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-gray-100': !highlight }\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-gray-500\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-gray-300 bg-white\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <component\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"changes[path.path] = $event; delete invalid[path.path];\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-gray-500 italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-gray-700 whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n </div>\n </div>\n </div>\n</div>\n";
3288
+ module.exports = "<div class=\"border border-gray-200 bg-white rounded-lg mb-2\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-gray-100': !highlight }\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-gray-500\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-gray-300 bg-white\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <component\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-gray-500 italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-gray-700 whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n </div>\n </div>\n </div>\n</div>\n";
3185
3289
 
3186
3290
  /***/ },
3187
3291
 
@@ -3197,7 +3301,7 @@ module.exports = "<div class=\"border border-gray-200 bg-white rounded-lg mb-2\"
3197
3301
 
3198
3302
 
3199
3303
  const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
3200
- const { inspect } = __webpack_require__(/*! node-inspect-extracted */ "./node_modules/node-inspect-extracted/dist/inspect.js");
3304
+ const deepEqual = __webpack_require__(/*! ../../_util/deepEqual */ "./frontend/src/_util/deepEqual.js");
3201
3305
  const template = __webpack_require__(/*! ./document-property.html */ "./frontend/src/document-details/document-property/document-property.html");
3202
3306
 
3203
3307
  const appendCSS = __webpack_require__(/*! ../../appendCSS */ "./frontend/src/appendCSS.js");
@@ -3289,6 +3393,20 @@ module.exports = app => app.component('document-property', {
3289
3393
  }
3290
3394
  },
3291
3395
  methods: {
3396
+ handleInputChange(newValue) {
3397
+ const currentValue = this.getValueForPath(this.path.path);
3398
+
3399
+ // Only record as a change if the value is actually different
3400
+ if (!deepEqual(currentValue, newValue)) {
3401
+ this.changes[this.path.path] = newValue;
3402
+ } else {
3403
+ // If the value is the same as the original, remove it from changes
3404
+ delete this.changes[this.path.path];
3405
+ }
3406
+
3407
+ // Always clear invalid state on input
3408
+ delete this.invalid[this.path.path];
3409
+ },
3292
3410
  getComponentForPath(schemaPath) {
3293
3411
  if (schemaPath.instance === 'Array') {
3294
3412
  return 'detail-array';
@@ -3434,6 +3552,16 @@ module.exports = app => app.component('confirm-changes', {
3434
3552
  props: ['value'],
3435
3553
  computed: {
3436
3554
  displayValue() {
3555
+ const hasUnsetFields = Object.keys(this.value).some(key => this.value[key] === undefined);
3556
+ if (hasUnsetFields) {
3557
+ const unsetFields = Object.keys(this.value)
3558
+ .filter(key => this.value[key] === undefined)
3559
+ .reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
3560
+ const setFields = Object.keys(this.value)
3561
+ .filter(key => this.value[key] !== undefined)
3562
+ .reduce((obj, key) => Object.assign(obj, { [key]: this.value[key] }), {});
3563
+ return JSON.stringify({ $set: setFields, $unset: unsetFields }, null, ' ').trim();
3564
+ }
3437
3565
  return JSON.stringify(this.value, null, ' ').trim();
3438
3566
  }
3439
3567
  },
@@ -3450,6 +3578,7 @@ module.exports = app => app.component('confirm-changes', {
3450
3578
  }
3451
3579
  });
3452
3580
 
3581
+
3453
3582
  /***/ },
3454
3583
 
3455
3584
  /***/ "./frontend/src/document/confirm-delete/confirm-delete.html"
@@ -3515,7 +3644,7 @@ module.exports = ".document .document-menu {\n display: flex;\n position: stic
3515
3644
  (module) {
3516
3645
 
3517
3646
  "use strict";
3518
- module.exports = "<div class=\"document px-1 pt-4 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n class=\"rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n &times; Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-ultramarine-100']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n :model=\"model\"\n @add-field=\"addField\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">&times;</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">&times;</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">&times;</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n </div>\n </div>\n</div>\n";
3647
+ module.exports = "<div class=\"document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full\">\n <div class=\"max-w-7xl mx-auto\">\n <div class=\"flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm\">\n <div class=\"font-bold overflow-hidden text-ellipsis\">{{model}}: {{documentId}}</div>\n <div class=\"flex grow\">\n <button\n @click=\"viewMode = 'fields'\"\n :class=\"viewMode === 'fields'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 border-r-0 rounded-l-lg rounded-r-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 6h16M4 10h16M4 14h16M4 18h16\"></path>\n </svg>\n Fields\n </button>\n <button\n @click=\"viewMode = 'json'\"\n :class=\"viewMode === 'json'\n ? 'bg-blue-600 text-white z-10'\n : 'bg-gray-200 text-gray-700 hover:bg-gray-300'\"\n class=\"px-2 py-1.5 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 flex items-center gap-2 border border-gray-300 rounded-r-lg rounded-l-none\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4\"></path>\n </svg>\n JSON\n </button>\n </div>\n\n <div class=\"gap-2 hidden md:flex items-center\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true\"\n :disabled=\"!canEdit\"\n :class=\"{'cursor-not-allowed opacity-50': !canEdit}\"\n type=\"button\"\n class=\"rounded-md bg-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n <img src=\"images/edit.svg\" class=\"inline\" /> Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false\"\n type=\"button\"\n class=\"rounded-md bg-slate-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-slate-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-600\">\n &times; Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"{'cursor-not-allowed opacity-50': !canManipulate}\"\n @click=\"shouldShowConfirmModal=true;\"\n type=\"button\"\n class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">\n <img src=\"images/save.svg\" class=\"inline\" /> Save\n </button>\n\n <!-- 3-dot menu -->\n <div class=\"relative\">\n <button\n @click=\"desktopMenuOpen = !desktopMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"desktopMenuOpen\"\n aria-label=\"More options\"\n >\n <svg class=\"w-5 h-5\" fill=\"currentColor\" viewBox=\"0 0 24 24\">\n <circle cx=\"12\" cy=\"5\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"12\" r=\"2\"></circle>\n <circle cx=\"12\" cy=\"19\" r=\"2\"></circle>\n </svg>\n </button>\n <div\n v-show=\"desktopMenuOpen\"\n @click.away=\"desktopMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n @click=\"addField(); desktopMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; desktopMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n <div class=\"md:hidden flex items-center\">\n <div class=\"relative\">\n <button\n @click=\"mobileMenuOpen = !mobileMenuOpen\"\n type=\"button\"\n class=\"inline-flex items-center justify-center rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500\"\n aria-expanded=\"mobileMenuOpen\"\n aria-label=\"Open menu\"\n >\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"\n d=\"M4 6h16M4 12h16M4 18h16\"></path>\n </svg>\n </button>\n <div\n v-show=\"mobileMenuOpen\"\n @click.away=\"mobileMenuOpen = false\"\n class=\"origin-top-right absolute right-0 mt-2 w-52 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50\"\n >\n <div class=\"py-1 flex flex-col\">\n <button\n v-if=\"!editting\"\n @click=\"editting = true; mobileMenuOpen = false\"\n :disabled=\"!canEdit\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canEdit ? 'cursor-not-allowed opacity-50' : 'hover:bg-ultramarine-100']\"\n type=\"button\"\n >\n Edit\n </button>\n <button\n v-if=\"editting\"\n @click=\"editting = false; mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-slate-100\"\n >\n Cancel\n </button>\n <button\n v-if=\"editting\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-green-100']\"\n @click=\"shouldShowConfirmModal=true; mobileMenuOpen = false\"\n type=\"button\"\n >\n Save\n </button>\n <button\n @click=\"addField(); mobileMenuOpen = false\"\n type=\"button\"\n class=\"flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-green-100\"\n >\n Add Field\n </button>\n <button\n @click=\"shouldShowDeleteModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-red-100']\"\n type=\"button\"\n >\n Delete\n </button>\n <button\n @click=\"shouldShowCloneModal=true; mobileMenuOpen = false\"\n :disabled=\"!canManipulate\"\n :class=\"['flex items-center px-4 py-2 text-sm text-gray-700', !canManipulate ? 'cursor-not-allowed opacity-50' : 'hover:bg-pink-100']\"\n type=\"button\"\n >\n Clone\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n <div v-if=\"status === 'loaded'\">\n <document-details\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :virtualPaths=\"virtualPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"\n :viewMode=\"viewMode\"\n :model=\"model\"\n @add-field=\"addField\"\n @view-mode-change=\"updateViewMode\"></document-details>\n <modal v-if=\"shouldShowConfirmModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowConfirmModal = false;\">&times;</div>\n <confirm-changes @close=\"shouldShowConfirmModal = false;\" @save=\"save\" :value=\"changes\"></confirm-changes>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteModal = false;\">&times;</div>\n <confirm-delete @close=\"shouldShowDeleteModal = false;\" @remove=\"remove\" :value=\"document\"></confirm-delete>\n </template>\n </modal>\n <modal v-if=\"shouldShowCloneModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCloneModal = false;\">&times;</div>\n <clone-document :currentModel=\"model\" :doc=\"document\" :schemaPaths=\"schemaPaths\" @close=\"showClonedDocument\"></clone-document>\n </template>\n </modal>\n </div>\n </div>\n</div>\n";
3519
3648
 
3520
3649
  /***/ },
3521
3650
 
@@ -3602,10 +3731,25 @@ module.exports = app => app.component('document', {
3602
3731
  if (Object.keys(this.invalid).length > 0) {
3603
3732
  throw new Error('Invalid paths: ' + Object.keys(this.invalid).join(', '));
3604
3733
  }
3734
+
3735
+ let update = this.changes;
3736
+ let unset = {};
3737
+ const hasUnsetFields = Object.keys(this.changes)
3738
+ .some(key => this.changes[key] === undefined);
3739
+ if (hasUnsetFields) {
3740
+ unset = Object.keys(this.changes)
3741
+ .filter(key => this.changes[key] === undefined)
3742
+ .reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
3743
+ update = Object.keys(this.changes)
3744
+ .filter(key => this.changes[key] !== undefined)
3745
+ .reduce((obj, key) => Object.assign(obj, { [key]: this.changes[key] }), {});
3746
+ }
3747
+
3605
3748
  const { doc } = await api.Model.updateDocument({
3606
3749
  model: this.model,
3607
3750
  _id: this.document._id,
3608
- update: this.changes
3751
+ update,
3752
+ unset
3609
3753
  });
3610
3754
  this.document = doc;
3611
3755
  this.changes = {};
@@ -3833,20 +3977,12 @@ module.exports = app => app.component('edit-boolean', {
3833
3977
  this.selectedValue = newValue;
3834
3978
  },
3835
3979
  selectedValue(newValue) {
3836
- // Convert null/undefined to strings for proper backend serialization
3837
- const emitValue = this.convertValueToString(newValue);
3838
- this.$emit('input', emitValue);
3980
+ this.$emit('input', newValue);
3839
3981
  }
3840
3982
  },
3841
3983
  methods: {
3842
3984
  selectValue(value) {
3843
3985
  this.selectedValue = value;
3844
- },
3845
- convertValueToString(value) {
3846
- // Convert null/undefined to strings for proper backend serialization
3847
- if (value === null) return 'null';
3848
- if (typeof value === 'undefined') return 'undefined';
3849
- return value;
3850
3986
  }
3851
3987
  }
3852
3988
  });
@@ -4398,7 +4534,11 @@ requireComponents.keys().forEach((filePath) => {
4398
4534
  // Check if the file name matches the directory name
4399
4535
  if (directoryName === fileName) {
4400
4536
  components[directoryName] = requireComponents(filePath);
4401
- components[directoryName](app);
4537
+ if (typeof components[directoryName] === 'function') {
4538
+ components[directoryName](app);
4539
+ } else {
4540
+ app.component(directoryName, components[directoryName]);
4541
+ }
4402
4542
  }
4403
4543
  });
4404
4544
 
@@ -4470,7 +4610,7 @@ app.component('app-component', {
4470
4610
 
4471
4611
  this.user = user;
4472
4612
  this.roles = roles;
4473
-
4613
+
4474
4614
  setTimeout(() => {
4475
4615
  this.$router.replace(this.$router.currentRoute.value.path);
4476
4616
  }, 0);
@@ -6949,7 +7089,7 @@ __webpack_require__.r(__webpack_exports__);
6949
7089
  /* harmony export */ });
6950
7090
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
6951
7091
  /**
6952
- * @vue/reactivity v3.5.26
7092
+ * @vue/reactivity v3.5.27
6953
7093
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
6954
7094
  * @license MIT
6955
7095
  **/
@@ -8078,20 +8218,20 @@ function createIterableMethod(method, isReadonly2, isShallow2) {
8078
8218
  "iterate",
8079
8219
  isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
8080
8220
  );
8081
- return {
8082
- // iterator protocol
8083
- next() {
8084
- const { value, done } = innerIterator.next();
8085
- return done ? { value, done } : {
8086
- value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
8087
- done
8088
- };
8089
- },
8090
- // iterable protocol
8091
- [Symbol.iterator]() {
8092
- return this;
8221
+ return (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.extend)(
8222
+ // inheriting all iterator properties
8223
+ Object.create(innerIterator),
8224
+ {
8225
+ // iterator protocol
8226
+ next() {
8227
+ const { value, done } = innerIterator.next();
8228
+ return done ? { value, done } : {
8229
+ value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
8230
+ done
8231
+ };
8232
+ }
8093
8233
  }
8094
- };
8234
+ );
8095
8235
  };
8096
8236
  }
8097
8237
  function createReadonlyMethod(type) {
@@ -8305,8 +8445,9 @@ function targetTypeMap(rawType) {
8305
8445
  function getTargetType(value) {
8306
8446
  return value["__v_skip"] || !Object.isExtensible(value) ? 0 /* INVALID */ : targetTypeMap((0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.toRawType)(value));
8307
8447
  }
8448
+ // @__NO_SIDE_EFFECTS__
8308
8449
  function reactive(target) {
8309
- if (isReadonly(target)) {
8450
+ if (/* @__PURE__ */ isReadonly(target)) {
8310
8451
  return target;
8311
8452
  }
8312
8453
  return createReactiveObject(
@@ -8317,6 +8458,7 @@ function reactive(target) {
8317
8458
  reactiveMap
8318
8459
  );
8319
8460
  }
8461
+ // @__NO_SIDE_EFFECTS__
8320
8462
  function shallowReactive(target) {
8321
8463
  return createReactiveObject(
8322
8464
  target,
@@ -8326,6 +8468,7 @@ function shallowReactive(target) {
8326
8468
  shallowReactiveMap
8327
8469
  );
8328
8470
  }
8471
+ // @__NO_SIDE_EFFECTS__
8329
8472
  function readonly(target) {
8330
8473
  return createReactiveObject(
8331
8474
  target,
@@ -8335,6 +8478,7 @@ function readonly(target) {
8335
8478
  readonlyMap
8336
8479
  );
8337
8480
  }
8481
+ // @__NO_SIDE_EFFECTS__
8338
8482
  function shallowReadonly(target) {
8339
8483
  return createReactiveObject(
8340
8484
  target,
@@ -8373,24 +8517,29 @@ function createReactiveObject(target, isReadonly2, baseHandlers, collectionHandl
8373
8517
  proxyMap.set(target, proxy);
8374
8518
  return proxy;
8375
8519
  }
8520
+ // @__NO_SIDE_EFFECTS__
8376
8521
  function isReactive(value) {
8377
- if (isReadonly(value)) {
8378
- return isReactive(value["__v_raw"]);
8522
+ if (/* @__PURE__ */ isReadonly(value)) {
8523
+ return /* @__PURE__ */ isReactive(value["__v_raw"]);
8379
8524
  }
8380
8525
  return !!(value && value["__v_isReactive"]);
8381
8526
  }
8527
+ // @__NO_SIDE_EFFECTS__
8382
8528
  function isReadonly(value) {
8383
8529
  return !!(value && value["__v_isReadonly"]);
8384
8530
  }
8531
+ // @__NO_SIDE_EFFECTS__
8385
8532
  function isShallow(value) {
8386
8533
  return !!(value && value["__v_isShallow"]);
8387
8534
  }
8535
+ // @__NO_SIDE_EFFECTS__
8388
8536
  function isProxy(value) {
8389
8537
  return value ? !!value["__v_raw"] : false;
8390
8538
  }
8539
+ // @__NO_SIDE_EFFECTS__
8391
8540
  function toRaw(observed) {
8392
8541
  const raw = observed && observed["__v_raw"];
8393
- return raw ? toRaw(raw) : observed;
8542
+ return raw ? /* @__PURE__ */ toRaw(raw) : observed;
8394
8543
  }
8395
8544
  function markRaw(value) {
8396
8545
  if (!(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.hasOwn)(value, "__v_skip") && Object.isExtensible(value)) {
@@ -8398,20 +8547,23 @@ function markRaw(value) {
8398
8547
  }
8399
8548
  return value;
8400
8549
  }
8401
- const toReactive = (value) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isObject)(value) ? reactive(value) : value;
8402
- const toReadonly = (value) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isObject)(value) ? readonly(value) : value;
8550
+ const toReactive = (value) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isObject)(value) ? /* @__PURE__ */ reactive(value) : value;
8551
+ const toReadonly = (value) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isObject)(value) ? /* @__PURE__ */ readonly(value) : value;
8403
8552
 
8553
+ // @__NO_SIDE_EFFECTS__
8404
8554
  function isRef(r) {
8405
8555
  return r ? r["__v_isRef"] === true : false;
8406
8556
  }
8557
+ // @__NO_SIDE_EFFECTS__
8407
8558
  function ref(value) {
8408
8559
  return createRef(value, false);
8409
8560
  }
8561
+ // @__NO_SIDE_EFFECTS__
8410
8562
  function shallowRef(value) {
8411
8563
  return createRef(value, true);
8412
8564
  }
8413
8565
  function createRef(rawValue, shallow) {
8414
- if (isRef(rawValue)) {
8566
+ if (/* @__PURE__ */ isRef(rawValue)) {
8415
8567
  return rawValue;
8416
8568
  }
8417
8569
  return new RefImpl(rawValue, shallow);
@@ -8470,7 +8622,7 @@ function triggerRef(ref2) {
8470
8622
  }
8471
8623
  }
8472
8624
  function unref(ref2) {
8473
- return isRef(ref2) ? ref2.value : ref2;
8625
+ return /* @__PURE__ */ isRef(ref2) ? ref2.value : ref2;
8474
8626
  }
8475
8627
  function toValue(source) {
8476
8628
  return (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isFunction)(source) ? source() : unref(source);
@@ -8479,7 +8631,7 @@ const shallowUnwrapHandlers = {
8479
8631
  get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)),
8480
8632
  set: (target, key, value, receiver) => {
8481
8633
  const oldValue = target[key];
8482
- if (isRef(oldValue) && !isRef(value)) {
8634
+ if (/* @__PURE__ */ isRef(oldValue) && !/* @__PURE__ */ isRef(value)) {
8483
8635
  oldValue.value = value;
8484
8636
  return true;
8485
8637
  } else {
@@ -8509,6 +8661,7 @@ class CustomRefImpl {
8509
8661
  function customRef(factory) {
8510
8662
  return new CustomRefImpl(factory);
8511
8663
  }
8664
+ // @__NO_SIDE_EFFECTS__
8512
8665
  function toRefs(object) {
8513
8666
  if ( true && !isProxy(object)) {
8514
8667
  warn(`toRefs() expects a reactive object but received a plain one.`);
@@ -8544,9 +8697,9 @@ class ObjectRefImpl {
8544
8697
  return this._value = val === void 0 ? this._defaultValue : val;
8545
8698
  }
8546
8699
  set value(newVal) {
8547
- if (this._shallow && isRef(this._raw[this._key])) {
8700
+ if (this._shallow && /* @__PURE__ */ isRef(this._raw[this._key])) {
8548
8701
  const nestedRef = this._object[this._key];
8549
- if (isRef(nestedRef)) {
8702
+ if (/* @__PURE__ */ isRef(nestedRef)) {
8550
8703
  nestedRef.value = newVal;
8551
8704
  return;
8552
8705
  }
@@ -8568,15 +8721,16 @@ class GetterRefImpl {
8568
8721
  return this._value = this._getter();
8569
8722
  }
8570
8723
  }
8724
+ // @__NO_SIDE_EFFECTS__
8571
8725
  function toRef(source, key, defaultValue) {
8572
- if (isRef(source)) {
8726
+ if (/* @__PURE__ */ isRef(source)) {
8573
8727
  return source;
8574
8728
  } else if ((0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isFunction)(source)) {
8575
8729
  return new GetterRefImpl(source);
8576
8730
  } else if ((0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isObject)(source) && arguments.length > 1) {
8577
8731
  return propertyToRef(source, key, defaultValue);
8578
8732
  } else {
8579
- return ref(source);
8733
+ return /* @__PURE__ */ ref(source);
8580
8734
  }
8581
8735
  }
8582
8736
  function propertyToRef(source, key, defaultValue) {
@@ -8657,6 +8811,7 @@ class ComputedRefImpl {
8657
8811
  }
8658
8812
  }
8659
8813
  }
8814
+ // @__NO_SIDE_EFFECTS__
8660
8815
  function computed(getterOrOptions, debugOptions, isSSR = false) {
8661
8816
  let getter;
8662
8817
  let setter;
@@ -9072,7 +9227,7 @@ __webpack_require__.r(__webpack_exports__);
9072
9227
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
9073
9228
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
9074
9229
  /**
9075
- * @vue/runtime-core v3.5.26
9230
+ * @vue/runtime-core v3.5.27
9076
9231
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
9077
9232
  * @license MIT
9078
9233
  **/
@@ -11223,7 +11378,7 @@ Server rendered element contains more child nodes than client vdom.`
11223
11378
  logMismatchError();
11224
11379
  }
11225
11380
  if (forcePatch && (key.endsWith("value") || key === "indeterminate") || (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isOn)(key) && !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isReservedProp)(key) || // force hydrate v-bind with .prop modifiers
11226
- key[0] === "." || isCustomElement) {
11381
+ key[0] === "." || isCustomElement && !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isReservedProp)(key)) {
11227
11382
  patchProp(el, key, null, props[key], void 0, parentComponent);
11228
11383
  }
11229
11384
  }
@@ -17571,7 +17726,7 @@ function isMemoSame(cached, memo) {
17571
17726
  return true;
17572
17727
  }
17573
17728
 
17574
- const version = "3.5.26";
17729
+ const version = "3.5.27";
17575
17730
  const warn = true ? warn$1 : 0;
17576
17731
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
17577
17732
  const devtools = true ? devtools$1 : 0;
@@ -17782,7 +17937,7 @@ __webpack_require__.r(__webpack_exports__);
17782
17937
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-core */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
17783
17938
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
17784
17939
  /**
17785
- * @vue/runtime-dom v3.5.26
17940
+ * @vue/runtime-dom v3.5.27
17786
17941
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
17787
17942
  * @license MIT
17788
17943
  **/
@@ -19788,7 +19943,7 @@ __webpack_require__.r(__webpack_exports__);
19788
19943
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
19789
19944
  /* harmony export */ });
19790
19945
  /**
19791
- * @vue/shared v3.5.26
19946
+ * @vue/shared v3.5.27
19792
19947
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
19793
19948
  * @license MIT
19794
19949
  **/
@@ -23512,6 +23667,7 @@ __webpack_require__.r(__webpack_exports__);
23512
23667
  /* harmony export */ BSONValue: () => (/* binding */ BSONValue),
23513
23668
  /* harmony export */ BSONVersionError: () => (/* binding */ BSONVersionError),
23514
23669
  /* harmony export */ Binary: () => (/* binding */ Binary),
23670
+ /* harmony export */ ByteUtils: () => (/* binding */ ByteUtils),
23515
23671
  /* harmony export */ Code: () => (/* binding */ Code),
23516
23672
  /* harmony export */ DBRef: () => (/* binding */ DBRef),
23517
23673
  /* harmony export */ Decimal128: () => (/* binding */ Decimal128),
@@ -23521,6 +23677,7 @@ __webpack_require__.r(__webpack_exports__);
23521
23677
  /* harmony export */ Long: () => (/* binding */ Long),
23522
23678
  /* harmony export */ MaxKey: () => (/* binding */ MaxKey),
23523
23679
  /* harmony export */ MinKey: () => (/* binding */ MinKey),
23680
+ /* harmony export */ NumberUtils: () => (/* binding */ NumberUtils),
23524
23681
  /* harmony export */ ObjectId: () => (/* binding */ ObjectId),
23525
23682
  /* harmony export */ Timestamp: () => (/* binding */ Timestamp),
23526
23683
  /* harmony export */ UUID: () => (/* binding */ UUID),
@@ -23765,6 +23922,7 @@ const nodejsRandomBytes = (() => {
23765
23922
  }
23766
23923
  })();
23767
23924
  const nodeJsByteUtils = {
23925
+ isUint8Array: isUint8Array,
23768
23926
  toLocalBufferType(potentialBuffer) {
23769
23927
  if (Buffer.isBuffer(potentialBuffer)) {
23770
23928
  return potentialBuffer;
@@ -23787,6 +23945,12 @@ const nodeJsByteUtils = {
23787
23945
  allocateUnsafe(size) {
23788
23946
  return Buffer.allocUnsafe(size);
23789
23947
  },
23948
+ compare(a, b) {
23949
+ return nodeJsByteUtils.toLocalBufferType(a).compare(b);
23950
+ },
23951
+ concat(list) {
23952
+ return Buffer.concat(list);
23953
+ },
23790
23954
  equals(a, b) {
23791
23955
  return nodeJsByteUtils.toLocalBufferType(a).equals(b);
23792
23956
  },
@@ -23796,6 +23960,9 @@ const nodeJsByteUtils = {
23796
23960
  fromBase64(base64) {
23797
23961
  return Buffer.from(base64, 'base64');
23798
23962
  },
23963
+ fromUTF8(utf8) {
23964
+ return Buffer.from(utf8, 'utf8');
23965
+ },
23799
23966
  toBase64(buffer) {
23800
23967
  return nodeJsByteUtils.toLocalBufferType(buffer).toString('base64');
23801
23968
  },
@@ -23870,6 +24037,7 @@ const webRandomBytes = (() => {
23870
24037
  })();
23871
24038
  const HEX_DIGIT = /(\d|[a-f])/i;
23872
24039
  const webByteUtils = {
24040
+ isUint8Array: isUint8Array,
23873
24041
  toLocalBufferType(potentialUint8array) {
23874
24042
  const stringTag = potentialUint8array?.[Symbol.toStringTag] ??
23875
24043
  Object.prototype.toString.call(potentialUint8array);
@@ -23896,12 +24064,43 @@ const webByteUtils = {
23896
24064
  allocateUnsafe(size) {
23897
24065
  return webByteUtils.allocate(size);
23898
24066
  },
23899
- equals(a, b) {
23900
- if (a.byteLength !== b.byteLength) {
24067
+ compare(uint8Array, otherUint8Array) {
24068
+ if (uint8Array === otherUint8Array)
24069
+ return 0;
24070
+ const len = Math.min(uint8Array.length, otherUint8Array.length);
24071
+ for (let i = 0; i < len; i++) {
24072
+ if (uint8Array[i] < otherUint8Array[i])
24073
+ return -1;
24074
+ if (uint8Array[i] > otherUint8Array[i])
24075
+ return 1;
24076
+ }
24077
+ if (uint8Array.length < otherUint8Array.length)
24078
+ return -1;
24079
+ if (uint8Array.length > otherUint8Array.length)
24080
+ return 1;
24081
+ return 0;
24082
+ },
24083
+ concat(uint8Arrays) {
24084
+ if (uint8Arrays.length === 0)
24085
+ return webByteUtils.allocate(0);
24086
+ let totalLength = 0;
24087
+ for (const uint8Array of uint8Arrays) {
24088
+ totalLength += uint8Array.length;
24089
+ }
24090
+ const result = webByteUtils.allocate(totalLength);
24091
+ let offset = 0;
24092
+ for (const uint8Array of uint8Arrays) {
24093
+ result.set(uint8Array, offset);
24094
+ offset += uint8Array.length;
24095
+ }
24096
+ return result;
24097
+ },
24098
+ equals(uint8Array, otherUint8Array) {
24099
+ if (uint8Array.byteLength !== otherUint8Array.byteLength) {
23901
24100
  return false;
23902
24101
  }
23903
- for (let i = 0; i < a.byteLength; i++) {
23904
- if (a[i] !== b[i]) {
24102
+ for (let i = 0; i < uint8Array.byteLength; i++) {
24103
+ if (uint8Array[i] !== otherUint8Array[i]) {
23905
24104
  return false;
23906
24105
  }
23907
24106
  }
@@ -23913,6 +24112,9 @@ const webByteUtils = {
23913
24112
  fromBase64(base64) {
23914
24113
  return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
23915
24114
  },
24115
+ fromUTF8(utf8) {
24116
+ return new TextEncoder().encode(utf8);
24117
+ },
23916
24118
  toBase64(uint8array) {
23917
24119
  return btoa(webByteUtils.toISO88591(uint8array));
23918
24120
  },
@@ -28148,6 +28350,7 @@ var bson = /*#__PURE__*/Object.freeze({
28148
28350
  BSONValue: BSONValue,
28149
28351
  BSONVersionError: BSONVersionError,
28150
28352
  Binary: Binary,
28353
+ ByteUtils: ByteUtils,
28151
28354
  Code: Code,
28152
28355
  DBRef: DBRef,
28153
28356
  Decimal128: Decimal128,
@@ -28157,6 +28360,7 @@ var bson = /*#__PURE__*/Object.freeze({
28157
28360
  Long: Long,
28158
28361
  MaxKey: MaxKey,
28159
28362
  MinKey: MinKey,
28363
+ NumberUtils: NumberUtils,
28160
28364
  ObjectId: ObjectId,
28161
28365
  Timestamp: Timestamp,
28162
28366
  UUID: UUID,
@@ -32210,7 +32414,7 @@ __webpack_require__.r(__webpack_exports__);
32210
32414
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js");
32211
32415
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js");
32212
32416
  /**
32213
- * vue v3.5.26
32417
+ * vue v3.5.27
32214
32418
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
32215
32419
  * @license MIT
32216
32420
  **/
@@ -32246,7 +32450,7 @@ const compile = () => {
32246
32450
  (module) {
32247
32451
 
32248
32452
  "use strict";
32249
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.2","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"9.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
32453
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.4","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
32250
32454
 
32251
32455
  /***/ }
32252
32456
 
@@ -1877,6 +1877,10 @@ video {
1877
1877
  padding-bottom: 2rem;
1878
1878
  }
1879
1879
 
1880
+ .pb-16 {
1881
+ padding-bottom: 4rem;
1882
+ }
1883
+
1880
1884
  .pb-2 {
1881
1885
  padding-bottom: 0.5rem;
1882
1886
  }
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ destroyed() {
5
+ this.$parent.$options.$children = this.$parent.$options.$children.filter(
6
+ (el) => el !== this
7
+ );
8
+ },
9
+ created() {
10
+ this.$parent.$options.$children = this.$parent.$options.$children || [];
11
+ this.$parent.$options.$children.push(this);
12
+ },
13
+ mounted() {
14
+ this.isMounted = true;
15
+ },
16
+ unmounted() {
17
+ this.isMounted = false;
18
+ }
19
+ };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Deep equality check for values (handles primitives, arrays, objects, dates, etc.)
5
+ * @param {*} a - First value to compare
6
+ * @param {*} b - Second value to compare
7
+ * @returns {boolean} - True if values are deeply equal
8
+ */
9
+ function deepEqual(a, b) {
10
+ // Handle primitives and same reference
11
+ if (a === b) return true;
12
+
13
+ // Handle null and undefined
14
+ if (a == null || b == null) return a === b;
15
+
16
+ // Handle different types
17
+ if (typeof a !== typeof b) return false;
18
+
19
+ // Handle dates
20
+ if (a instanceof Date && b instanceof Date) {
21
+ return a.getTime() === b.getTime();
22
+ }
23
+
24
+ // Handle arrays - must both be arrays
25
+ if (Array.isArray(a) || Array.isArray(b)) {
26
+ if (!Array.isArray(a) || !Array.isArray(b)) return false;
27
+ if (a.length !== b.length) return false;
28
+ for (let i = 0; i < a.length; i++) {
29
+ if (!deepEqual(a[i], b[i])) return false;
30
+ }
31
+ return true;
32
+ }
33
+
34
+ // Handle objects (non-arrays)
35
+ if (typeof a === 'object' && typeof b === 'object') {
36
+ const keysA = Object.keys(a);
37
+ const keysB = Object.keys(b);
38
+
39
+ if (keysA.length !== keysB.length) return false;
40
+
41
+ for (const key of keysA) {
42
+ if (!keysB.includes(key)) return false;
43
+ if (!deepEqual(a[key], b[key])) return false;
44
+ }
45
+
46
+ return true;
47
+ }
48
+
49
+ // Fallback for primitives (strings, numbers, booleans)
50
+ return false;
51
+ }
52
+
53
+ module.exports = deepEqual;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { inspect } = require('node-inspect-extracted');
4
+ const deepEqual = require('./_util/deepEqual');
4
5
 
5
6
  /**
6
7
  * Format a value for display in array views
@@ -1,10 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
+ const baseComponent = require('../_util/baseComponent');
4
5
  const template = require('./dashboard.html');
5
6
 
6
- module.exports = app => app.component('dashboard', {
7
+ module.exports = {
7
8
  template: template,
9
+ extends: baseComponent,
8
10
  props: ['dashboardId'],
9
11
  data: function() {
10
12
  return {
@@ -48,7 +50,7 @@ module.exports = app => app.component('dashboard', {
48
50
  }
49
51
  },
50
52
  shouldEvaluateDashboard() {
51
- if (this.dashboardResults.length === 0) {
53
+ if (!this.dashboardResults || this.dashboardResults.length === 0) {
52
54
  return true;
53
55
  }
54
56
 
@@ -115,6 +117,22 @@ module.exports = app => app.component('dashboard', {
115
117
  this.startingChat = false;
116
118
  this.showActionsMenu = false;
117
119
  }
120
+ },
121
+ async loadInitial() {
122
+ const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
123
+ if (!dashboard) {
124
+ return;
125
+ }
126
+ this.dashboard = dashboard;
127
+ this.code = this.dashboard.code;
128
+ this.title = this.dashboard.title;
129
+ this.description = this.dashboard.description ?? '';
130
+ this.dashboardResults = dashboardResults;
131
+ if (this.shouldEvaluateDashboard()) {
132
+ await this.evaluateDashboard();
133
+ return;
134
+ }
135
+ this.status = 'loaded';
118
136
  }
119
137
  },
120
138
  computed: {
@@ -125,22 +143,9 @@ module.exports = app => app.component('dashboard', {
125
143
  mounted: async function() {
126
144
  document.addEventListener('click', this.handleDocumentClick);
127
145
  this.showEditor = this.$route.query.edit;
128
- const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
129
- if (!dashboard) {
130
- return;
131
- }
132
- this.dashboard = dashboard;
133
- this.code = this.dashboard.code;
134
- this.title = this.dashboard.title;
135
- this.description = this.dashboard.description ?? '';
136
- this.dashboardResults = dashboardResults;
137
- if (this.shouldEvaluateDashboard()) {
138
- await this.evaluateDashboard();
139
- return;
140
- }
141
- this.status = 'loaded';
146
+ await this.loadInitial();
142
147
  },
143
148
  beforeDestroy() {
144
149
  document.removeEventListener('click', this.handleDocumentClick);
145
150
  }
146
- });
151
+ };
@@ -7,6 +7,16 @@ module.exports = app => app.component('confirm-changes', {
7
7
  props: ['value'],
8
8
  computed: {
9
9
  displayValue() {
10
+ const hasUnsetFields = Object.keys(this.value).some(key => this.value[key] === undefined);
11
+ if (hasUnsetFields) {
12
+ const unsetFields = Object.keys(this.value)
13
+ .filter(key => this.value[key] === undefined)
14
+ .reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
15
+ const setFields = Object.keys(this.value)
16
+ .filter(key => this.value[key] !== undefined)
17
+ .reduce((obj, key) => Object.assign(obj, { [key]: this.value[key] }), {});
18
+ return JSON.stringify({ $set: setFields, $unset: unsetFields }, null, ' ').trim();
19
+ }
10
20
  return JSON.stringify(this.value, null, ' ').trim();
11
21
  }
12
22
  },
@@ -21,4 +31,4 @@ module.exports = app => app.component('confirm-changes', {
21
31
  mounted() {
22
32
  Prism.highlightElement(this.$refs.code);
23
33
  }
24
- });
34
+ });
@@ -1,4 +1,4 @@
1
- <div class="document px-1 pt-4 md:px-0 bg-slate-50 w-full">
1
+ <div class="document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full">
2
2
  <div class="max-w-7xl mx-auto">
3
3
  <div class="flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm">
4
4
  <div class="font-bold overflow-hidden text-ellipsis">{{model}}: {{documentId}}</div>
@@ -74,10 +74,25 @@ module.exports = app => app.component('document', {
74
74
  if (Object.keys(this.invalid).length > 0) {
75
75
  throw new Error('Invalid paths: ' + Object.keys(this.invalid).join(', '));
76
76
  }
77
+
78
+ let update = this.changes;
79
+ let unset = {};
80
+ const hasUnsetFields = Object.keys(this.changes)
81
+ .some(key => this.changes[key] === undefined);
82
+ if (hasUnsetFields) {
83
+ unset = Object.keys(this.changes)
84
+ .filter(key => this.changes[key] === undefined)
85
+ .reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
86
+ update = Object.keys(this.changes)
87
+ .filter(key => this.changes[key] !== undefined)
88
+ .reduce((obj, key) => Object.assign(obj, { [key]: this.changes[key] }), {});
89
+ }
90
+
77
91
  const { doc } = await api.Model.updateDocument({
78
92
  model: this.model,
79
93
  _id: this.document._id,
80
- update: this.changes
94
+ update,
95
+ unset
81
96
  });
82
97
  this.document = doc;
83
98
  this.changes = {};
@@ -74,7 +74,7 @@
74
74
  :value="getEditValueForPath(path)"
75
75
  :format="dateType"
76
76
  v-bind="getEditComponentProps(path)"
77
- @input="changes[path.path] = $event; delete invalid[path.path];"
77
+ @input="handleInputChange($event)"
78
78
  @error="invalid[path.path] = $event;"
79
79
  >
80
80
  </component>
@@ -3,7 +3,7 @@
3
3
  'use strict';
4
4
 
5
5
  const mpath = require('mpath');
6
- const { inspect } = require('node-inspect-extracted');
6
+ const deepEqual = require('../../_util/deepEqual');
7
7
  const template = require('./document-property.html');
8
8
 
9
9
  const appendCSS = require('../../appendCSS');
@@ -95,6 +95,20 @@ module.exports = app => app.component('document-property', {
95
95
  }
96
96
  },
97
97
  methods: {
98
+ handleInputChange(newValue) {
99
+ const currentValue = this.getValueForPath(this.path.path);
100
+
101
+ // Only record as a change if the value is actually different
102
+ if (!deepEqual(currentValue, newValue)) {
103
+ this.changes[this.path.path] = newValue;
104
+ } else {
105
+ // If the value is the same as the original, remove it from changes
106
+ delete this.changes[this.path.path];
107
+ }
108
+
109
+ // Always clear invalid state on input
110
+ delete this.invalid[this.path.path];
111
+ },
98
112
  getComponentForPath(schemaPath) {
99
113
  if (schemaPath.instance === 'Array') {
100
114
  return 'detail-array';
@@ -19,20 +19,12 @@ module.exports = app => app.component('edit-boolean', {
19
19
  this.selectedValue = newValue;
20
20
  },
21
21
  selectedValue(newValue) {
22
- // Convert null/undefined to strings for proper backend serialization
23
- const emitValue = this.convertValueToString(newValue);
24
- this.$emit('input', emitValue);
22
+ this.$emit('input', newValue);
25
23
  }
26
24
  },
27
25
  methods: {
28
26
  selectValue(value) {
29
27
  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
28
  }
37
29
  }
38
30
  });
@@ -44,7 +44,11 @@ requireComponents.keys().forEach((filePath) => {
44
44
  // Check if the file name matches the directory name
45
45
  if (directoryName === fileName) {
46
46
  components[directoryName] = requireComponents(filePath);
47
- components[directoryName](app);
47
+ if (typeof components[directoryName] === 'function') {
48
+ components[directoryName](app);
49
+ } else {
50
+ app.component(directoryName, components[directoryName]);
51
+ }
48
52
  }
49
53
  });
50
54
 
@@ -116,7 +120,7 @@ app.component('app-component', {
116
120
 
117
121
  this.user = user;
118
122
  this.roles = roles;
119
-
123
+
120
124
  setTimeout(() => {
121
125
  this.$router.replace(this.$router.currentRoute.value.path);
122
126
  }, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -34,12 +34,14 @@
34
34
  "eslint": "9.30.0",
35
35
  "express": "4.x",
36
36
  "mocha": "10.2.0",
37
- "mongoose": "9.x"
37
+ "mongoose": "9.x",
38
+ "sinon": "^21.0.1"
38
39
  },
39
40
  "scripts": {
40
41
  "lint": "eslint .",
41
42
  "tailwind": "tailwindcss -o ./frontend/public/tw.css",
42
43
  "tailwind:watch": "tailwindcss -o ./frontend/public/tw.css --watch",
43
- "test": "mocha test/*.test.js"
44
+ "test": "mocha test/*.test.js",
45
+ "test:frontend": "mocha test/frontend/*.test.js"
44
46
  }
45
47
  }