@mongoosejs/studio 0.5.13 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +7 -0
  2. package/CLAUDE.md +7 -0
  3. package/README.md +3 -0
  4. package/backend/actions/ChatMessage/executeScript.js +7 -5
  5. package/backend/actions/ChatThread/createChatMessage.js +4 -2
  6. package/backend/actions/ChatThread/getChatThread.js +5 -2
  7. package/backend/actions/ChatThread/listChatThreads.js +3 -1
  8. package/backend/actions/ChatThread/shareChatThread.js +5 -2
  9. package/backend/actions/ChatThread/streamChatMessage.js +4 -2
  10. package/backend/actions/ChatThread/toggleAgentMode.js +5 -2
  11. package/backend/actions/Dashboard/deleteDashboard.js +7 -3
  12. package/backend/actions/Dashboard/getDashboard.js +21 -10
  13. package/backend/actions/Dashboard/getDashboards.js +3 -1
  14. package/backend/actions/Dashboard/updateDashboard.js +3 -2
  15. package/backend/actions/Model/addField.js +3 -2
  16. package/backend/actions/Model/analyzeSchema.js +6 -4
  17. package/backend/actions/Model/deleteDocument.js +3 -3
  18. package/backend/actions/Model/deleteDocuments.js +3 -1
  19. package/backend/actions/Model/executeDocumentScript.js +25 -29
  20. package/backend/actions/Model/exportQueryResults.js +3 -1
  21. package/backend/actions/Model/getCollectionInfo.js +5 -3
  22. package/backend/actions/Model/getDocument.js +3 -2
  23. package/backend/actions/Model/getDocuments.js +15 -5
  24. package/backend/actions/Model/getDocumentsStream.js +28 -18
  25. package/backend/actions/Model/getEstimatedDocumentCounts.js +5 -2
  26. package/backend/actions/Model/updateDocument.js +4 -2
  27. package/backend/actions/Model/updateDocuments.js +4 -2
  28. package/backend/actions/Model/validateDocument.js +5 -2
  29. package/backend/actions/Task/cancelTask.js +6 -3
  30. package/backend/actions/Task/getTaskOverview.js +3 -2
  31. package/backend/actions/Task/getTasks.js +3 -2
  32. package/backend/actions/Task/getTasksOverTime.js +3 -2
  33. package/backend/actions/Task/rescheduleTask.js +6 -3
  34. package/backend/actions/Task/runTask.js +6 -3
  35. package/backend/helpers/omitNullish.js +5 -0
  36. package/backend/index.js +4 -4
  37. package/backend/sandbox/createSandbox.js +2 -2
  38. package/backend/sandbox/createScriptDb.js +42 -19
  39. package/frontend/public/app.js +319 -96
  40. package/frontend/public/tw.css +68 -0
  41. package/frontend/src/api.js +5 -1
  42. package/frontend/src/models/models.html +29 -8
  43. package/frontend/src/models/models.js +30 -2
  44. package/frontend/src/tooltip/tooltip.html +28 -0
  45. package/frontend/src/tooltip/tooltip.js +94 -0
  46. package/index.d.ts +1 -1
  47. package/local.js +2 -1
  48. package/nest.d.ts +1 -0
  49. package/package.json +1 -1
  50. package/vercel.d.ts +2 -0
@@ -1269,7 +1269,11 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
1269
1269
  getDocumentsStream: async function* getDocumentsStream(params) {
1270
1270
  const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
1271
1271
  yield { schemaPaths: data.schemaPaths, suggestedFields: data.suggestedFields };
1272
- yield { numDocs: data.numDocs };
1272
+ if (data.numDocsError) {
1273
+ yield { numDocsError: data.numDocsError };
1274
+ } else {
1275
+ yield { numDocs: data.numDocs };
1276
+ }
1273
1277
  for (const doc of data.docs) {
1274
1278
  yield { document: doc };
1275
1279
  }
@@ -9621,6 +9625,8 @@ module.exports = app => app.component('models', {
9621
9625
  filteredPaths: [],
9622
9626
  selectedPaths: [],
9623
9627
  numDocuments: null,
9628
+ numDocumentsError: null,
9629
+ documentsError: null,
9624
9630
  mongoDBIndexes: [],
9625
9631
  schemaIndexes: [],
9626
9632
  status: 'loading',
@@ -10308,7 +10314,8 @@ module.exports = app => app.component('models', {
10308
10314
  return filteredDoc;
10309
10315
  },
10310
10316
  async checkIfScrolledToBottom() {
10311
- if (this.status === 'loading' || this.loadedAllDocs) {
10317
+ // Stop infinite scroll if the last load failed, otherwise we would retry the same failing request forever.
10318
+ if (this.status === 'loading' || this.loadedAllDocs || this.documentsError) {
10312
10319
  return;
10313
10320
  }
10314
10321
  // Infinite scroll only applies to table/json views.
@@ -10334,11 +10341,20 @@ module.exports = app => app.component('models', {
10334
10341
  try {
10335
10342
  const skip = this.documents.length;
10336
10343
  const params = this.buildDocumentFetchParams({ skip });
10337
- const { docs } = await api.Model.getDocuments(params);
10344
+ const { docs, numDocs, numDocsError } = await api.Model.getDocuments(params);
10345
+ if (numDocsError) {
10346
+ this.numDocumentsError = numDocsError;
10347
+ } else if (numDocs !== undefined) {
10348
+ this.numDocuments = numDocs;
10349
+ this.numDocumentsError = null;
10350
+ }
10338
10351
  if (docs.length < limit) {
10339
10352
  this.loadedAllDocs = true;
10340
10353
  }
10341
10354
  this.documents.push(...docs);
10355
+ this.documentsError = null;
10356
+ } catch (err) {
10357
+ this.documentsError = err?.message;
10342
10358
  } finally {
10343
10359
  this.loadingMore = false;
10344
10360
  this.status = 'loaded';
@@ -10551,6 +10567,8 @@ module.exports = app => app.component('models', {
10551
10567
  // Clear previous data
10552
10568
  this.documents = [];
10553
10569
  this.numDocuments = null;
10570
+ this.numDocumentsError = null;
10571
+ this.documentsError = null;
10554
10572
  this.loadedAllDocs = false;
10555
10573
  this.lastSelectedIndex = null;
10556
10574
 
@@ -10600,6 +10618,10 @@ module.exports = app => app.component('models', {
10600
10618
  }
10601
10619
  if (event.numDocs !== undefined) {
10602
10620
  this.numDocuments = event.numDocs;
10621
+ this.numDocumentsError = null;
10622
+ }
10623
+ if (event.numDocsError) {
10624
+ this.numDocumentsError = event.numDocsError;
10603
10625
  }
10604
10626
  if (event.document) {
10605
10627
  this.documents.push(event.document);
@@ -10613,6 +10635,9 @@ module.exports = app => app.component('models', {
10613
10635
  if (docsCount < limit) {
10614
10636
  this.loadedAllDocs = true;
10615
10637
  }
10638
+ this.documentsError = null;
10639
+ } catch (err) {
10640
+ this.documentsError = err?.message;
10616
10641
  } finally {
10617
10642
  this.status = 'loaded';
10618
10643
  }
@@ -10639,8 +10664,12 @@ module.exports = app => app.component('models', {
10639
10664
  for await (const event of api.Model.getDocumentsStream(params)) {
10640
10665
  if (event.numDocs !== undefined && !numDocsReceived) {
10641
10666
  this.numDocuments = event.numDocs;
10667
+ this.numDocumentsError = null;
10642
10668
  numDocsReceived = true;
10643
10669
  }
10670
+ if (event.numDocsError) {
10671
+ this.numDocumentsError = event.numDocsError;
10672
+ }
10644
10673
  if (event.document) {
10645
10674
  this.documents.push(event.document);
10646
10675
  docsCount++;
@@ -10653,6 +10682,9 @@ module.exports = app => app.component('models', {
10653
10682
  if (docsCount < limit) {
10654
10683
  this.loadedAllDocs = true;
10655
10684
  }
10685
+ this.documentsError = null;
10686
+ } catch (err) {
10687
+ this.documentsError = err?.message;
10656
10688
  } finally {
10657
10689
  this.loadingMore = false;
10658
10690
  this.status = 'loaded';
@@ -13175,6 +13207,111 @@ module.exports = app => app.component('team', {
13175
13207
  });
13176
13208
 
13177
13209
 
13210
+ /***/ },
13211
+
13212
+ /***/ "./frontend/src/tooltip/tooltip.js"
13213
+ /*!*****************************************!*\
13214
+ !*** ./frontend/src/tooltip/tooltip.js ***!
13215
+ \*****************************************/
13216
+ (module, __unused_webpack_exports, __webpack_require__) {
13217
+
13218
+ "use strict";
13219
+
13220
+
13221
+ const template = __webpack_require__(/*! ./tooltip.html */ "./frontend/src/tooltip/tooltip.html");
13222
+
13223
+ let nextTooltipId = 0;
13224
+
13225
+ module.exports = app => app.component('tooltip', {
13226
+ props: {
13227
+ text: {
13228
+ type: String,
13229
+ required: true
13230
+ }
13231
+ },
13232
+ template,
13233
+ data() {
13234
+ return {
13235
+ isOpen: false,
13236
+ isPinned: false,
13237
+ placement: 'bottom',
13238
+ tooltipId: `tooltip-${++nextTooltipId}`,
13239
+ tooltipStyle: {
13240
+ visibility: 'hidden'
13241
+ }
13242
+ };
13243
+ },
13244
+ mounted() {
13245
+ document.addEventListener('click', this.handleOutsideClick);
13246
+ window.addEventListener('resize', this.updatePosition);
13247
+ window.addEventListener('scroll', this.updatePosition, true);
13248
+ },
13249
+ beforeUnmount() {
13250
+ document.removeEventListener('click', this.handleOutsideClick);
13251
+ window.removeEventListener('resize', this.updatePosition);
13252
+ window.removeEventListener('scroll', this.updatePosition, true);
13253
+ },
13254
+ methods: {
13255
+ show() {
13256
+ this.isOpen = true;
13257
+ this.tooltipStyle = { visibility: 'hidden' };
13258
+ this.$nextTick(this.updatePosition);
13259
+ },
13260
+ hide() {
13261
+ this.isOpen = false;
13262
+ this.isPinned = false;
13263
+ },
13264
+ hideUnlessPinned() {
13265
+ if (!this.isPinned) {
13266
+ this.isOpen = false;
13267
+ }
13268
+ },
13269
+ toggle() {
13270
+ if (this.isPinned) {
13271
+ this.hide();
13272
+ return;
13273
+ }
13274
+
13275
+ this.isPinned = true;
13276
+ this.show();
13277
+ },
13278
+ handleFocusOut(event) {
13279
+ if (!this.$el.contains(event.relatedTarget)) {
13280
+ this.hideUnlessPinned();
13281
+ }
13282
+ },
13283
+ handleOutsideClick(event) {
13284
+ if (!this.$el.contains(event.target)) {
13285
+ this.hide();
13286
+ }
13287
+ },
13288
+ updatePosition() {
13289
+ if (!this.isOpen || !this.$refs.trigger || !this.$refs.tooltip) {
13290
+ return;
13291
+ }
13292
+
13293
+ const gap = 8;
13294
+ const viewportPadding = 8;
13295
+ const triggerRect = this.$refs.trigger.getBoundingClientRect();
13296
+ const tooltipRect = this.$refs.tooltip.getBoundingClientRect();
13297
+ const fitsBelow = triggerRect.bottom + gap + tooltipRect.height <= window.innerHeight - viewportPadding;
13298
+ const top = fitsBelow ?
13299
+ triggerRect.bottom + gap :
13300
+ Math.max(viewportPadding, triggerRect.top - gap - tooltipRect.height);
13301
+ const centeredLeft = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
13302
+ const maxLeft = Math.max(viewportPadding, window.innerWidth - tooltipRect.width - viewportPadding);
13303
+ const left = Math.min(Math.max(viewportPadding, centeredLeft), maxLeft);
13304
+
13305
+ this.placement = fitsBelow ? 'bottom' : 'top';
13306
+ this.tooltipStyle = {
13307
+ left: `${Math.round(left)}px`,
13308
+ top: `${Math.round(top)}px`
13309
+ };
13310
+ }
13311
+ }
13312
+ });
13313
+
13314
+
13178
13315
  /***/ },
13179
13316
 
13180
13317
  /***/ "./frontend/src/update-document/update-document.js"
@@ -13491,6 +13628,9 @@ const map = {
13491
13628
  "./team/team": "./frontend/src/team/team.js",
13492
13629
  "./team/team.html": "./frontend/src/team/team.html",
13493
13630
  "./team/team.js": "./frontend/src/team/team.js",
13631
+ "./tooltip/tooltip": "./frontend/src/tooltip/tooltip.js",
13632
+ "./tooltip/tooltip.html": "./frontend/src/tooltip/tooltip.html",
13633
+ "./tooltip/tooltip.js": "./frontend/src/tooltip/tooltip.js",
13494
13634
  "./update-document/update-document": "./frontend/src/update-document/update-document.js",
13495
13635
  "./update-document/update-document.css": "./frontend/src/update-document/update-document.css",
13496
13636
  "./update-document/update-document.html": "./frontend/src/update-document/update-document.html",
@@ -13581,7 +13721,7 @@ __webpack_require__.r(__webpack_exports__);
13581
13721
  /* harmony export */ });
13582
13722
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
13583
13723
  /**
13584
- * @vue/reactivity v3.5.41
13724
+ * @vue/reactivity v3.5.42
13585
13725
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
13586
13726
  * @license MIT
13587
13727
  **/
@@ -15754,7 +15894,7 @@ __webpack_require__.r(__webpack_exports__);
15754
15894
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
15755
15895
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
15756
15896
  /**
15757
- * @vue/runtime-core v3.5.41
15897
+ * @vue/runtime-core v3.5.42
15758
15898
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
15759
15899
  * @license MIT
15760
15900
  **/
@@ -17822,10 +17962,10 @@ function createHydrationFunctions(rendererInternals) {
17822
17962
  getContainerType(container),
17823
17963
  optimized
17824
17964
  );
17825
- if (isAsyncWrapper(vnode) && !vnode.type.__asyncResolved) {
17965
+ if (isAsyncWrapper(vnode) && !vnode.component.subTree) {
17826
17966
  let subTree;
17827
17967
  if (isFragmentStart) {
17828
- subTree = createVNode(Fragment);
17968
+ subTree = createVNode(Static);
17829
17969
  subTree.anchor = nextNode ? nextNode.previousSibling : container.lastChild;
17830
17970
  } else {
17831
17971
  subTree = node.nodeType === 3 ? createTextVNode("") : createVNode("div");
@@ -18736,7 +18876,10 @@ const KeepAliveImpl = {
18736
18876
  if (pendingCacheKey != null) {
18737
18877
  if (isSuspense(instance.subTree.type)) {
18738
18878
  queuePostRenderEffect(() => {
18739
- cache.set(pendingCacheKey, getInnerChild(instance.subTree));
18879
+ const vnode = getInnerChild(instance.subTree);
18880
+ if (vnode.component) {
18881
+ cache.set(pendingCacheKey, vnode);
18882
+ }
18740
18883
  }, instance.subTree.suspense);
18741
18884
  } else {
18742
18885
  cache.set(pendingCacheKey, getInnerChild(instance.subTree));
@@ -19137,12 +19280,41 @@ const getPublicInstance = (i) => {
19137
19280
  if (isStatefulComponent(i)) return getComponentPublicInstance(i);
19138
19281
  return getPublicInstance(i.parent);
19139
19282
  };
19283
+ const resolveDevRootEl = (vnode) => {
19284
+ let found = false;
19285
+ while (true) {
19286
+ if (vnode.patchFlag > 0 && vnode.patchFlag & 2048) {
19287
+ const root = filterSingleRoot(vnode.children);
19288
+ if (!root) {
19289
+ return;
19290
+ }
19291
+ vnode = root;
19292
+ found = true;
19293
+ continue;
19294
+ }
19295
+ const component = vnode.component;
19296
+ if (component && component.subTree) {
19297
+ vnode = component.subTree;
19298
+ continue;
19299
+ }
19300
+ const suspense = vnode.suspense;
19301
+ if (suspense && suspense.activeBranch) {
19302
+ vnode = suspense.activeBranch;
19303
+ continue;
19304
+ }
19305
+ return found ? vnode.el : void 0;
19306
+ }
19307
+ };
19308
+ const getDevRootFragmentEl = (i) => {
19309
+ const el = i.subTree && resolveDevRootEl(i.subTree);
19310
+ return el === void 0 ? i.vnode.el : el;
19311
+ };
19140
19312
  const publicPropertiesMap = (
19141
19313
  // Move PURE marker to new line to workaround compiler discarding it
19142
19314
  // due to type annotation
19143
19315
  /* @__PURE__ */ (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.extend)(/* @__PURE__ */ Object.create(null), {
19144
19316
  $: (i) => i,
19145
- $el: (i) => i.vnode.el,
19317
+ $el: (i) => true ? getDevRootFragmentEl(i) : 0,
19146
19318
  $data: (i) => i.data,
19147
19319
  $props: (i) => true ? (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.shallowReadonly)(i.props) : 0,
19148
19320
  $attrs: (i) => true ? (0,_vue_reactivity__WEBPACK_IMPORTED_MODULE_0__.shallowReadonly)(i.attrs) : 0,
@@ -20248,7 +20420,7 @@ function emit(instance, event, ...rawArgs) {
20248
20420
  args = rawArgs.map((a) => (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isString)(a) ? a.trim() : a);
20249
20421
  }
20250
20422
  if (modifiers.number) {
20251
- args = rawArgs.map(_vue_shared__WEBPACK_IMPORTED_MODULE_1__.looseToNumber);
20423
+ args = args.map(_vue_shared__WEBPACK_IMPORTED_MODULE_1__.looseToNumber);
20252
20424
  }
20253
20425
  }
20254
20426
  if (true) {
@@ -22917,7 +23089,7 @@ function patchSuspense(n1, n2, container, anchor, parentComponent, namespace, sl
22917
23089
  if (suspense.deps <= 0) {
22918
23090
  suspense.resolve();
22919
23091
  } else if (isInFallback) {
22920
- if (!isHydrating) {
23092
+ if (!isHydrating && !suspense.isFallbackMountPending) {
22921
23093
  patch(
22922
23094
  activeBranch,
22923
23095
  newFallback,
@@ -22958,7 +23130,7 @@ function patchSuspense(n1, n2, container, anchor, parentComponent, namespace, sl
22958
23130
  );
22959
23131
  if (suspense.deps <= 0) {
22960
23132
  suspense.resolve();
22961
- } else {
23133
+ } else if (!suspense.isFallbackMountPending) {
22962
23134
  patch(
22963
23135
  activeBranch,
22964
23136
  newFallback,
@@ -23199,9 +23371,10 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
23199
23371
  if (!suspense.isInFallback) {
23200
23372
  return;
23201
23373
  }
23374
+ const latestFallback = suspense.vnode.ssFallback;
23202
23375
  patch(
23203
23376
  null,
23204
- fallbackVNode,
23377
+ latestFallback,
23205
23378
  container2,
23206
23379
  anchor2,
23207
23380
  parentComponent2,
@@ -23211,7 +23384,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
23211
23384
  slotScopeIds,
23212
23385
  optimized
23213
23386
  );
23214
- setActiveBranch(suspense, fallbackVNode);
23387
+ setActiveBranch(suspense, latestFallback);
23215
23388
  };
23216
23389
  const delayEnter = fallbackVNode.transition && fallbackVNode.transition.mode === "out-in";
23217
23390
  if (delayEnter) {
@@ -24479,7 +24652,7 @@ function isMemoSame(cached, memo) {
24479
24652
  return true;
24480
24653
  }
24481
24654
 
24482
- const version = "3.5.41";
24655
+ const version = "3.5.42";
24483
24656
  const warn = true ? warn$1 : 0;
24484
24657
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
24485
24658
  const devtools = true ? devtools$1 : 0;
@@ -24690,7 +24863,7 @@ __webpack_require__.r(__webpack_exports__);
24690
24863
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-core */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
24691
24864
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
24692
24865
  /**
24693
- * @vue/runtime-dom v3.5.41
24866
+ * @vue/runtime-dom v3.5.42
24694
24867
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
24695
24868
  * @license MIT
24696
24869
  **/
@@ -25273,7 +25446,11 @@ function setStyle(style, name, val) {
25273
25446
  }
25274
25447
  }
25275
25448
  if (name.startsWith("--")) {
25276
- style.setProperty(name, val);
25449
+ if (importantRE.test(val)) {
25450
+ style.setProperty(name, val.replace(importantRE, ""), "important");
25451
+ } else {
25452
+ style.setProperty(name, val);
25453
+ }
25277
25454
  } else {
25278
25455
  const prefixed = autoPrefix(style, name);
25279
25456
  if (importantRE.test(val)) {
@@ -26407,13 +26584,21 @@ const vModelSelect = {
26407
26584
  const selectedVal = Array.prototype.filter.call(el.options, (o) => o.selected).map(
26408
26585
  (o) => number ? (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.looseToNumber)(getValue(o)) : getValue(o)
26409
26586
  );
26410
- el[assignKey](
26411
- el.multiple ? (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isSet)(el._modelValue) ? new Set(selectedVal) : selectedVal : selectedVal[0]
26412
- );
26413
- el._assigning = true;
26414
- (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_0__.nextTick)(() => {
26415
- el._assigning = false;
26416
- });
26587
+ const multiple = el.multiple;
26588
+ const assignedValue = multiple ? (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isSet)(el._modelValue) ? new Set(selectedVal) : selectedVal : selectedVal[0];
26589
+ const pending = el._pendingValue = [
26590
+ multiple,
26591
+ multiple ? (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isArray)(assignedValue) ? selectedVal.slice() : selectedVal : assignedValue
26592
+ ];
26593
+ try {
26594
+ el[assignKey](assignedValue);
26595
+ } finally {
26596
+ ;(0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_0__.nextTick)(() => {
26597
+ if (el._pendingValue === pending) {
26598
+ el._pendingValue = void 0;
26599
+ }
26600
+ });
26601
+ }
26417
26602
  });
26418
26603
  el[assignKey] = getModelAssigner(vnode);
26419
26604
  },
@@ -26427,11 +26612,25 @@ const vModelSelect = {
26427
26612
  el[assignKey] = getModelAssigner(vnode);
26428
26613
  },
26429
26614
  updated(el, { value }) {
26430
- if (!el._assigning) {
26615
+ const pending = el._pendingValue;
26616
+ el._pendingValue = void 0;
26617
+ if (!pending || pending[0] !== el.multiple || !isSameSelectValue(value, pending[1], pending[0])) {
26431
26618
  setSelected(el, value);
26432
26619
  }
26433
26620
  }
26434
26621
  };
26622
+ function isSameSelectValue(value, assignedValue, multiple) {
26623
+ if (!multiple) return (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.looseEqual)(value, assignedValue);
26624
+ if ((0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isArray)(value)) return (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.looseEqual)(value, assignedValue);
26625
+ if ((0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isSet)(value)) {
26626
+ if (value.size !== assignedValue.length) return false;
26627
+ for (const item of assignedValue) {
26628
+ if (!value.has(item)) return false;
26629
+ }
26630
+ return true;
26631
+ }
26632
+ return false;
26633
+ }
26435
26634
  function setSelected(el, value) {
26436
26635
  const isMultiple = el.multiple;
26437
26636
  const isArrayValue = (0,_vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__.isArray)(value);
@@ -26812,7 +27011,7 @@ __webpack_require__.r(__webpack_exports__);
26812
27011
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
26813
27012
  /* harmony export */ });
26814
27013
  /**
26815
- * @vue/shared v3.5.41
27014
+ * @vue/shared v3.5.42
26816
27015
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
26817
27016
  * @license MIT
26818
27017
  **/
@@ -27153,7 +27352,7 @@ const isBooleanAttr = /* @__PURE__ */ makeMap(
27153
27352
  function includeBooleanAttr(value) {
27154
27353
  return !!value || value === "";
27155
27354
  }
27156
- const unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u0020]/;
27355
+ const unsafeAttrCharRE = /[>/="'\u0009\u000a\u000c\u000d\u0020]/;
27157
27356
  const attrValidationCache = {};
27158
27357
  function isSSRSafeAttrName(name) {
27159
27358
  if (attrValidationCache.hasOwnProperty(name)) {
@@ -27252,6 +27451,23 @@ function looseCompareArrays(a, b) {
27252
27451
  }
27253
27452
  return equal;
27254
27453
  }
27454
+ function looseCompareCollections(a, b) {
27455
+ if (a.size !== b.size) return false;
27456
+ const candidates = Array.from(b);
27457
+ const matched = new Uint8Array(candidates.length);
27458
+ for (const item of a) {
27459
+ let index = -1;
27460
+ for (let i = 0; i < candidates.length; i++) {
27461
+ if (!matched[i] && looseEqual(item, candidates[i])) {
27462
+ index = i;
27463
+ break;
27464
+ }
27465
+ }
27466
+ if (index < 0) return false;
27467
+ matched[index] = 1;
27468
+ }
27469
+ return true;
27470
+ }
27255
27471
  function looseEqual(a, b) {
27256
27472
  if (a === b) return true;
27257
27473
  let aValidType = isDate(a);
@@ -27275,6 +27491,16 @@ function looseEqual(a, b) {
27275
27491
  if (!aValidType || !bValidType) {
27276
27492
  return false;
27277
27493
  }
27494
+ aValidType = isMap(a);
27495
+ bValidType = isMap(b);
27496
+ if (aValidType || bValidType) {
27497
+ return aValidType && bValidType ? looseCompareCollections(a, b) : false;
27498
+ }
27499
+ aValidType = isSet(a);
27500
+ bValidType = isSet(b);
27501
+ if (aValidType || bValidType) {
27502
+ return aValidType && bValidType ? looseCompareCollections(a, b) : false;
27503
+ }
27278
27504
  const aKeysCount = Object.keys(a).length;
27279
27505
  const bKeysCount = Object.keys(b).length;
27280
27506
  if (aKeysCount !== bKeysCount) {
@@ -52532,7 +52758,7 @@ __webpack_require__.r(__webpack_exports__);
52532
52758
  /* 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");
52533
52759
  /* 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");
52534
52760
  /**
52535
- * vue v3.5.41
52761
+ * vue v3.5.42
52536
52762
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
52537
52763
  * @license MIT
52538
52764
  **/
@@ -54390,7 +54616,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
54390
54616
  (module) {
54391
54617
 
54392
54618
  "use strict";
54393
- module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <aside class=\"bg-page border-r overflow-hidden transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative shrink-0 flex flex-col top-[55px] bottom-0 lg:top-auto lg:bottom-auto lg:h-full\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''\">\n <!-- Search -->\n <div class=\"p-3 shrink-0\">\n <div class=\"relative\">\n <svg class=\"absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n <input\n v-model=\"modelSearch\"\n type=\"text\"\n placeholder=\"Find model...\"\n @keydown.esc=\"modelSearch = ''\"\n class=\"w-full rounded-md border border-edge bg-surface py-1.5 pl-8 pr-3 text-sm text-content placeholder:text-gray-400 focus:border-edge-strong focus:outline-none focus:ring-1 focus:ring-gray-300\"\n />\n </div>\n </div>\n <!-- Model list (scrollable) -->\n <nav class=\"flex-1 overflow-y-auto px-2 pb-2\">\n <!-- Recently Viewed -->\n <div v-if=\"filteredRecentModels.length > 0 && !modelSearch.trim()\">\n <div class=\"px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider\">Recently Viewed</div>\n <ul role=\"list\">\n <li v-for=\"model in filteredRecentModels\" :key=\"'recent-' + model\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary\"\n :class=\"model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'\">\n <span class=\"truncate\" v-html=\"highlightMatch(model)\"></span>\n <span\n v-if=\"modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel\"\n class=\"ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]\"\n >\n {{formatCompactCount(modelDocumentCounts[model])}}\n </span>\n </router-link>\n </li>\n </ul>\n <div class=\"border-b border-edge my-2\"></div>\n </div>\n <!-- All Models / Search Results -->\n <div class=\"px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider\">{{ modelSearch.trim() ? 'Search Results' : 'All Models' }}</div>\n <ul role=\"list\">\n <li v-for=\"model in filteredModels\" :key=\"'all-' + model\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary\"\n :class=\"model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'\">\n <span class=\"truncate\" v-html=\"highlightMatch(model)\"></span>\n <span\n v-if=\"modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel\"\n class=\"ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]\"\n >\n {{formatCompactCount(modelDocumentCounts[model])}}\n </span>\n </router-link>\n </li>\n </ul>\n <div v-if=\"filteredModels.length === 0 && modelSearch.trim()\" class=\"px-2 py-2 text-sm text-content-tertiary\">\n No models match \"{{modelSearch}}\"\n </div>\n <div v-if=\"models.length === 0 && status === 'loaded'\" class=\"p-2 bg-red-100 rounded-md\">\n No models found\n </div>\n </nav>\n <!-- Bottom toolbar -->\n <div class=\"shrink-0 border-t border-edge bg-page px-2 py-1.5 flex items-center gap-1\">\n <button\n type=\"button\"\n @click=\"hideSidebar = true\"\n class=\"rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n title=\"Hide sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5\" />\n </svg>\n </button>\n <button\n type=\"button\"\n @click=\"openModelSwitcher\"\n class=\"rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n title=\"Quick switch (Ctrl+P)\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n </button>\n </div>\n </aside>\n <div class=\"documents bg-slate-50 min-w-0\" ref=\"documentsList\">\n <div class=\"documents-menu bg-slate-50\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <button\n v-if=\"hideSidebar === true || hideSidebar === null\"\n type=\"button\"\n @click=\"hideSidebar = false\"\n class=\"shrink-0 rounded-md p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n :class=\"hideSidebar === null ? 'lg:hidden' : ''\"\n title=\"Show sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5\" />\n </svg>\n </button>\n <document-search\n ref=\"documentSearch\"\n :value=\"searchText\"\n :schema-paths=\"schemaPaths\"\n @search=\"search\"\n >\n </document-search>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{documents.length}}/{{numDocuments === 1 ? numDocuments + ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{\n 'bg-gray-700 ring-inset ring-2 ring-gray-300 text-white hover:bg-gray-600': selectMultiple,\n 'bg-primary hover:bg-primary-hover text-primary-text': !selectMultiple\n }\"\n class=\"rounded px-2 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {{ selectMultiple ? 'Cancel' : 'Select' }}\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 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 >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <div class=\"relative\" v-show=\"!selectMultiple\" ref=\"actionsMenuContainer\" @keyup.esc.prevent=\"closeActionsMenu\">\n <button\n @click=\"toggleActionsMenu\"\n type=\"button\"\n aria-label=\"More actions\"\n class=\"rounded bg-surface px-2 py-2 text-sm font-semibold text-content-secondary shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-page focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z\" />\n </svg>\n </button>\n <div\n v-if=\"showActionsMenu\"\n class=\"absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50\"\n >\n <div class=\"py-1\">\n <button\n @click=\"shouldShowExportModal = true; showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Export\n </button>\n <button\n @click=\"shouldShowCreateModal = true; showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Create\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Indexes\n </button>\n <button\n @click=\"openAnalyzeSchemaModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Analyze Schema\n </button>\n <button\n @click=\"openCollectionInfo\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Collection Info\n </button>\n <button\n @click=\"toggleRowNumbers()\"\n :disabled=\"outputType !== 'table'\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm\"\n :class=\"outputType === 'table' ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'\"\n >\n {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}\n </button>\n <button\n @click=\"resetFilter()\"\n :disabled=\"searchText.trim().length === 0\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm\"\n :class=\"searchText.trim().length > 0 ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'\"\n >\n Reset Filter\n </button>\n <button\n @click=\"toggleProjectionMenu(); showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm hover:bg-muted\"\n :class=\"isProjectionMenuSelected ? 'text-primary font-semibold' : 'text-content-secondary'\"\n >\n {{ isProjectionMenuSelected ? 'Projection (On)' : 'Projection' }}\n </button>\n <button\n v-if=\"isProjectionMenuSelected\"\n @click=\"clearProjection()\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Reset Projection\n </button>\n <async-button\n v-if=\"isProjectionMenuSelected\"\n type=\"button\"\n @click=\"applyDefaultProjectionColumns()\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Default Projection\n </async-button>\n <button\n @click=\"openDropCollectionModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50\"\n >\n Drop Collection\n </button>\n </div>\n </div>\n </div>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"setOutputType('table')\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"setOutputType('json')\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n <button\n @click=\"setOutputType('map')\"\n :disabled=\"geoJsonFields.length === 0\"\n type=\"button\"\n :title=\"geoJsonFields.length > 0 ? 'Map view' : 'No GeoJSON fields detected'\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-10\"\n :class=\"[\n geoJsonFields.length === 0 ? 'text-gray-300 cursor-not-allowed bg-muted' : 'text-gray-400 hover:bg-page',\n outputType === 'map' ? 'bg-gray-200' : (geoJsonFields.length > 0 ? 'bg-surface' : '')\n ]\">\n <svg class=\"h-5 w-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z\" />\n </svg>\n </button>\n </span>\n </div>\n <div v-if=\"isProjectionMenuSelected && (outputType === 'table' || outputType === 'json')\" class=\"flex items-center gap-2 w-full mt-2 flex-shrink-0\">\n <projection-search\n ref=\"projectionSearch\"\n :value=\"projectionText\"\n :schema-paths=\"schemaPaths\"\n @projection-input=\"projectionText = $event\"\n @apply=\"applyProjectionFromInput\"\n />\n </div>\n </div>\n <!-- In JSON view, this container is the scrollable element used for infinite scroll. -->\n <div class=\"documents-container relative\" ref=\"documentsContainerScroll\" @scroll=\"checkIfScrolledToBottom\">\n <div v-if=\"error\">\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md\" role=\"alert\">\n <span class=\"block font-bold\">Error</span>\n <span class=\"block\">{{ error }}</span>\n <span class=\"block mt-2\">\n Need help?\n <a\n href=\"https://discord.gg/P3YCfKYxpy\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"underline font-medium text-red-800 hover:text-red-900\"\n >Ask in Discord</a>\n or\n <a\n href=\"https://github.com/mongoosejs/studio/issues\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"underline font-medium text-red-800 hover:text-red-900\"\n >\n open a GitHub issue.\n </a>\n </span>\n </div>\n </div>\n <div v-else-if=\"outputType === 'table'\" class=\"flex-1 min-h-0 flex flex-col overflow-hidden\">\n <div\n ref=\"documentsScrollContainer\"\n class=\"overflow-x-auto overflow-y-auto flex-1 min-h-0 border border-edge rounded-lg bg-surface\"\n @scroll=\"checkIfScrolledToBottom\"\n >\n <table class=\"min-w-full border-collapse text-sm\">\n <thead class=\"sticky top-0 z-10 bg-slate-100 dark:bg-shark-800 border-b border-edge\">\n <tr>\n <th v-if=\"showRowNumbers\" class=\"px-3 py-2.5 text-left font-medium text-content border-r border-edge whitespace-nowrap align-middle w-0\">\n #\n </th>\n <th\n v-for=\"path in tableDisplayPaths\"\n :key=\"path.path\"\n class=\"px-3 py-2.5 text-left font-medium text-content border-r border-edge last:border-r-0 whitespace-nowrap align-middle\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n @click=\"addPathFilter(path.path)\"\n class=\"cursor-pointer hover:text-primary truncate min-w-0\"\n :title=\"path.path\"\n >\n {{ path.path }}\n </span>\n <span class=\"text-xs text-content-tertiary shrink-0\">({{ path.instance || 'unknown' }})</span>\n <span class=\"inline-flex shrink-0 gap-0.5 items-center\">\n <button\n type=\"button\"\n @click.stop=\"sortDocs(1, path.path)\"\n class=\"p-0.5 rounded text-content-tertiary hover:text-content hover:bg-muted\"\n :class=\"{ 'text-primary font-semibold': sortBy[path.path] === 1 }\"\n :title=\"sortBy[path.path] === 1 ? 'Clear sort' : 'Sort ascending'\"\n >\n ↑\n </button>\n <button\n type=\"button\"\n @click.stop=\"sortDocs(-1, path.path)\"\n class=\"p-0.5 rounded text-content-tertiary hover:text-content hover:bg-muted\"\n :class=\"{ 'text-primary font-semibold': sortBy[path.path] === -1 }\"\n :title=\"sortBy[path.path] === -1 ? 'Clear sort' : 'Sort descending'\"\n >\n ↓\n </button>\n <button\n v-if=\"filteredPaths.length > 0\"\n type=\"button\"\n @click.stop=\"removeField(path)\"\n class=\"p-1.5 rounded-md border border-transparent text-content-tertiary hover:text-valencia-600 hover:bg-valencia-50 hover:border-valencia-200 focus:outline-none focus:ring-2 focus:ring-valencia-500/30\"\n title=\"Remove column\"\n aria-label=\"Remove column\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n </span>\n </div>\n </th>\n <th v-if=\"filteredPaths.length > 0\" class=\"px-2 py-2.5 border-r border-edge last:border-r-0 align-middle w-0 bg-slate-50 dark:bg-shark-800/80\">\n <div class=\"relative\" ref=\"addFieldContainer\">\n <button\n type=\"button\"\n @click=\"toggleAddFieldDropdown()\"\n class=\"flex items-center justify-center w-8 h-8 rounded border border-dashed border-edge text-content-tertiary hover:border-primary hover:text-primary hover:bg-primary-subtle/30\"\n title=\"Add column\"\n aria-label=\"Add column\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"> <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4.5v15m7.5-7.5h-15\" /> </svg>\n </button>\n <div\n v-if=\"showAddFieldDropdown\"\n class=\"absolute right-0 top-full mt-1 z-[100] min-w-[180px] max-w-[280px] rounded-md border border-edge bg-surface shadow-lg py-1 max-h-48 overflow-y-auto\"\n >\n <input\n v-if=\"availablePathsToAdd.length > 5\"\n ref=\"addFieldFilterInput\"\n v-model=\"addFieldFilterText\"\n type=\"text\"\n placeholder=\"Filter fields...\"\n class=\"mx-2 mb-1 w-[calc(100%-1rem)] rounded border border-edge px-2 py-1 text-sm\"\n @click.stop\n />\n <button\n v-for=\"p in filteredPathsToAdd\"\n :key=\"p.path\"\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left text-sm hover:bg-muted\"\n @click.stop=\"addField(p)\"\n >\n {{ p.path }}\n </button>\n <p v-if=\"filteredPathsToAdd.length === 0\" class=\"px-3 py-2 text-sm text-content-tertiary\">\n {{ addFieldFilterText.trim() ? 'No matching fields' : 'All fields added' }}\n </p>\n </div>\n </div>\n </th>\n </tr>\n </thead>\n <tbody class=\"bg-surface\">\n <tr\n v-for=\"(document, docIndex) in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentClick(document, $event)\"\n class=\"border-b border-edge cursor-pointer transition-colors\"\n :class=\"selectedDocuments.some(x => x._id.toString() === document._id.toString())\n ? 'bg-blue-200'\n : 'hover:bg-muted'\"\n >\n <td v-if=\"showRowNumbers\" class=\"px-3 py-2 border-r border-edge align-top text-content-tertiary whitespace-nowrap\">\n {{ docIndex + 1 }}\n </td>\n <td\n v-for=\"schemaPath in tableDisplayPaths\"\n :key=\"schemaPath.path\"\n class=\"px-3 py-2 border-r border-edge last:border-r-0 align-top max-w-[280px]\"\n >\n <div class=\"table-cell-content flex items-center gap-1.5 min-w-0 group\">\n <span class=\"min-w-0 overflow-hidden text-ellipsis flex-1\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\"\n />\n </span>\n <button\n type=\"button\"\n class=\"table-cell-copy shrink-0 p-1 rounded text-content-tertiary hover:text-content hover:bg-muted focus:outline-none focus:ring-1 focus:ring-edge\"\n aria-label=\"Copy cell value\"\n @click.stop=\"copyCellValue(getValueForPath(document, schemaPath.path))\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n </div>\n </td>\n <td v-if=\"filteredPaths.length > 0\" class=\"w-0 px-0 py-0 border-r border-edge last:border-r-0 bg-slate-50/50 dark:bg-shark-800/30\"></td>\n </tr>\n </tbody>\n </table>\n </div>\n <div v-if=\"outputType === 'table' && (loadingMore || (status === 'loading' && documents.length > 0))\" class=\"flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary border-t border-edge bg-surface\">\n <img src=\"images/loader.gif\" alt=\"\" class=\"h-5 w-5\">\n <span>Loading documents…</span>\n </div>\n <p v-if=\"outputType === 'table' && documents.length === 0 && status === 'loaded'\" class=\"mt-2 text-sm text-content-tertiary px-1\">\n No documents to show. Use Projection in the menu to choose columns.\n </p>\n </div>\n <div v-else-if=\"outputType === 'json'\" class=\"flex min-w-0 flex-col space-y-2 p-1 mt-1\">\n <div\n v-for=\"document in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentContainerClick(document, $event)\"\n :class=\"[\n 'group relative min-w-0 max-w-full transition-colors rounded-md border border-slate-100',\n selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'\n ]\"\n >\n <router-link\n class=\"absolute top-2 right-2 z-10 inline-flex items-center rounded bg-primary px-2 py-1 text-xs font-semibold text-primary-text shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n :to=\"{ path: '/model/' + currentModel + '/document/' + document._id, query: $route.query }\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.stop\n >\n Open\n </router-link>\n <list-json :value=\"filterDocument(document)\" :references=\"referenceMap\">\n </list-json>\n </div>\n <div v-if=\"outputType === 'json' && (loadingMore || (status === 'loading' && documents.length > 0))\" class=\"flex items-center justify-center gap-2 py-3 text-sm text-content-tertiary\">\n <img src=\"images/loader.gif\" alt=\"\" class=\"h-5 w-5\">\n <span>Loading documents…</span>\n </div>\n </div>\n <div v-else-if=\"outputType === 'map'\" class=\"flex flex-col h-full\">\n <div class=\"p-2 bg-surface border-b flex items-center gap-2\">\n <label class=\"text-sm font-medium text-content-secondary\">GeoJSON Field:</label>\n <select\n :value=\"selectedGeoField\"\n @change=\"setSelectedGeoField($event.target.value)\"\n class=\"rounded-md border border-edge-strong py-1 px-2 text-sm focus:border-primary focus:ring-primary\"\n >\n <option v-for=\"field in geoJsonFields\" :key=\"field.path\" :value=\"field.path\">\n {{ field.label }}\n </option>\n </select>\n <async-button\n @click=\"loadMoreDocuments\"\n :disabled=\"loadedAllDocs\"\n type=\"button\"\n class=\"rounded px-2 py-1 text-xs font-semibold text-primary-text shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n :class=\"loadedAllDocs ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary hover:bg-primary-hover'\"\n >\n Load more\n </async-button>\n </div>\n <div class=\"flex-1 min-h-[400px]\" ref=\"modelsMap\"></div>\n </div>\n <div v-if=\"status === 'loading' && !loadingMore && documents.length === 0\" class=\"loader loader-overlay\" aria-busy=\"true\">\n <img src=\"images/loader.gif\" alt=\"Loading\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold flex items-center gap-2\">\n <div>{{ index.name }}</div>\n <div v-if=\"isTTLIndex(index)\" class=\"rounded-full bg-primary-subtle px-2 py-0.5 text-xs font-semibold text-primary\">\n TTL: {{ formatTTL(index.expireAfterSeconds) }}\n </div>\n </div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowAnalyzeSchemaModal\" containerClass=\"md:!w-[70vw]\">\n <template v-slot:body>\n <analyze-schema-modal\n :current-model=\"currentModel\"\n @close=\"shouldShowAnalyzeSchemaModal = false\"\n ></analyze-schema-modal>\n </template>\n </modal>\n <modal v-if=\"shouldShowCollectionInfoModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCollectionInfoModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Collection Info</div>\n <div v-if=\"!collectionInfo\" class=\"text-gray-600\">Loading collection details...</div>\n <div v-else class=\"space-y-3\">\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Documents</div>\n <div class=\"text-content\">{{ formatNumber(collectionInfo.documentCount) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Indexes</div>\n <div class=\"text-content\">{{ formatNumber(collectionInfo.indexCount) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Total Index Size</div>\n <div class=\"text-content\">{{ formatCollectionSize(collectionInfo.totalIndexSize) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Total Storage Size</div>\n <div class=\"text-content\">{{ formatCollectionSize(collectionInfo.size) }}</div>\n </div>\n <div class=\"flex flex-col gap-1\">\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Collation</div>\n <div class=\"text-content\">{{ collectionInfo.hasCollation ? 'Yes' : 'No' }}</div>\n </div>\n <div v-if=\"collectionInfo.hasCollation\" class=\"rounded bg-muted p-3 text-sm text-gray-800 overflow-x-auto\">\n <pre class=\"whitespace-pre-wrap\">{{ JSON.stringify(collectionInfo.collation, null, 2) }}</pre>\n </div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Capped</div>\n <div class=\"text-content\">{{ collectionInfo.capped ? 'Yes' : 'No' }}</div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowDropCollectionModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDropCollectionModal = false\">&times;</div>\n <h2 class=\"text-xl font-bold mb-3\">Are you sure?</h2>\n <p class=\"text-content-secondary mb-3\">\n This will permanently drop the <span class=\"font-semibold\">{{ currentModel }}</span> collection and all its documents.\n </p>\n <div v-if=\"requiresDropCollectionNameConfirmation()\" class=\"rounded border border-red-300 bg-red-50 px-3 py-2 mb-4\">\n <p class=\"text-sm font-semibold text-red-700\">Danger zone</p>\n <p class=\"text-sm text-red-700 mb-2\">Type <span class=\"font-mono font-semibold\">{{ currentModel }}</span> to confirm.</p>\n <input\n v-model=\"dropCollectionConfirmName\"\n type=\"text\"\n :placeholder=\"currentModel\"\n class=\"w-full rounded border border-red-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-red-200\"\n />\n </div>\n <div class=\"flex gap-4\">\n <async-button\n @click=\"dropCollection\"\n :disabled=\"requiresDropCollectionNameConfirmation() && dropCollectionConfirmName !== currentModel\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Drop Collection\n </async-button>\n <button @click=\"shouldShowDropCollectionModal = false\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n <model-switcher\n :show=\"showModelSwitcher\"\n :models=\"models\"\n :recently-viewed-models=\"recentlyViewedModels\"\n :model-document-counts=\"modelDocumentCounts\"\n @close=\"showModelSwitcher = false\"\n @select=\"selectSwitcherModel\"\n ></model-switcher>\n</div>\n";
54619
+ module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <aside class=\"bg-page border-r overflow-hidden transition-all duration-300 ease-in-out z-20 w-0 lg:w-64 fixed lg:relative shrink-0 flex flex-col top-[55px] bottom-0 lg:top-auto lg:bottom-auto lg:h-full\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-64' : ''\">\n <!-- Search -->\n <div class=\"p-3 shrink-0\">\n <div class=\"relative\">\n <svg class=\"absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n <input\n v-model=\"modelSearch\"\n type=\"text\"\n placeholder=\"Find model...\"\n @keydown.esc=\"modelSearch = ''\"\n class=\"w-full rounded-md border border-edge bg-surface py-1.5 pl-8 pr-3 text-sm text-content placeholder:text-gray-400 focus:border-edge-strong focus:outline-none focus:ring-1 focus:ring-gray-300\"\n />\n </div>\n </div>\n <!-- Model list (scrollable) -->\n <nav class=\"flex-1 overflow-y-auto px-2 pb-2\">\n <!-- Recently Viewed -->\n <div v-if=\"filteredRecentModels.length > 0 && !modelSearch.trim()\">\n <div class=\"px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider\">Recently Viewed</div>\n <ul role=\"list\">\n <li v-for=\"model in filteredRecentModels\" :key=\"'recent-' + model\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary\"\n :class=\"model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'\">\n <span class=\"truncate\" v-html=\"highlightMatch(model)\"></span>\n <span\n v-if=\"modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel\"\n class=\"ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]\"\n >\n {{formatCompactCount(modelDocumentCounts[model])}}\n </span>\n </router-link>\n </li>\n </ul>\n <div class=\"border-b border-edge my-2\"></div>\n </div>\n <!-- All Models / Search Results -->\n <div class=\"px-2 py-1 text-xs font-semibold text-gray-400 uppercase tracking-wider\">{{ modelSearch.trim() ? 'Search Results' : 'All Models' }}</div>\n <ul role=\"list\">\n <li v-for=\"model in filteredModels\" :key=\"'all-' + model\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"flex items-center rounded-md py-1.5 px-2 text-sm text-content-secondary\"\n :class=\"model === currentModel ? 'bg-gray-200 font-semibold text-content' : 'hover:bg-muted'\">\n <span class=\"truncate\" v-html=\"highlightMatch(model)\"></span>\n <span\n v-if=\"modelDocumentCounts && modelDocumentCounts[model] !== undefined && model !== currentModel\"\n class=\"ml-auto text-xs text-gray-400 bg-muted rounded px-1.5 py-[1px]\"\n >\n {{formatCompactCount(modelDocumentCounts[model])}}\n </span>\n </router-link>\n </li>\n </ul>\n <div v-if=\"filteredModels.length === 0 && modelSearch.trim()\" class=\"px-2 py-2 text-sm text-content-tertiary\">\n No models match \"{{modelSearch}}\"\n </div>\n <div v-if=\"models.length === 0 && status === 'loaded'\" class=\"p-2 bg-red-100 rounded-md\">\n No models found\n </div>\n </nav>\n <!-- Bottom toolbar -->\n <div class=\"shrink-0 border-t border-edge bg-page px-2 py-1.5 flex items-center gap-1\">\n <button\n type=\"button\"\n @click=\"hideSidebar = true\"\n class=\"rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n title=\"Hide sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5\" />\n </svg>\n </button>\n <button\n type=\"button\"\n @click=\"openModelSwitcher\"\n class=\"rounded p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n title=\"Quick switch (Ctrl+P)\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z\" />\n </svg>\n </button>\n </div>\n </aside>\n <div class=\"documents bg-slate-50 min-w-0\" ref=\"documentsList\">\n <div class=\"documents-menu bg-slate-50\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <button\n v-if=\"hideSidebar === true || hideSidebar === null\"\n type=\"button\"\n @click=\"hideSidebar = false\"\n class=\"shrink-0 rounded-md p-1.5 text-gray-400 hover:text-gray-600 hover:bg-muted\"\n :class=\"hideSidebar === null ? 'lg:hidden' : ''\"\n title=\"Show sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5\" />\n </svg>\n </button>\n <document-search\n ref=\"documentSearch\"\n :value=\"searchText\"\n :schema-paths=\"schemaPaths\"\n @search=\"search\"\n >\n </document-search>\n <div>\n <tooltip\n v-if=\"numDocumentsError\"\n :text=\"'Error counting documents: ' + numDocumentsError\"\n >\n <span\n class=\"inline-flex rounded-md bg-red-50 px-2 py-2 text-sm text-red-700 ring-1 ring-inset ring-red-200\"\n role=\"alert\"\n >\n Count Error\n </span>\n </tooltip>\n <span v-else-if=\"numDocuments == null\">Loading...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{documents.length}}/{{numDocuments === 1 ? numDocuments + ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{\n 'bg-gray-700 ring-inset ring-2 ring-gray-300 text-white hover:bg-gray-600': selectMultiple,\n 'bg-primary hover:bg-primary-hover text-primary-text': !selectMultiple\n }\"\n class=\"rounded px-2 py-2 text-sm font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n >\n {{ selectMultiple ? 'Cancel' : 'Select' }}\n </button>\n <button\n v-show=\"selectMultiple\"\n @click=\"shouldShowUpdateMultipleModal=true;\"\n type=\"button\"\n class=\"rounded bg-green-600 px-2 py-2 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 >\n Update\n </button>\n <button\n @click=\"shouldShowDeleteMultipleModal=true;\"\n type=\"button\"\n v-show=\"selectMultiple\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500\"\n >\n Delete\n </button>\n <div class=\"relative\" v-show=\"!selectMultiple\" ref=\"actionsMenuContainer\" @keyup.esc.prevent=\"closeActionsMenu\">\n <button\n @click=\"toggleActionsMenu\"\n type=\"button\"\n aria-label=\"More actions\"\n class=\"rounded bg-surface px-2 py-2 text-sm font-semibold text-content-secondary shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-page focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Zm0 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z\" />\n </svg>\n </button>\n <div\n v-if=\"showActionsMenu\"\n class=\"absolute right-0 mt-2 w-48 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none z-50\"\n >\n <div class=\"py-1\">\n <button\n @click=\"shouldShowExportModal = true; showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Export\n </button>\n <button\n @click=\"shouldShowCreateModal = true; showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Create\n </button>\n <button\n @click=\"openIndexModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Indexes\n </button>\n <button\n @click=\"openAnalyzeSchemaModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Analyze Schema\n </button>\n <button\n @click=\"openCollectionInfo\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Collection Info\n </button>\n <button\n @click=\"toggleRowNumbers()\"\n :disabled=\"outputType !== 'table'\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm\"\n :class=\"outputType === 'table' ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'\"\n >\n {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}\n </button>\n <button\n @click=\"resetFilter()\"\n :disabled=\"searchText.trim().length === 0\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm\"\n :class=\"searchText.trim().length > 0 ? 'text-content-secondary hover:bg-muted' : 'text-content-tertiary cursor-not-allowed opacity-50'\"\n >\n Reset Filter\n </button>\n <button\n @click=\"toggleProjectionMenu(); showActionsMenu = false\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm hover:bg-muted\"\n :class=\"isProjectionMenuSelected ? 'text-primary font-semibold' : 'text-content-secondary'\"\n >\n {{ isProjectionMenuSelected ? 'Projection (On)' : 'Projection' }}\n </button>\n <button\n v-if=\"isProjectionMenuSelected\"\n @click=\"clearProjection()\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Reset Projection\n </button>\n <async-button\n v-if=\"isProjectionMenuSelected\"\n type=\"button\"\n @click=\"applyDefaultProjectionColumns()\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Default Projection\n </async-button>\n <button\n @click=\"openDropCollectionModal\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-red-700 hover:bg-red-50\"\n >\n Drop Collection\n </button>\n </div>\n </div>\n </div>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"setOutputType('table')\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"setOutputType('json')\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n <button\n @click=\"setOutputType('map')\"\n :disabled=\"geoJsonFields.length === 0\"\n type=\"button\"\n :title=\"geoJsonFields.length > 0 ? 'Map view' : 'No GeoJSON fields detected'\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-10\"\n :class=\"[\n geoJsonFields.length === 0 ? 'text-gray-300 cursor-not-allowed bg-muted' : 'text-gray-400 hover:bg-page',\n outputType === 'map' ? 'bg-gray-200' : (geoJsonFields.length > 0 ? 'bg-surface' : '')\n ]\">\n <svg class=\"h-5 w-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z\" />\n </svg>\n </button>\n </span>\n </div>\n <div v-if=\"isProjectionMenuSelected && (outputType === 'table' || outputType === 'json')\" class=\"flex items-center gap-2 w-full mt-2 flex-shrink-0\">\n <projection-search\n ref=\"projectionSearch\"\n :value=\"projectionText\"\n :schema-paths=\"schemaPaths\"\n @projection-input=\"projectionText = $event\"\n @apply=\"applyProjectionFromInput\"\n />\n </div>\n </div>\n <!-- In JSON view, this container is the scrollable element used for infinite scroll. -->\n <div class=\"documents-container relative\" ref=\"documentsContainerScroll\" @scroll=\"checkIfScrolledToBottom\">\n <div v-if=\"error\">\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md\" role=\"alert\">\n <span class=\"block font-bold\">Error</span>\n <span class=\"block\">{{ error }}</span>\n <span class=\"block mt-2\">\n Need help?\n <a\n href=\"https://discord.gg/P3YCfKYxpy\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"underline font-medium text-red-800 hover:text-red-900\"\n >Ask in Discord</a>\n or\n <a\n href=\"https://github.com/mongoosejs/studio/issues\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n class=\"underline font-medium text-red-800 hover:text-red-900\"\n >\n open a GitHub issue.\n </a>\n </span>\n </div>\n </div>\n <div v-else-if=\"outputType === 'table'\" class=\"flex-1 min-h-0 flex flex-col overflow-hidden\">\n <div\n ref=\"documentsScrollContainer\"\n class=\"overflow-x-auto overflow-y-auto flex-1 min-h-0 border border-edge rounded-lg bg-surface\"\n @scroll=\"checkIfScrolledToBottom\"\n >\n <table class=\"min-w-full border-collapse text-sm\">\n <thead class=\"sticky top-0 z-10 bg-slate-100 dark:bg-shark-800 border-b border-edge\">\n <tr>\n <th v-if=\"showRowNumbers\" class=\"px-3 py-2.5 text-left font-medium text-content border-r border-edge whitespace-nowrap align-middle w-0\">\n #\n </th>\n <th\n v-for=\"path in tableDisplayPaths\"\n :key=\"path.path\"\n class=\"px-3 py-2.5 text-left font-medium text-content border-r border-edge last:border-r-0 whitespace-nowrap align-middle\"\n >\n <div class=\"flex items-center gap-2\">\n <span\n @click=\"addPathFilter(path.path)\"\n class=\"cursor-pointer hover:text-primary truncate min-w-0\"\n :title=\"path.path\"\n >\n {{ path.path }}\n </span>\n <span class=\"text-xs text-content-tertiary shrink-0\">({{ path.instance || 'unknown' }})</span>\n <span class=\"inline-flex shrink-0 gap-0.5 items-center\">\n <button\n type=\"button\"\n @click.stop=\"sortDocs(1, path.path)\"\n class=\"p-0.5 rounded text-content-tertiary hover:text-content hover:bg-muted\"\n :class=\"{ 'text-primary font-semibold': sortBy[path.path] === 1 }\"\n :title=\"sortBy[path.path] === 1 ? 'Clear sort' : 'Sort ascending'\"\n >\n ↑\n </button>\n <button\n type=\"button\"\n @click.stop=\"sortDocs(-1, path.path)\"\n class=\"p-0.5 rounded text-content-tertiary hover:text-content hover:bg-muted\"\n :class=\"{ 'text-primary font-semibold': sortBy[path.path] === -1 }\"\n :title=\"sortBy[path.path] === -1 ? 'Clear sort' : 'Sort descending'\"\n >\n ↓\n </button>\n <button\n v-if=\"filteredPaths.length > 0\"\n type=\"button\"\n @click.stop=\"removeField(path)\"\n class=\"p-1.5 rounded-md border border-transparent text-content-tertiary hover:text-valencia-600 hover:bg-valencia-50 hover:border-valencia-200 focus:outline-none focus:ring-2 focus:ring-valencia-500/30\"\n title=\"Remove column\"\n aria-label=\"Remove column\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-4 h-4\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M6 18 18 6M6 6l12 12\" />\n </svg>\n </button>\n </span>\n </div>\n </th>\n <th v-if=\"filteredPaths.length > 0\" class=\"px-2 py-2.5 border-r border-edge last:border-r-0 align-middle w-0 bg-slate-50 dark:bg-shark-800/80\">\n <div class=\"relative\" ref=\"addFieldContainer\">\n <button\n type=\"button\"\n @click=\"toggleAddFieldDropdown()\"\n class=\"flex items-center justify-center w-8 h-8 rounded border border-dashed border-edge text-content-tertiary hover:border-primary hover:text-primary hover:bg-primary-subtle/30\"\n title=\"Add column\"\n aria-label=\"Add column\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"> <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M12 4.5v15m7.5-7.5h-15\" /> </svg>\n </button>\n <div\n v-if=\"showAddFieldDropdown\"\n class=\"absolute right-0 top-full mt-1 z-[100] min-w-[180px] max-w-[280px] rounded-md border border-edge bg-surface shadow-lg py-1 max-h-48 overflow-y-auto\"\n >\n <input\n v-if=\"availablePathsToAdd.length > 5\"\n ref=\"addFieldFilterInput\"\n v-model=\"addFieldFilterText\"\n type=\"text\"\n placeholder=\"Filter fields...\"\n class=\"mx-2 mb-1 w-[calc(100%-1rem)] rounded border border-edge px-2 py-1 text-sm\"\n @click.stop\n />\n <button\n v-for=\"p in filteredPathsToAdd\"\n :key=\"p.path\"\n type=\"button\"\n class=\"w-full px-3 py-1.5 text-left text-sm hover:bg-muted\"\n @click.stop=\"addField(p)\"\n >\n {{ p.path }}\n </button>\n <p v-if=\"filteredPathsToAdd.length === 0\" class=\"px-3 py-2 text-sm text-content-tertiary\">\n {{ addFieldFilterText.trim() ? 'No matching fields' : 'All fields added' }}\n </p>\n </div>\n </div>\n </th>\n </tr>\n </thead>\n <tbody class=\"bg-surface\">\n <tr\n v-for=\"(document, docIndex) in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentClick(document, $event)\"\n class=\"border-b border-edge cursor-pointer transition-colors\"\n :class=\"selectedDocuments.some(x => x._id.toString() === document._id.toString())\n ? 'bg-blue-200'\n : 'hover:bg-muted'\"\n >\n <td v-if=\"showRowNumbers\" class=\"px-3 py-2 border-r border-edge align-top text-content-tertiary whitespace-nowrap\">\n {{ docIndex + 1 }}\n </td>\n <td\n v-for=\"schemaPath in tableDisplayPaths\"\n :key=\"schemaPath.path\"\n class=\"px-3 py-2 border-r border-edge last:border-r-0 align-top max-w-[280px]\"\n >\n <div class=\"table-cell-content flex items-center gap-1.5 min-w-0 group\">\n <span class=\"min-w-0 overflow-hidden text-ellipsis flex-1\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\"\n />\n </span>\n <button\n type=\"button\"\n class=\"table-cell-copy shrink-0 p-1 rounded text-content-tertiary hover:text-content hover:bg-muted focus:outline-none focus:ring-1 focus:ring-edge\"\n aria-label=\"Copy cell value\"\n @click.stop=\"copyCellValue(getValueForPath(document, schemaPath.path))\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\" class=\"w-5 h-5\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M8 5H6a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1M8 5a2 2 0 0 0 2 2h2a2 2 0 0 0 2-2M8 5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2m0 0h2a2 2 0 0 1 2 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n </div>\n </td>\n <td v-if=\"filteredPaths.length > 0\" class=\"w-0 px-0 py-0 border-r border-edge last:border-r-0 bg-slate-50/50 dark:bg-shark-800/30\"></td>\n </tr>\n </tbody>\n </table>\n </div>\n <div\n v-if=\"outputType === 'table' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))\"\n class=\"flex items-center justify-center gap-2 py-3 text-sm border-t\"\n :class=\"documentsError ? 'border-red-200 bg-red-50 text-red-700' : 'border-edge bg-surface text-content-tertiary'\"\n :role=\"documentsError ? 'alert' : null\"\n >\n <img v-if=\"!documentsError\" src=\"images/loader.gif\" alt=\"\" class=\"h-5 w-5\">\n <span>{{documentsError || 'Loading documents…'}}</span>\n </div>\n <p v-if=\"outputType === 'table' && documents.length === 0 && status === 'loaded' && !documentsError\" class=\"mt-2 text-sm text-content-tertiary px-1\">\n No documents to show. Use Projection in the menu to choose columns.\n </p>\n </div>\n <div v-else-if=\"outputType === 'json'\" class=\"flex min-w-0 flex-col space-y-2 p-1 mt-1\">\n <div\n v-for=\"document in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentContainerClick(document, $event)\"\n :class=\"[\n 'group relative min-w-0 max-w-full transition-colors rounded-md border border-slate-100',\n selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'\n ]\"\n >\n <router-link\n class=\"absolute top-2 right-2 z-10 inline-flex items-center rounded bg-primary px-2 py-1 text-xs font-semibold text-primary-text shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n :to=\"{ path: '/model/' + currentModel + '/document/' + document._id, query: $route.query }\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click.stop\n >\n Open\n </router-link>\n <list-json :value=\"filterDocument(document)\" :references=\"referenceMap\">\n </list-json>\n </div>\n <div\n v-if=\"outputType === 'json' && (documentsError || loadingMore || (status === 'loading' && documents.length > 0))\"\n class=\"flex items-center justify-center gap-2 py-3 text-sm\"\n :class=\"documentsError ? 'rounded-md border border-red-200 bg-red-50 text-red-700' : 'text-content-tertiary'\"\n :role=\"documentsError ? 'alert' : null\"\n >\n <img v-if=\"!documentsError\" src=\"images/loader.gif\" alt=\"\" class=\"h-5 w-5\">\n <span>{{documentsError || 'Loading documents…'}}</span>\n </div>\n </div>\n <div v-else-if=\"outputType === 'map'\" class=\"flex flex-col h-full\">\n <div class=\"p-2 bg-surface border-b flex items-center gap-2\">\n <label class=\"text-sm font-medium text-content-secondary\">GeoJSON Field:</label>\n <select\n :value=\"selectedGeoField\"\n @change=\"setSelectedGeoField($event.target.value)\"\n class=\"rounded-md border border-edge-strong py-1 px-2 text-sm focus:border-primary focus:ring-primary\"\n >\n <option v-for=\"field in geoJsonFields\" :key=\"field.path\" :value=\"field.path\">\n {{ field.label }}\n </option>\n </select>\n <async-button\n @click=\"loadMoreDocuments\"\n :disabled=\"loadedAllDocs\"\n type=\"button\"\n class=\"rounded px-2 py-1 text-xs font-semibold text-primary-text shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n :class=\"loadedAllDocs ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary hover:bg-primary-hover'\"\n >\n Load more\n </async-button>\n </div>\n <div class=\"flex-1 min-h-[400px]\" ref=\"modelsMap\"></div>\n </div>\n <div v-if=\"status === 'loading' && !loadingMore && documents.length === 0\" class=\"loader loader-overlay\" aria-busy=\"true\">\n <img src=\"images/loader.gif\" alt=\"Loading\">\n </div>\n </div>\n </div>\n <modal v-if=\"shouldShowExportModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowExportModal = false\">&times;</div>\n <export-query-results\n :schemaPaths=\"schemaPaths\"\n :search-text=\"searchText\"\n :currentModel=\"currentModel\"\n @done=\"shouldShowExportModal = false\">\n </export-query-results>\n </template>\n </modal>\n <modal v-if=\"shouldShowIndexModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowIndexModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Indexes</div>\n <div v-for=\"index in mongoDBIndexes\" class=\"w-full flex items-center\">\n <div class=\"grow shrink text-left flex justify-between items-center\" v-if=\"index.name != '_id_'\">\n <div>\n <div class=\"font-bold flex items-center gap-2\">\n <div>{{ index.name }}</div>\n <div v-if=\"isTTLIndex(index)\" class=\"rounded-full bg-primary-subtle px-2 py-0.5 text-xs font-semibold text-primary\">\n TTL: {{ formatTTL(index.expireAfterSeconds) }}\n </div>\n </div>\n <div class=\"text-sm font-mono\">{{ JSON.stringify(index.key) }}</div>\n </div>\n <div>\n <async-button\n type=\"button\"\n @click=\"dropIndex(index.name)\"\n class=\"rounded-md bg-valencia-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-valencia-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed\">\n Drop\n </async-button>\n </div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowAnalyzeSchemaModal\" containerClass=\"md:!w-[70vw]\">\n <template v-slot:body>\n <analyze-schema-modal\n :current-model=\"currentModel\"\n @close=\"shouldShowAnalyzeSchemaModal = false\"\n ></analyze-schema-modal>\n </template>\n </modal>\n <modal v-if=\"shouldShowCollectionInfoModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCollectionInfoModal = false\">&times;</div>\n <div class=\"text-xl font-bold mb-2\">Collection Info</div>\n <div v-if=\"!collectionInfo\" class=\"text-gray-600\">Loading collection details...</div>\n <div v-else class=\"space-y-3\">\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Documents</div>\n <div class=\"text-content\">{{ formatNumber(collectionInfo.documentCount) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Indexes</div>\n <div class=\"text-content\">{{ formatNumber(collectionInfo.indexCount) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Total Index Size</div>\n <div class=\"text-content\">{{ formatCollectionSize(collectionInfo.totalIndexSize) }}</div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Total Storage Size</div>\n <div class=\"text-content\">{{ formatCollectionSize(collectionInfo.size) }}</div>\n </div>\n <div class=\"flex flex-col gap-1\">\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Collation</div>\n <div class=\"text-content\">{{ collectionInfo.hasCollation ? 'Yes' : 'No' }}</div>\n </div>\n <div v-if=\"collectionInfo.hasCollation\" class=\"rounded bg-muted p-3 text-sm text-gray-800 overflow-x-auto\">\n <pre class=\"whitespace-pre-wrap\">{{ JSON.stringify(collectionInfo.collation, null, 2) }}</pre>\n </div>\n </div>\n <div class=\"flex justify-between gap-4\">\n <div class=\"font-semibold text-content-secondary\">Capped</div>\n <div class=\"text-content\">{{ collectionInfo.capped ? 'Yes' : 'No' }}</div>\n </div>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowCreateModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowCreateModal = false;\">&times;</div>\n <create-document :currentModel=\"currentModel\" :paths=\"schemaPaths\" @close=\"closeCreationModal\"></create-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowUpdateMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowUpdateMultipleModal = false;\">&times;</div>\n <update-document :currentModel=\"currentModel\" :document=\"selectedDocuments\" :multiple=\"true\" @update=\"updateDocuments\" @close=\"shouldShowUpdateMultipleModal=false;\"></update-document>\n </template>\n </modal>\n <modal v-if=\"shouldShowDeleteMultipleModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDeleteMultipleModal = false;\">&times;</div>\n <h2>Are you sure you want to delete {{selectedDocuments.length}} documents?</h2>\n <div>\n <list-json :value=\"selectedDocuments\"></list-json>\n </div>\n <div class=\"flex gap-4\">\n <async-button @click=\"deleteDocuments\" class=\"rounded bg-red-500 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600\">\n Confirm\n </async-button>\n <button @click=\"shouldShowDeleteMultipleModal = false;\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n <modal v-if=\"shouldShowDropCollectionModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowDropCollectionModal = false\">&times;</div>\n <h2 class=\"text-xl font-bold mb-3\">Are you sure?</h2>\n <p class=\"text-content-secondary mb-3\">\n This will permanently drop the <span class=\"font-semibold\">{{ currentModel }}</span> collection and all its documents.\n </p>\n <div v-if=\"requiresDropCollectionNameConfirmation()\" class=\"rounded border border-red-300 bg-red-50 px-3 py-2 mb-4\">\n <p class=\"text-sm font-semibold text-red-700\">Danger zone</p>\n <p class=\"text-sm text-red-700 mb-2\">Type <span class=\"font-mono font-semibold\">{{ currentModel }}</span> to confirm.</p>\n <input\n v-model=\"dropCollectionConfirmName\"\n type=\"text\"\n :placeholder=\"currentModel\"\n class=\"w-full rounded border border-red-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-red-200\"\n />\n </div>\n <div class=\"flex gap-4\">\n <async-button\n @click=\"dropCollection\"\n :disabled=\"requiresDropCollectionNameConfirmation() && dropCollectionConfirmName !== currentModel\"\n class=\"rounded bg-red-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Drop Collection\n </async-button>\n <button @click=\"shouldShowDropCollectionModal = false\" class=\"rounded bg-gray-400 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-page0\">\n Cancel\n </button>\n </div>\n </template>\n </modal>\n <model-switcher\n :show=\"showModelSwitcher\"\n :models=\"models\"\n :recently-viewed-models=\"recentlyViewedModels\"\n :model-document-counts=\"modelDocumentCounts\"\n @close=\"showModelSwitcher = false\"\n @select=\"selectSwitcherModel\"\n ></model-switcher>\n</div>\n";
54394
54620
 
54395
54621
  /***/ },
54396
54622
 
@@ -54526,6 +54752,17 @@ module.exports = "<div class=\"mx-auto max-w-5xl py-6 px-2 flex flex-col gap-8\"
54526
54752
 
54527
54753
  /***/ },
54528
54754
 
54755
+ /***/ "./frontend/src/tooltip/tooltip.html"
54756
+ /*!*******************************************!*\
54757
+ !*** ./frontend/src/tooltip/tooltip.html ***!
54758
+ \*******************************************/
54759
+ (module) {
54760
+
54761
+ "use strict";
54762
+ module.exports = "<span\n ref=\"trigger\"\n class=\"inline-flex outline-none focus-visible:rounded-md focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600\"\n tabindex=\"0\"\n :aria-describedby=\"isOpen ? tooltipId : null\"\n @mouseenter=\"show\"\n @mouseleave=\"hideUnlessPinned\"\n @focusin=\"show\"\n @focusout=\"handleFocusOut\"\n @click=\"toggle\"\n @keydown.esc.stop=\"hide\"\n>\n <slot></slot>\n <span\n v-show=\"isOpen\"\n ref=\"tooltip\"\n :id=\"tooltipId\"\n class=\"pointer-events-none fixed z-[1000] w-max max-w-[min(20rem,calc(100vw-1rem))] whitespace-normal rounded-md bg-gray-900 px-2.5 py-1.5 text-xs font-normal leading-4 text-white shadow-lg\"\n :style=\"tooltipStyle\"\n role=\"tooltip\"\n >\n {{ text }}\n <span\n class=\"absolute left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-gray-900\"\n :class=\"placement === 'bottom' ? '-top-1' : '-bottom-1'\"\n ></span>\n </span>\n</span>\n";
54763
+
54764
+ /***/ },
54765
+
54529
54766
  /***/ "./frontend/src/update-document/update-document.css"
54530
54767
  /*!**********************************************************!*\
54531
54768
  !*** ./frontend/src/update-document/update-document.css ***!
@@ -62994,11 +63231,20 @@ class ObjectId extends BSONValue {
62994
63231
 
62995
63232
  function internalCalculateObjectSize(object, serializeFunctions, ignoreUndefined) {
62996
63233
  const objectStack = [
62997
- { obj: object, ignoreUndefined: ignoreUndefined ?? false }
63234
+ { object, ignoreUndefined: ignoreUndefined ?? false, exit: false }
62998
63235
  ];
63236
+ const path = new Set();
62999
63237
  let total = 0;
63000
63238
  while (objectStack.length > 0) {
63001
- const { obj, ignoreUndefined: frameIgnoreUndefined } = objectStack.pop();
63239
+ const frame = objectStack.pop();
63240
+ if (frame.exit) {
63241
+ path.delete(frame.object);
63242
+ continue;
63243
+ }
63244
+ const { object: obj, ignoreUndefined: frameIgnoreUndefined } = frame;
63245
+ path.add(obj);
63246
+ frame.exit = true;
63247
+ objectStack.push(frame);
63002
63248
  total += 5;
63003
63249
  const isObjArray = Array.isArray(obj);
63004
63250
  const isObjMap = !isObjArray && (obj instanceof Map || isMap(obj));
@@ -63009,23 +63255,23 @@ function internalCalculateObjectSize(object, serializeFunctions, ignoreUndefined
63009
63255
  if (isObjArray) {
63010
63256
  const array = target;
63011
63257
  for (let i = 0; i < array.length; i++) {
63012
- total += calculateElementSize(i.toString(), array[i], serializeFunctions, true, frameIgnoreUndefined, objectStack);
63258
+ total += calculateElementSize(i.toString(), array[i], serializeFunctions, true, frameIgnoreUndefined, objectStack, path);
63013
63259
  }
63014
63260
  }
63015
63261
  else if (isObjMap) {
63016
63262
  for (const [key, value] of target) {
63017
- total += calculateElementSize(key, value, serializeFunctions, false, frameIgnoreUndefined, objectStack);
63263
+ total += calculateElementSize(key, value, serializeFunctions, false, frameIgnoreUndefined, objectStack, path);
63018
63264
  }
63019
63265
  }
63020
63266
  else {
63021
63267
  for (const key of Object.keys(target)) {
63022
- total += calculateElementSize(key, target[key], serializeFunctions, false, frameIgnoreUndefined, objectStack);
63268
+ total += calculateElementSize(key, target[key], serializeFunctions, false, frameIgnoreUndefined, objectStack, path);
63023
63269
  }
63024
63270
  }
63025
63271
  }
63026
63272
  return total;
63027
63273
  }
63028
- function calculateElementSize(name, value, serializeFunctions = false, isArray = false, ignoreUndefined = false, objectStack) {
63274
+ function calculateElementSize(name, value, serializeFunctions = false, isArray = false, ignoreUndefined = false, objectStack, path) {
63029
63275
  if (typeof value?.toBSON === 'function') {
63030
63276
  value = value.toBSON();
63031
63277
  }
@@ -63085,7 +63331,10 @@ function calculateElementSize(name, value, serializeFunctions = false, isArray =
63085
63331
  }
63086
63332
  else if (value._bsontype === 'Code') {
63087
63333
  if (value.scope != null && Object.keys(value.scope).length > 0) {
63088
- objectStack.push({ obj: value.scope, ignoreUndefined });
63334
+ if (path.has(value.scope)) {
63335
+ throw new BSONError('Cannot convert circular structure to BSON');
63336
+ }
63337
+ objectStack.push({ object: value.scope, ignoreUndefined, exit: false });
63089
63338
  return (ByteUtils.utf8ByteLength(name) +
63090
63339
  1 +
63091
63340
  1 +
@@ -63123,7 +63372,7 @@ function calculateElementSize(name, value, serializeFunctions = false, isArray =
63123
63372
  if (value.db != null) {
63124
63373
  ordered_values['$db'] = value.db;
63125
63374
  }
63126
- objectStack.push({ obj: ordered_values, ignoreUndefined: true });
63375
+ objectStack.push({ object: ordered_values, ignoreUndefined: true, exit: false });
63127
63376
  return ByteUtils.utf8ByteLength(name) + 1 + 1;
63128
63377
  }
63129
63378
  else if (value instanceof RegExp || isRegExp(value)) {
@@ -63147,7 +63396,10 @@ function calculateElementSize(name, value, serializeFunctions = false, isArray =
63147
63396
  1);
63148
63397
  }
63149
63398
  else {
63150
- objectStack.push({ obj: value, ignoreUndefined });
63399
+ if (path.has(value)) {
63400
+ throw new BSONError('Cannot convert circular structure to BSON');
63401
+ }
63402
+ objectStack.push({ object: value, ignoreUndefined, exit: false });
63151
63403
  return ByteUtils.utf8ByteLength(name) + 1 + 1;
63152
63404
  }
63153
63405
  case 'function':
@@ -66010,7 +66262,7 @@ var src_default = VueToastificationPlugin;
66010
66262
  (module) {
66011
66263
 
66012
66264
  "use strict";
66013
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.13","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","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","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"@nestjs/common":"10.x || 11.x","mongoose":"7.x || 8.x || ^9.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true}},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","@nestjs/common":"^11.1.9","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","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","eval:agent":"node eval/agent/run.js"}}');
66265
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.5.14","description":"AI-Powered Data Workspace for MongoDB: Turn your MongoDB data into dashboards, maps, and interactive workflows - powered by your Mongoose models and AI.","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","ace-builds":"^1.43.6","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","mpath":"^0.9.0","node-inspect-extracted":"3.x","regexp.escape":"^2.0.1","tailwindcss":"3.4.0","time-commando":"1.0.1","typescript":"^6.0.2","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x","xss":"^1.0.15"},"peerDependencies":{"@nestjs/common":"10.x || 11.x","mongoose":"7.x || 8.x || ^9.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true}},"optionalPeerDependencies":{"@mongoosejs/task":"0.5.x || 0.6.x"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","@nestjs/common":"^11.1.9","express":"4.x","mocha":"10.2.0","mongodb-memory-server":"^11.0.1","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","seed":"node seed/index.js","start":"node ./local.js","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","eval:agent":"node eval/agent/run.js"}}');
66014
66266
 
66015
66267
  /***/ }
66016
66268
 
@@ -66051,75 +66303,46 @@ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version"
66051
66303
  /******/
66052
66304
  /************************************************************************/
66053
66305
  /******/ /* webpack/runtime/amd define */
66054
- /******/ (() => {
66055
- /******/ __webpack_require__.amdD = function () {
66056
- /******/ throw new Error('define cannot be used indirect');
66057
- /******/ };
66058
- /******/ })();
66306
+ /******/ __webpack_require__.amdD = function () {
66307
+ /******/ throw new Error('define cannot be used indirect');
66308
+ /******/ };
66059
66309
  /******/
66060
66310
  /******/ /* webpack/runtime/define property getters */
66061
- /******/ (() => {
66062
- /******/ // define getter/value functions for harmony exports
66063
- /******/ __webpack_require__.d = (exports, definition) => {
66064
- /******/ if(Array.isArray(definition)) {
66065
- /******/ var i = 0;
66066
- /******/ while(i < definition.length) {
66067
- /******/ var key = definition[i++];
66068
- /******/ var binding = definition[i++];
66069
- /******/ if(!__webpack_require__.o(exports, key)) {
66070
- /******/ if(binding === 0) {
66071
- /******/ Object.defineProperty(exports, key, { enumerable: true, value: definition[i++] });
66072
- /******/ } else {
66073
- /******/ Object.defineProperty(exports, key, { enumerable: true, get: binding });
66074
- /******/ }
66075
- /******/ } else if(binding === 0) { i++; }
66076
- /******/ }
66077
- /******/ } else {
66078
- /******/ for(var key in definition) {
66079
- /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
66080
- /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
66081
- /******/ }
66082
- /******/ }
66311
+ /******/ // define getter/value functions for harmony exports
66312
+ /******/ __webpack_require__.d = (exports, definition) => {
66313
+ /******/ for(var key in definition) {
66314
+ /******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
66315
+ /******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
66083
66316
  /******/ }
66084
- /******/ };
66085
- /******/ })();
66317
+ /******/ }
66318
+ /******/ };
66086
66319
  /******/
66087
66320
  /******/ /* webpack/runtime/global */
66088
- /******/ (() => {
66089
- /******/ __webpack_require__.g = (function() {
66090
- /******/ if (typeof globalThis === 'object') return globalThis;
66091
- /******/ try {
66092
- /******/ return this || new Function('return this')();
66093
- /******/ } catch (e) {
66094
- /******/ if (typeof window === 'object') return window;
66095
- /******/ }
66096
- /******/ })();
66321
+ /******/ __webpack_require__.g = (function() {
66322
+ /******/ if (typeof globalThis === 'object') return globalThis;
66323
+ /******/ try {
66324
+ /******/ return this || new Function('return this')();
66325
+ /******/ } catch (e) {
66326
+ /******/ if (typeof window === 'object') return window;
66327
+ /******/ }
66097
66328
  /******/ })();
66098
66329
  /******/
66099
66330
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
66100
- /******/ (() => {
66101
- /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
66102
- /******/ })();
66331
+ /******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop));
66103
66332
  /******/
66104
66333
  /******/ /* webpack/runtime/make namespace object */
66105
- /******/ (() => {
66106
- /******/ // define __esModule on exports
66107
- /******/ __webpack_require__.r = (exports) => {
66108
- /******/ if(Symbol.toStringTag) {
66109
- /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
66110
- /******/ }
66111
- /******/ Object.defineProperty(exports, '__esModule', { value: true });
66112
- /******/ };
66113
- /******/ })();
66334
+ /******/ // define __esModule on exports
66335
+ /******/ __webpack_require__.r = (exports) => {
66336
+ /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
66337
+ /******/ Object.defineProperty(exports, '__esModule', { value: true });
66338
+ /******/ };
66114
66339
  /******/
66115
66340
  /******/ /* webpack/runtime/node module decorator */
66116
- /******/ (() => {
66117
- /******/ __webpack_require__.nmd = (module) => {
66118
- /******/ module.paths = [];
66119
- /******/ if (!module.children) module.children = [];
66120
- /******/ return module;
66121
- /******/ };
66122
- /******/ })();
66341
+ /******/ __webpack_require__.nmd = (module) => {
66342
+ /******/ module.paths = [];
66343
+ /******/ if (!module.children) module.children = [];
66344
+ /******/ return module;
66345
+ /******/ };
66123
66346
  /******/
66124
66347
  /************************************************************************/
66125
66348
  /******/