@mongoosejs/studio 0.4.0 → 0.4.1

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.
@@ -4416,6 +4416,84 @@ module.exports = app => app.component('detail-default', {
4416
4416
  });
4417
4417
 
4418
4418
 
4419
+ /***/ },
4420
+
4421
+ /***/ "./frontend/src/detail-objectid/detail-objectid.js"
4422
+ /*!*********************************************************!*\
4423
+ !*** ./frontend/src/detail-objectid/detail-objectid.js ***!
4424
+ \*********************************************************/
4425
+ (module, __unused_webpack_exports, __webpack_require__) {
4426
+
4427
+ "use strict";
4428
+
4429
+
4430
+ const template = __webpack_require__(/*! ./detail-objectid.html */ "./frontend/src/detail-objectid/detail-objectid.html");
4431
+ const ObjectId = __webpack_require__(/*! mongoose/lib/types/objectid */ "./node_modules/mongoose/lib/types/objectid.js");
4432
+
4433
+ module.exports = app => app.component('detail-objectid', {
4434
+ template,
4435
+ props: ['value', 'viewMode'],
4436
+ emits: ['updated'],
4437
+ watch: {
4438
+ displayValue: {
4439
+ immediate: true,
4440
+ handler(val) {
4441
+ this.$emit('updated', val);
4442
+ }
4443
+ }
4444
+ },
4445
+ computed: {
4446
+ format() {
4447
+ return this.viewMode || 'hex';
4448
+ },
4449
+ hexValue() {
4450
+ if (this.value == null) {
4451
+ return null;
4452
+ }
4453
+ if (typeof this.value === 'string') {
4454
+ return this.value;
4455
+ }
4456
+ if (typeof this.value === 'object') {
4457
+ if (typeof this.value.toHexString === 'function') {
4458
+ return this.value.toHexString();
4459
+ }
4460
+ if (typeof this.value.$oid === 'string') {
4461
+ return this.value.$oid;
4462
+ }
4463
+ }
4464
+ return String(this.value);
4465
+ },
4466
+ objectIdDate() {
4467
+ const hex = this.hexValue;
4468
+ return new ObjectId(hex).getTimestamp();
4469
+ },
4470
+ displayValue() {
4471
+ if (this.value == null) {
4472
+ return String(this.value);
4473
+ }
4474
+
4475
+ const hex = this.hexValue;
4476
+ if (this.format === 'object_call') {
4477
+ return `ObjectId('${hex}')`;
4478
+ }
4479
+ if (this.format === 'unix_seconds') {
4480
+ if (!this.objectIdDate) {
4481
+ return 'Invalid ObjectId';
4482
+ }
4483
+ return String(Math.floor(this.objectIdDate.getTime() / 1000));
4484
+ }
4485
+ if (this.format === 'date') {
4486
+ if (!this.objectIdDate) {
4487
+ return 'Invalid ObjectId';
4488
+ }
4489
+ return this.objectIdDate.toISOString();
4490
+ }
4491
+ return hex;
4492
+ }
4493
+ }
4494
+ });
4495
+
4496
+
4419
4497
  /***/ },
4420
4498
 
4421
4499
  /***/ "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js"
@@ -4940,6 +5018,7 @@ module.exports = app => app.component('document-property', {
4940
5018
  return {
4941
5019
  dateType: 'picker', // picker, iso
4942
5020
  dateViewMode: 'utc_iso',
5021
+ objectIdViewMode: 'hex',
4943
5022
  renderedValue: UNSET,
4944
5023
  isCollapsed: false, // Start uncollapsed by default
4945
5024
  isValueExpanded: false, // Track if the value is expanded
@@ -4960,6 +5039,9 @@ module.exports = app => app.component('document-property', {
4960
5039
  isDatePath() {
4961
5040
  return this.path?.instance === 'Date';
4962
5041
  },
5042
+ isObjectIdPath() {
5043
+ return this.path?.instance === 'ObjectID' || this.path?.instance === 'ObjectId';
5044
+ },
4963
5045
  rawValue() {
4964
5046
  return this.getValueForPath(this.path.path);
4965
5047
  },
@@ -5099,6 +5181,9 @@ module.exports = app => app.component('document-property', {
5099
5181
  if (schemaPath.instance === 'Date') {
5100
5182
  return 'detail-date';
5101
5183
  }
5184
+ if (schemaPath.instance === 'ObjectID' || schemaPath.instance === 'ObjectId') {
5185
+ return 'detail-objectid';
5186
+ }
5102
5187
  return 'detail-default';
5103
5188
  },
5104
5189
  getEditComponentForPath(path) {
@@ -5226,6 +5311,36 @@ module.exports = app => app.component('document-property', {
5226
5311
  });
5227
5312
 
5228
5313
 
5314
+ /***/ },
5315
+
5316
+ /***/ "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js"
5317
+ /*!**********************************************************************************************!*\
5318
+ !*** ./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js ***!
5319
+ \**********************************************************************************************/
5320
+ (module, __unused_webpack_exports, __webpack_require__) {
5321
+
5322
+ "use strict";
5323
+
5324
+
5325
+ const template = __webpack_require__(/*! ./objectid-view-mode-picker.html */ "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html");
5326
+
5327
+ module.exports = app => app.component('objectid-view-mode-picker', {
5328
+ template,
5329
+ props: ['viewMode'],
5330
+ emits: ['update:viewMode'],
5331
+ computed: {
5332
+ format() {
5333
+ return this.viewMode || 'hex';
5334
+ }
5335
+ },
5336
+ methods: {
5337
+ onFormatChange(newFormat) {
5338
+ this.$emit('update:viewMode', newFormat);
5339
+ }
5340
+ }
5341
+ });
5342
+
5343
+
5229
5344
  /***/ },
5230
5345
 
5231
5346
  /***/ "./frontend/src/document/confirm-changes/confirm-changes.js"
@@ -7288,6 +7403,12 @@ module.exports = app => app.component('document-search', {
7288
7403
  this.$refs.searchInput.focus();
7289
7404
  },
7290
7405
  methods: {
7406
+ focusInput() {
7407
+ const input = this.$refs.searchInput;
7408
+ if (input && typeof input.focus === 'function') {
7409
+ input.focus();
7410
+ }
7411
+ },
7291
7412
  emitSearch() {
7292
7413
  this.$emit('input', this.searchText);
7293
7414
  this.$emit('search', this.searchText);
@@ -7540,58 +7661,126 @@ const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
7540
7661
  const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
7541
7662
  const SHOW_ROW_NUMBERS_STORAGE_KEY = 'studio:model-show-row-numbers';
7542
7663
  const PROJECTION_MODE_QUERY_KEY = 'projectionMode';
7664
+ const PROJECTION_INPUT_QUERY_KEY = 'projectionInput';
7543
7665
  const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
7544
7666
  const MAX_RECENT_MODELS = 4;
7667
+ const FOCUS_AFTER_MODELS_REMOUNT_KEY = '__studioModelsFocusAfterRemount';
7545
7668
 
7546
- /** Parse `fields` from the route (JSON array or inclusion projection object only). */
7547
- function parseFieldsQueryParam(fields) {
7548
- if (fields == null || fields === '') {
7549
- return [];
7550
- }
7551
- const s = typeof fields === 'string' ? fields : String(fields);
7552
- const trimmed = s.trim();
7553
- if (!trimmed) {
7554
- return [];
7555
- }
7556
- let parsed;
7557
- try {
7558
- parsed = JSON.parse(trimmed);
7559
- } catch (e) {
7560
- return [];
7669
+ function setModelsRemountFocusIntent(target) {
7670
+ if (typeof window === 'undefined') {
7671
+ return;
7561
7672
  }
7562
- if (Array.isArray(parsed)) {
7563
- return parsed.map(x => String(x).trim()).filter(Boolean);
7673
+ window[FOCUS_AFTER_MODELS_REMOUNT_KEY] = target;
7674
+ }
7675
+
7676
+ function takeModelsRemountFocusIntent() {
7677
+ if (typeof window === 'undefined') {
7678
+ return null;
7564
7679
  }
7565
- if (parsed != null && typeof parsed === 'object') {
7566
- return Object.keys(parsed).filter(k =>
7567
- Object.prototype.hasOwnProperty.call(parsed, k) && parsed[k]
7568
- );
7680
+ const v = window[FOCUS_AFTER_MODELS_REMOUNT_KEY] || null;
7681
+ delete window[FOCUS_AFTER_MODELS_REMOUNT_KEY];
7682
+ return v;
7683
+ }
7684
+
7685
+ /**
7686
+ * Split projection list tokens on commas/whitespace, merging a lone `-` or `+` with the next
7687
+ * chunk so `- password` and `-password` both exclude `password`.
7688
+ * @returns {string[]|null} null when a dangling `+`/`-` has no following field name.
7689
+ */
7690
+ function normalizeProjectionTokens(trimmed) {
7691
+ const rawTokens = trimmed.split(/[,\s]+/).filter(Boolean);
7692
+ const tokens = [];
7693
+ for (let i = 0; i < rawTokens.length; i++) {
7694
+ const t = rawTokens[i];
7695
+ if (t === '-' || t === '+') {
7696
+ if (i + 1 >= rawTokens.length) {
7697
+ return null;
7698
+ }
7699
+ tokens.push(t + rawTokens[++i]);
7700
+ } else {
7701
+ tokens.push(t);
7702
+ }
7569
7703
  }
7570
- return [];
7704
+ return tokens;
7571
7705
  }
7572
7706
 
7573
- /** Pass through a valid JSON `fields` string for Model.getDocuments / getDocumentsStream. */
7574
- function normalizeFieldsParamForApi(fieldsStr) {
7575
- if (fieldsStr == null || fieldsStr === '') {
7707
+ /**
7708
+ * Parse projection object syntax like:
7709
+ * { deletedAt: 1 }
7710
+ * { "deletedAt": true, email: 1 }
7711
+ * { password: 0 }
7712
+ *
7713
+ * Supported values are 1/0/true/false (quoted or unquoted).
7714
+ * Returns include paths, exclude-derived include paths, or null when invalid.
7715
+ */
7716
+ function parseProjectionObjectNotation(trimmed, schemaPaths) {
7717
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
7576
7718
  return null;
7577
7719
  }
7578
- const s = typeof fieldsStr === 'string' ? fieldsStr : String(fieldsStr);
7579
- const trimmed = s.trim();
7580
- if (!trimmed) {
7581
- return null;
7720
+ const body = trimmed.slice(1, -1).trim();
7721
+ if (!body) {
7722
+ return [];
7582
7723
  }
7583
- try {
7584
- const parsed = JSON.parse(trimmed);
7585
- if (Array.isArray(parsed)) {
7586
- return trimmed;
7724
+
7725
+ const pairRe = /(?:^|,)\s*(?:"([^"]+)"|'([^']+)'|([a-zA-Z_$][\w.$]*))\s*:\s*("[^"]*"|'[^']*'|[^,]+?)\s*(?=,|$)/g;
7726
+ const includeKeys = [];
7727
+ const excludeKeys = [];
7728
+ let matchedChars = '';
7729
+ let match;
7730
+
7731
+ while ((match = pairRe.exec(body)) !== null) {
7732
+ matchedChars += match[0];
7733
+ const key = (match[1] || match[2] || match[3] || '').trim();
7734
+ const rawValue = (match[4] || '').trim();
7735
+ if (!key || !rawValue) {
7736
+ return null;
7587
7737
  }
7588
- if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
7589
- return trimmed;
7738
+
7739
+ const valueLower = rawValue.replace(/^['"]|['"]$/g, '').trim().toLowerCase();
7740
+ const isInclude = valueLower === '1' || valueLower === 'true';
7741
+ const isExclude = valueLower === '0' || valueLower === 'false';
7742
+ if (!isInclude && !isExclude) {
7743
+ return null;
7590
7744
  }
7591
- } catch (e) {
7745
+ if (isInclude) {
7746
+ includeKeys.push(key);
7747
+ } else {
7748
+ excludeKeys.push(key);
7749
+ }
7750
+ }
7751
+
7752
+ // Reject malformed object strings where regex skipped segments.
7753
+ const normalizedBody = body.replace(/\s+/g, '');
7754
+ const normalizedMatched = matchedChars.replace(/\s+/g, '').replace(/^,/, '');
7755
+ if (!normalizedMatched || normalizedMatched !== normalizedBody) {
7592
7756
  return null;
7593
7757
  }
7594
- return null;
7758
+
7759
+ const normalizeKey = (key) => String(key).trim();
7760
+ if (includeKeys.length > 0 && excludeKeys.length > 0) {
7761
+ const includeSet = new Set(includeKeys.map(normalizeKey));
7762
+ for (const path of excludeKeys) {
7763
+ const ex = normalizeKey(path);
7764
+ for (const k of Array.from(includeSet)) {
7765
+ if (k.toLowerCase() === ex.toLowerCase()) {
7766
+ includeSet.delete(k);
7767
+ }
7768
+ }
7769
+ }
7770
+ if (includeSet.size === 0) {
7771
+ return null;
7772
+ }
7773
+ return Array.from(includeSet);
7774
+ }
7775
+
7776
+ if (excludeKeys.length > 0) {
7777
+ const excludeNorm = excludeKeys.map(normalizeKey);
7778
+ return schemaPaths
7779
+ .map(p => p.path)
7780
+ .filter(p => !excludeNorm.some(ex => ex === p || p.toLowerCase() === ex.toLowerCase()));
7781
+ }
7782
+
7783
+ return includeKeys.map(normalizeKey);
7595
7784
  }
7596
7785
 
7597
7786
  module.exports = app => app.component('models', {
@@ -7736,10 +7925,19 @@ module.exports = app => app.component('models', {
7736
7925
  this.setOutputType('json');
7737
7926
  }
7738
7927
  this.$nextTick(() => {
7739
- if (!this.isProjectionMenuSelected) return;
7740
- const input = this.$refs.projectionInput;
7741
- if (input && typeof input.focus === 'function') {
7742
- input.focus();
7928
+ const focusIntent = takeModelsRemountFocusIntent();
7929
+ if (focusIntent === 'documentSearch') {
7930
+ const ds = this.$refs.documentSearch;
7931
+ if (ds && typeof ds.focusInput === 'function') {
7932
+ ds.focusInput();
7933
+ }
7934
+ return;
7935
+ }
7936
+ if (focusIntent === 'projection' && this.isProjectionMenuSelected) {
7937
+ const projectionSearch = this.$refs.projectionSearch;
7938
+ if (projectionSearch && typeof projectionSearch.focusInput === 'function') {
7939
+ projectionSearch.focusInput();
7940
+ }
7743
7941
  }
7744
7942
  });
7745
7943
  },
@@ -8146,21 +8344,21 @@ module.exports = app => app.component('models', {
8146
8344
  params.searchText = this.searchText;
8147
8345
  }
8148
8346
 
8149
- // Prefer explicit URL projection (`query.fields`) so the first fetch after
8150
- // mount/remount respects deep-linked projections before `filteredPaths`
8151
- // is rehydrated from schema paths.
8152
- let fieldsParam = normalizeFieldsParamForApi(this.query?.fields);
8153
- if (!fieldsParam && this.isProjectionMenuSelected === true) {
8347
+ const projectionInput = typeof this.query?.[PROJECTION_INPUT_QUERY_KEY] === 'string' ?
8348
+ this.query[PROJECTION_INPUT_QUERY_KEY].trim() :
8349
+ '';
8350
+ if (projectionInput) {
8351
+ params.projectionInput = projectionInput;
8352
+ }
8353
+
8354
+ if (!projectionInput && this.isProjectionMenuSelected === true) {
8154
8355
  const fieldPaths = this.filteredPaths && this.filteredPaths.length > 0
8155
8356
  ? this.filteredPaths.map(p => p.path).filter(Boolean)
8156
8357
  : null;
8157
8358
  if (fieldPaths && fieldPaths.length > 0) {
8158
- fieldsParam = JSON.stringify(fieldPaths);
8359
+ params.projectionInput = fieldPaths.join(' ');
8159
8360
  }
8160
8361
  }
8161
- if (fieldsParam) {
8162
- params.fields = fieldsParam;
8163
- }
8164
8362
 
8165
8363
  return params;
8166
8364
  },
@@ -8195,13 +8393,16 @@ module.exports = app => app.component('models', {
8195
8393
  if (this.currentModel != null) {
8196
8394
  await this.getDocuments();
8197
8395
  }
8198
- if (this.$route.query?.fields) {
8199
- const urlPaths = parseFieldsQueryParam(this.$route.query.fields);
8200
- if (urlPaths.length > 0) {
8201
- this.filteredPaths = urlPaths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
8202
- if (this.filteredPaths.length > 0) {
8203
- this.syncProjectionFromPaths();
8204
- }
8396
+ const routeProjectionInput = this.$route.query?.[PROJECTION_INPUT_QUERY_KEY];
8397
+ if (typeof routeProjectionInput === 'string' && routeProjectionInput.trim().length > 0) {
8398
+ const urlPaths = this.normalizeProjectionPathsForDisplay(routeProjectionInput);
8399
+ if (urlPaths !== null) {
8400
+ const nextFiltered = urlPaths
8401
+ .map(path => this.schemaPaths.find(p => p.path === path))
8402
+ .filter(Boolean);
8403
+ this.filteredPaths = nextFiltered;
8404
+ this.selectedPaths = [...this.filteredPaths];
8405
+ this.syncProjectionInputFromQueryOrPaths();
8205
8406
  }
8206
8407
  }
8207
8408
  this.status = 'loaded';
@@ -8310,6 +8511,7 @@ module.exports = app => app.component('models', {
8310
8511
  } else {
8311
8512
  delete this.query.search;
8312
8513
  }
8514
+ setModelsRemountFocusIntent('documentSearch');
8313
8515
  const query = this.query;
8314
8516
  this.$router.push({ query });
8315
8517
  this.documents = [];
@@ -8344,11 +8546,13 @@ module.exports = app => app.component('models', {
8344
8546
  // Persist projection UI state in the URL so Reset/Suggest don't turn the mode off.
8345
8547
  if (next) {
8346
8548
  this.query[PROJECTION_MODE_QUERY_KEY] = '1';
8549
+ setModelsRemountFocusIntent('projection');
8347
8550
  if (this.outputType === 'map') {
8348
8551
  this.setOutputType('json');
8349
8552
  }
8350
8553
  } else {
8351
8554
  delete this.query[PROJECTION_MODE_QUERY_KEY];
8555
+ delete this.query[PROJECTION_INPUT_QUERY_KEY];
8352
8556
  delete this.query.fields;
8353
8557
  this.filteredPaths = [];
8354
8558
  this.selectedPaths = [];
@@ -8494,14 +8698,25 @@ module.exports = app => app.component('models', {
8494
8698
  }
8495
8699
  const isProjectionModeOn = this.isProjectionMenuSelected === true;
8496
8700
  if (isProjectionModeOn) {
8497
- this.applyDefaultProjection(event.suggestedFields);
8701
+ const routeProjectionInput = this.$route.query?.[PROJECTION_INPUT_QUERY_KEY];
8702
+ if (typeof routeProjectionInput === 'string' && routeProjectionInput.trim().length > 0) {
8703
+ const projectionPaths = this.normalizeProjectionPathsForDisplay(routeProjectionInput);
8704
+ const fromProjectionInput = Array.isArray(projectionPaths) ?
8705
+ projectionPaths
8706
+ .map(path => this.schemaPaths.find(p => p.path === path))
8707
+ .filter(Boolean) :
8708
+ [];
8709
+ this.filteredPaths = fromProjectionInput;
8710
+ } else {
8711
+ this.filteredPaths = [];
8712
+ }
8498
8713
  } else {
8499
8714
  this.filteredPaths = [];
8500
8715
  this.selectedPaths = [];
8501
8716
  this.projectionText = '';
8502
8717
  }
8503
8718
  this.selectedPaths = [...this.filteredPaths];
8504
- this.syncProjectionFromPaths();
8719
+ this.syncProjectionInputFromQueryOrPaths();
8505
8720
  schemaPathsReceived = true;
8506
8721
  }
8507
8722
  if (event.numDocs !== undefined) {
@@ -8582,6 +8797,10 @@ module.exports = app => app.component('models', {
8582
8797
  // Keep current filter input in sync with the URL so projection reset
8583
8798
  // does not unintentionally wipe the filter on remount.
8584
8799
  this.syncFilterToQuery();
8800
+ if (this.$route.query?.fields) {
8801
+ delete this.query.fields;
8802
+ }
8803
+ delete this.query[PROJECTION_INPUT_QUERY_KEY];
8585
8804
  this.filteredPaths = [];
8586
8805
  this.selectedPaths = [];
8587
8806
  this.projectionText = '';
@@ -8604,18 +8823,9 @@ module.exports = app => app.component('models', {
8604
8823
  this.applyDefaultProjection(pathNames.slice(0, DEFAULT_FIRST_N_FIELDS));
8605
8824
  this.selectedPaths = [...this.filteredPaths];
8606
8825
  this.syncProjectionFromPaths();
8826
+ setModelsRemountFocusIntent('projection');
8607
8827
  this.updateProjectionQuery();
8608
8828
  },
8609
- initProjection(ev) {
8610
- if (!this.projectionText || !this.projectionText.trim()) {
8611
- this.projectionText = '';
8612
- this.$nextTick(() => {
8613
- if (ev && ev.target) {
8614
- ev.target.setSelectionRange(0, 0);
8615
- }
8616
- });
8617
- }
8618
- },
8619
8829
  syncProjectionFromPaths() {
8620
8830
  if (this.filteredPaths.length === 0) {
8621
8831
  this.projectionText = '';
@@ -8625,6 +8835,29 @@ module.exports = app => app.component('models', {
8625
8835
  // Since `filteredPaths` represents the final include set, we serialize as space-separated fields.
8626
8836
  this.projectionText = this.filteredPaths.map(p => p.path).join(' ');
8627
8837
  },
8838
+ syncProjectionInputFromQueryOrPaths() {
8839
+ const fromQuery = this.$route.query?.[PROJECTION_INPUT_QUERY_KEY];
8840
+ if (typeof fromQuery === 'string' && fromQuery.trim().length > 0) {
8841
+ this.projectionText = fromQuery;
8842
+ return;
8843
+ }
8844
+ this.syncProjectionFromPaths();
8845
+ },
8846
+ normalizeProjectionPathsForDisplay(text) {
8847
+ const paths = this.parseProjectionInput(text);
8848
+ if (paths == null) {
8849
+ return null;
8850
+ }
8851
+ const excludesId = this.projectionExplicitlyExcludesId(text);
8852
+ const hasIdSchemaPath = this.schemaPaths.some(p => p.path === '_id');
8853
+ if (hasIdSchemaPath && !excludesId) {
8854
+ const hasIdAlready = paths.some(p => String(p).toLowerCase() === '_id');
8855
+ if (!hasIdAlready) {
8856
+ paths.unshift('_id');
8857
+ }
8858
+ }
8859
+ return paths;
8860
+ },
8628
8861
  parseProjectionInput(text) {
8629
8862
  if (!text || typeof text !== 'string') {
8630
8863
  return [];
@@ -8635,17 +8868,21 @@ module.exports = app => app.component('models', {
8635
8868
  }
8636
8869
  const normalizeKey = (key) => String(key).trim();
8637
8870
 
8638
- // String-only projection syntax:
8871
+ // String projection syntax:
8639
8872
  // name email
8640
8873
  // -password (exclusion-only)
8641
8874
  // +email (inclusion-only)
8875
+ // { deletedAt: 1 } (object notation)
8642
8876
  //
8643
- // Brace/object syntax is intentionally NOT supported.
8644
- if (trimmed.startsWith('{') || trimmed.endsWith('}')) {
8645
- return null;
8877
+ // For object notation, return include paths based on 1/0/true/false values.
8878
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
8879
+ return parseProjectionObjectNotation(trimmed, this.schemaPaths);
8646
8880
  }
8647
8881
 
8648
- const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
8882
+ const tokens = normalizeProjectionTokens(trimmed);
8883
+ if (tokens === null) {
8884
+ return null;
8885
+ }
8649
8886
  if (tokens.length === 0) return [];
8650
8887
 
8651
8888
  const includeKeys = [];
@@ -8674,22 +8911,78 @@ module.exports = app => app.component('models', {
8674
8911
  // `name email createdAt -email` -> `name createdAt`.
8675
8912
  const includeSet = new Set(includeKeys.map(normalizeKey));
8676
8913
  for (const path of excludeKeys) {
8677
- includeSet.delete(normalizeKey(path));
8914
+ const ex = normalizeKey(path);
8915
+ for (const k of Array.from(includeSet)) {
8916
+ if (k.toLowerCase() === ex.toLowerCase()) {
8917
+ includeSet.delete(k);
8918
+ }
8919
+ }
8920
+ }
8921
+ if (includeSet.size === 0) {
8922
+ return null;
8678
8923
  }
8679
8924
  return Array.from(includeSet);
8680
8925
  }
8681
8926
 
8682
8927
  if (excludeKeys.length > 0) {
8683
- const excludeSet = new Set(excludeKeys.map(normalizeKey));
8684
- return this.schemaPaths.map(p => p.path).filter(p => !excludeSet.has(p));
8928
+ const excludeNorm = excludeKeys.map(normalizeKey);
8929
+ return this.schemaPaths
8930
+ .map(p => p.path)
8931
+ .filter(p => !excludeNorm.some(ex => ex === p || p.toLowerCase() === ex.toLowerCase()));
8685
8932
  }
8686
8933
 
8687
8934
  return includeKeys.map(normalizeKey);
8688
8935
  },
8936
+ projectionExplicitlyExcludesId(text) {
8937
+ if (!text || typeof text !== 'string') {
8938
+ return false;
8939
+ }
8940
+ const trimmed = text.trim();
8941
+ if (!trimmed) {
8942
+ return false;
8943
+ }
8944
+
8945
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
8946
+ const body = trimmed.slice(1, -1).trim();
8947
+ if (!body) {
8948
+ return false;
8949
+ }
8950
+ const pairRe = /(?:^|,)\s*(?:"([^"]+)"|'([^']+)'|([a-zA-Z_$][\w.$]*))\s*:\s*("[^"]*"|'[^']*'|[^,]+?)\s*(?=,|$)/g;
8951
+ let match;
8952
+ while ((match = pairRe.exec(body)) !== null) {
8953
+ const key = (match[1] || match[2] || match[3] || '').trim();
8954
+ if (!key || key.toLowerCase() !== '_id') {
8955
+ continue;
8956
+ }
8957
+ const rawValue = (match[4] || '').trim();
8958
+ const valueLower = rawValue.replace(/^['"]|['"]$/g, '').trim().toLowerCase();
8959
+ if (valueLower === '0' || valueLower === 'false') {
8960
+ return true;
8961
+ }
8962
+ }
8963
+ return false;
8964
+ }
8965
+
8966
+ const tokens = normalizeProjectionTokens(trimmed);
8967
+ if (!tokens) {
8968
+ return false;
8969
+ }
8970
+ return tokens.some((rawToken) => {
8971
+ const token = rawToken.trim();
8972
+ return token.toLowerCase() === '-_id';
8973
+ });
8974
+ },
8689
8975
  applyProjectionFromInput() {
8690
- const paths = this.parseProjectionInput(this.projectionText);
8691
- if (paths === null) {
8692
- this.syncProjectionFromPaths();
8976
+ if (typeof this.projectionText === 'string' && this.projectionText.trim().length === 0) {
8977
+ this.filteredPaths = [];
8978
+ this.selectedPaths = [];
8979
+ setModelsRemountFocusIntent('projection');
8980
+ this.updateProjectionQuery();
8981
+ return;
8982
+ }
8983
+
8984
+ const paths = this.normalizeProjectionPathsForDisplay(this.projectionText);
8985
+ if (paths == null) {
8693
8986
  return;
8694
8987
  }
8695
8988
  if (paths.length === 0) {
@@ -8711,16 +9004,16 @@ module.exports = app => app.component('models', {
8711
9004
  }
8712
9005
  }
8713
9006
  this.selectedPaths = [...this.filteredPaths];
8714
- this.syncProjectionFromPaths();
9007
+ setModelsRemountFocusIntent('projection');
8715
9008
  this.updateProjectionQuery();
8716
9009
  },
8717
9010
  updateProjectionQuery() {
8718
- const paths = this.filteredPaths.map(x => x.path).filter(Boolean);
8719
- if (paths.length > 0) {
8720
- this.query.fields = JSON.stringify(paths);
9011
+ if (typeof this.projectionText === 'string' && this.projectionText.trim().length > 0) {
9012
+ this.query[PROJECTION_INPUT_QUERY_KEY] = this.projectionText;
8721
9013
  } else {
8722
- delete this.query.fields;
9014
+ delete this.query[PROJECTION_INPUT_QUERY_KEY];
8723
9015
  }
9016
+ delete this.query.fields;
8724
9017
  this.$router.push({ query: this.query });
8725
9018
  },
8726
9019
  removeField(schemaPath) {
@@ -8949,6 +9242,243 @@ module.exports = app => app.component('models', {
8949
9242
  });
8950
9243
 
8951
9244
 
9245
+ /***/ },
9246
+
9247
+ /***/ "./frontend/src/models/projection-search/projection-search.js"
9248
+ /*!********************************************************************!*\
9249
+ !*** ./frontend/src/models/projection-search/projection-search.js ***!
9250
+ \********************************************************************/
9251
+ (module, __unused_webpack_exports, __webpack_require__) {
9252
+
9253
+ "use strict";
9254
+
9255
+
9256
+ const template = __webpack_require__(/*! ./projection-search.html */ "./frontend/src/models/projection-search/projection-search.html");
9257
+
9258
+ function getBraceDepth(text, pos) {
9259
+ let depth = 0;
9260
+ for (let i = 0; i < pos; i++) {
9261
+ const ch = text[i];
9262
+ if (ch === "{") {
9263
+ depth++;
9264
+ } else if (ch === "}") {
9265
+ depth--;
9266
+ }
9267
+ }
9268
+ return depth;
9269
+ }
9270
+
9271
+ function collectObjectKeysBefore(text, endPos) {
9272
+ const before = text.slice(0, endPos);
9273
+ const keys = new Set();
9274
+ const re = /([a-zA-Z_][\w.]*)\s*:/g;
9275
+ let m;
9276
+ while ((m = re.exec(before)) !== null) {
9277
+ keys.add(m[1]);
9278
+ }
9279
+ return keys;
9280
+ }
9281
+
9282
+ function getProjectionTokenContext(fullText, cursorPos) {
9283
+ const before = fullText.slice(0, cursorPos);
9284
+ const depth = getBraceDepth(before, before.length);
9285
+
9286
+ if (depth > 0) {
9287
+ const m = before.match(/(?:\{|,)\s*([+-]?)([^\s:},]*)$/);
9288
+ if (!m) {
9289
+ return null;
9290
+ }
9291
+ const prefix = m[1] || '';
9292
+ const raw = m[2] || '';
9293
+ const term = raw.trim().toLowerCase();
9294
+ if (!term) {
9295
+ return null;
9296
+ }
9297
+ const token = `${prefix}${raw}`;
9298
+ const start = cursorPos - token.length;
9299
+ const selectedPaths = collectObjectKeysBefore(before, before.length);
9300
+ if (raw) {
9301
+ selectedPaths.delete(raw.trim());
9302
+ }
9303
+ return { mode: 'object', prefix, term, start, selectedPaths };
9304
+ }
9305
+
9306
+ const tokenMatch = before.match(/(?:^|[,\s])([+-]?)([^\s,]*)$/);
9307
+ if (!tokenMatch) {
9308
+ return null;
9309
+ }
9310
+ const prefix = tokenMatch[1] || '';
9311
+ const raw = tokenMatch[2] || '';
9312
+ const term = raw.trim().toLowerCase();
9313
+ if (!term) {
9314
+ return null;
9315
+ }
9316
+
9317
+ const token = `${prefix}${raw}`;
9318
+ const after = fullText.slice(cursorPos);
9319
+ const start = cursorPos - token.length;
9320
+ const normalizedText = `${fullText.slice(0, start)}${token}${after}`;
9321
+ const tokenCandidates = normalizedText.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
9322
+ const selectedPaths = new Set(tokenCandidates.map(t => t.replace(/^[+-]/, '')).filter(Boolean));
9323
+ if (raw) {
9324
+ selectedPaths.delete(raw);
9325
+ }
9326
+
9327
+ return { mode: 'list', prefix, term, start, selectedPaths };
9328
+ }
9329
+
9330
+ module.exports = app => app.component('projection-search', {
9331
+ template,
9332
+ props: {
9333
+ value: {
9334
+ type: String,
9335
+ default: ''
9336
+ },
9337
+ schemaPaths: {
9338
+ type: Array,
9339
+ default: () => []
9340
+ }
9341
+ },
9342
+ data() {
9343
+ return {
9344
+ projectionText: this.value || '',
9345
+ autocompleteSuggestions: [],
9346
+ autocompleteIndex: 0
9347
+ };
9348
+ },
9349
+ watch: {
9350
+ value(val) {
9351
+ const nextValue = val || '';
9352
+ if (nextValue === this.projectionText) {
9353
+ return;
9354
+ }
9355
+ this.projectionText = nextValue;
9356
+ this.clearAutocomplete();
9357
+ }
9358
+ },
9359
+ methods: {
9360
+ focusInput() {
9361
+ const input = this.$refs.projectionInput;
9362
+ if (input && typeof input.focus === 'function') {
9363
+ input.focus();
9364
+ }
9365
+ },
9366
+ emitInput() {
9367
+ this.$emit('projection-input', this.projectionText);
9368
+ },
9369
+ initProjection(ev) {
9370
+ if (!this.projectionText || !this.projectionText.trim()) {
9371
+ this.projectionText = '';
9372
+ this.emitInput();
9373
+ this.$nextTick(() => {
9374
+ if (ev && ev.target) {
9375
+ ev.target.setSelectionRange(0, 0);
9376
+ }
9377
+ });
9378
+ }
9379
+ },
9380
+ handleInput() {
9381
+ this.emitInput();
9382
+ this.updateAutocomplete();
9383
+ },
9384
+ updateAutocomplete() {
9385
+ const input = this.$refs.projectionInput;
9386
+ const cursorPos = input ? input.selectionStart : 0;
9387
+ const ctx = getProjectionTokenContext(this.projectionText, cursorPos);
9388
+ if (!ctx) {
9389
+ this.clearAutocomplete();
9390
+ return;
9391
+ }
9392
+
9393
+ const { prefix, term, selectedPaths } = ctx;
9394
+
9395
+ this.autocompleteSuggestions = this.schemaPaths
9396
+ .map(path => path.path)
9397
+ .filter(path => typeof path === 'string' && path.toLowerCase().includes(term) && !selectedPaths.has(path))
9398
+ .slice(0, 10)
9399
+ .map(path => (ctx.mode === 'object' ? path : `${prefix}${path}`));
9400
+ this.autocompleteIndex = 0;
9401
+ },
9402
+ handleKeyDown(ev) {
9403
+ if (this.autocompleteSuggestions.length > 0) {
9404
+ if (ev.key === 'Tab') {
9405
+ ev.preventDefault();
9406
+ this.applySuggestion(this.autocompleteIndex);
9407
+ return;
9408
+ }
9409
+ if (ev.key === 'Enter') {
9410
+ ev.preventDefault();
9411
+ this.applySuggestion(this.autocompleteIndex);
9412
+ // Enter should both accept autocomplete and apply projection.
9413
+ this.$nextTick(() => this.$emit('apply'));
9414
+ return;
9415
+ }
9416
+ if (ev.key === 'ArrowDown') {
9417
+ ev.preventDefault();
9418
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
9419
+ return;
9420
+ }
9421
+ if (ev.key === 'ArrowUp') {
9422
+ ev.preventDefault();
9423
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) %
9424
+ this.autocompleteSuggestions.length;
9425
+ return;
9426
+ }
9427
+ }
9428
+
9429
+ if (ev.key === 'Enter') {
9430
+ ev.preventDefault();
9431
+ this.$emit('apply');
9432
+ } else if (ev.key === 'Escape') {
9433
+ this.clearAutocomplete();
9434
+ }
9435
+ },
9436
+ applySuggestion(index) {
9437
+ const suggestion = this.autocompleteSuggestions[index];
9438
+ if (!suggestion) {
9439
+ return;
9440
+ }
9441
+ const input = this.$refs.projectionInput;
9442
+ if (!input) {
9443
+ return;
9444
+ }
9445
+
9446
+ const cursorPos = input.selectionStart;
9447
+ const after = this.projectionText.slice(cursorPos);
9448
+ const ctx = getProjectionTokenContext(this.projectionText, cursorPos);
9449
+ if (!ctx) {
9450
+ return;
9451
+ }
9452
+
9453
+ const { mode, start } = ctx;
9454
+ let replacement;
9455
+ if (mode === 'object') {
9456
+ const colon = /^\s*:/.test(after) ? '' : ': ';
9457
+ replacement = `${suggestion}${colon}`;
9458
+ } else {
9459
+ const needsSpace = after.length === 0 || !/^[,\s]/.test(after);
9460
+ replacement = needsSpace ? `${suggestion} ` : suggestion;
9461
+ }
9462
+
9463
+ this.projectionText = this.projectionText.slice(0, start) + replacement + after;
9464
+ this.emitInput();
9465
+ this.$nextTick(() => {
9466
+ const newCursorPos = start + replacement.length;
9467
+ input.setSelectionRange(newCursorPos, newCursorPos);
9468
+ });
9469
+ this.clearAutocomplete();
9470
+ },
9471
+ hideAutocomplete() {
9472
+ window.setTimeout(() => this.clearAutocomplete(), 100);
9473
+ },
9474
+ clearAutocomplete() {
9475
+ this.autocompleteSuggestions = [];
9476
+ this.autocompleteIndex = 0;
9477
+ }
9478
+ }
9479
+ });
9480
+
9481
+
8952
9482
  /***/ },
8953
9483
 
8954
9484
  /***/ "./frontend/src/models/trie.js"
@@ -10861,6 +11391,9 @@ var map = {
10861
11391
  "./detail-default/detail-default": "./frontend/src/detail-default/detail-default.js",
10862
11392
  "./detail-default/detail-default.html": "./frontend/src/detail-default/detail-default.html",
10863
11393
  "./detail-default/detail-default.js": "./frontend/src/detail-default/detail-default.js",
11394
+ "./detail-objectid/detail-objectid": "./frontend/src/detail-objectid/detail-objectid.js",
11395
+ "./detail-objectid/detail-objectid.html": "./frontend/src/detail-objectid/detail-objectid.html",
11396
+ "./detail-objectid/detail-objectid.js": "./frontend/src/detail-objectid/detail-objectid.js",
10864
11397
  "./document-details/date-view-mode-picker/date-view-mode-picker": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js",
10865
11398
  "./document-details/date-view-mode-picker/date-view-mode-picker.html": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html",
10866
11399
  "./document-details/date-view-mode-picker/date-view-mode-picker.js": "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js",
@@ -10872,6 +11405,9 @@ var map = {
10872
11405
  "./document-details/document-property/document-property.css": "./frontend/src/document-details/document-property/document-property.css",
10873
11406
  "./document-details/document-property/document-property.html": "./frontend/src/document-details/document-property/document-property.html",
10874
11407
  "./document-details/document-property/document-property.js": "./frontend/src/document-details/document-property/document-property.js",
11408
+ "./document-details/objectid-view-mode-picker/objectid-view-mode-picker": "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js",
11409
+ "./document-details/objectid-view-mode-picker/objectid-view-mode-picker.html": "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html",
11410
+ "./document-details/objectid-view-mode-picker/objectid-view-mode-picker.js": "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js",
10875
11411
  "./document/confirm-changes/confirm-changes": "./frontend/src/document/confirm-changes/confirm-changes.js",
10876
11412
  "./document/confirm-changes/confirm-changes.html": "./frontend/src/document/confirm-changes/confirm-changes.html",
10877
11413
  "./document/confirm-changes/confirm-changes.js": "./frontend/src/document/confirm-changes/confirm-changes.js",
@@ -10957,6 +11493,9 @@ var map = {
10957
11493
  "./models/models.css": "./frontend/src/models/models.css",
10958
11494
  "./models/models.html": "./frontend/src/models/models.html",
10959
11495
  "./models/models.js": "./frontend/src/models/models.js",
11496
+ "./models/projection-search/projection-search": "./frontend/src/models/projection-search/projection-search.js",
11497
+ "./models/projection-search/projection-search.html": "./frontend/src/models/projection-search/projection-search.html",
11498
+ "./models/projection-search/projection-search.js": "./frontend/src/models/projection-search/projection-search.js",
10960
11499
  "./models/trie": "./frontend/src/models/trie.js",
10961
11500
  "./models/trie.js": "./frontend/src/models/trie.js",
10962
11501
  "./mothership": "./frontend/src/mothership.js",
@@ -51208,6 +51747,17 @@ module.exports = "<div class=\"w-full\">\n <pre v-if=\"!isGeoJsonGeometry || !m
51208
51747
 
51209
51748
  /***/ },
51210
51749
 
51750
+ /***/ "./frontend/src/detail-objectid/detail-objectid.html"
51751
+ /*!***********************************************************!*\
51752
+ !*** ./frontend/src/detail-objectid/detail-objectid.html ***!
51753
+ \***********************************************************/
51754
+ (module) {
51755
+
51756
+ "use strict";
51757
+ module.exports = "<pre class=\"w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0\">{{displayValue}}</pre>\n";
51758
+
51759
+ /***/ },
51760
+
51211
51761
  /***/ "./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html"
51212
51762
  /*!****************************************************************************************!*\
51213
51763
  !*** ./frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html ***!
@@ -51259,7 +51809,18 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
51259
51809
  (module) {
51260
51810
 
51261
51811
  "use strict";
51262
- module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-muted': !highlight }\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <date-view-mode-picker\n v-if=\"isDatePath\"\n :viewMode=\"dateViewMode\"\n :path=\"path\"\n @update:viewMode=\"dateViewMode = $event\"\n ></date-view-mode-picker>\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :viewMode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : detailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : detailViewMode\"\n @updated=\"renderedValue = $event\"></component>\n </div>\n </div>\n </div>\n</div>\n";
51812
+ module.exports = "<div class=\"border border-edge bg-surface rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-edge transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-muted': !highlight }\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-content-tertiary mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-content\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-content-tertiary\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <date-view-mode-picker\n v-if=\"isDatePath\"\n :viewMode=\"dateViewMode\"\n :path=\"path\"\n @update:viewMode=\"dateViewMode = $event\"\n ></date-view-mode-picker>\n <objectid-view-mode-picker\n v-if=\"isObjectIdPath\"\n :viewMode=\"objectIdViewMode\"\n @update:viewMode=\"objectIdViewMode = $event\"\n ></objectid-view-mode-picker>\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-primary hover:bg-primary-hover text-primary-text px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :viewMode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-content-tertiary italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-content-secondary whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : (isObjectIdPath ? objectIdViewMode : detailViewMode)\"\n @updated=\"renderedValue = $event\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"rawValue\"\n :viewMode=\"isDatePath ? dateViewMode : (isObjectIdPath ? objectIdViewMode : detailViewMode)\"\n @updated=\"renderedValue = $event\"></component>\n </div>\n </div>\n </div>\n</div>\n";
51813
+
51814
+ /***/ },
51815
+
51816
+ /***/ "./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html"
51817
+ /*!************************************************************************************************!*\
51818
+ !*** ./frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html ***!
51819
+ \************************************************************************************************/
51820
+ (module) {
51821
+
51822
+ "use strict";
51823
+ module.exports = "<div class=\"flex items-center gap-2\" @click.stop>\n <select\n :value=\"format\"\n @input=\"onFormatChange($event.target.value)\"\n class=\"text-xs border border-edge rounded-md bg-surface px-2 py-1\"\n >\n <option value=\"hex\">Hex string</option>\n <option value=\"object_call\">ObjectId('...')</option>\n <option value=\"unix_seconds\">Unix timestamp</option>\n <option value=\"date\">Date</option>\n </select>\n</div>\n";
51263
51824
 
51264
51825
  /***/ },
51265
51826
 
@@ -51622,7 +52183,18 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
51622
52183
  (module) {
51623
52184
 
51624
52185
  "use strict";
51625
- 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 hover:bg-gray-200 text-content-secondary': 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=\"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=\"findOldestDocument\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Find oldest document\n </button>\n <button\n @click=\"toggleRowNumbers()\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}\n </button>\n <button\n @click=\"resetFilter()\"\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 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 </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 <input\n ref=\"projectionInput\"\n v-model=\"projectionText\"\n type=\"text\"\n placeholder=\"Projection: name email, or -password\"\n class=\"flex-1 min-w-0 rounded border border-edge bg-surface px-2 py-1.5 text-sm font-mono placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n @focus=\"initProjection($event)\"\n @keydown.enter=\"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 hover:bg-muted/60\"\n :class=\"{ 'bg-primary-subtle/50 hover:bg-primary-subtle/70': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\"\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 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 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=\"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 <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";
52186
+ 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 hover:bg-gray-200 text-content-secondary': 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=\"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=\"findOldestDocument\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n Find oldest document\n </button>\n <button\n @click=\"toggleRowNumbers()\"\n type=\"button\"\n class=\"block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted\"\n >\n {{ showRowNumbers ? 'Hide row numbers' : 'Show row numbers' }}\n </button>\n <button\n @click=\"resetFilter()\"\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 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 </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 hover:bg-muted/60\"\n :class=\"{ 'bg-primary-subtle/50 hover:bg-primary-subtle/70': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\"\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 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 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=\"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 <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";
52187
+
52188
+ /***/ },
52189
+
52190
+ /***/ "./frontend/src/models/projection-search/projection-search.html"
52191
+ /*!**********************************************************************!*\
52192
+ !*** ./frontend/src/models/projection-search/projection-search.html ***!
52193
+ \**********************************************************************/
52194
+ (module) {
52195
+
52196
+ "use strict";
52197
+ module.exports = "<div class=\"relative flex-grow m-0\">\n <input\n ref=\"projectionInput\"\n class=\"w-full min-w-0 rounded border border-edge bg-surface px-2 py-1.5 text-sm font-mono placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n type=\"text\"\n placeholder=\"Projection: name email, or -password\"\n v-model=\"projectionText\"\n @focus=\"initProjection\"\n @input=\"handleInput\"\n @keydown=\"handleKeyDown\"\n @blur=\"hideAutocomplete\"\n />\n <ul\n v-if=\"autocompleteSuggestions.length\"\n class=\"absolute left-0 right-0 top-full z-[9999] mt-1 max-h-40 overflow-y-auto rounded border border-edge bg-surface shadow\"\n >\n <li\n v-for=\"(suggestion, index) in autocompleteSuggestions\"\n :key=\"suggestion\"\n class=\"cursor-pointer px-2 py-1 text-sm font-mono\"\n :class=\"{ 'bg-primary-subtle': index === autocompleteIndex }\"\n @mousedown.prevent=\"applySuggestion(index)\"\n >\n {{ suggestion }}\n </li>\n </ul>\n</div>\n";
51626
52198
 
51627
52199
  /***/ },
51628
52200
 
@@ -57166,6 +57738,89 @@ var lexer = _Lexer.lex;
57166
57738
  //# sourceMappingURL=marked.cjs.map
57167
57739
 
57168
57740
 
57741
+ /***/ },
57742
+
57743
+ /***/ "./node_modules/mongoose/lib/helpers/symbols.js"
57744
+ /*!******************************************************!*\
57745
+ !*** ./node_modules/mongoose/lib/helpers/symbols.js ***!
57746
+ \******************************************************/
57747
+ (__unused_webpack_module, exports) {
57748
+
57749
+ "use strict";
57750
+
57751
+
57752
+ exports.arrayAtomicsBackupSymbol = Symbol('mongoose#Array#atomicsBackup');
57753
+ exports.arrayAtomicsSymbol = Symbol('mongoose#Array#_atomics');
57754
+ exports.arrayParentSymbol = Symbol('mongoose#Array#_parent');
57755
+ exports.arrayPathSymbol = Symbol('mongoose#Array#_path');
57756
+ exports.arraySchemaSymbol = Symbol('mongoose#Array#_schema');
57757
+ exports.documentArrayParent = Symbol('mongoose#documentArrayParent');
57758
+ exports.documentIsSelected = Symbol('mongoose#Document#isSelected');
57759
+ exports.documentIsModified = Symbol('mongoose#Document#isModified');
57760
+ exports.documentModifiedPaths = Symbol('mongoose#Document#modifiedPaths');
57761
+ exports.documentSchemaSymbol = Symbol('mongoose#Document#schema');
57762
+ exports.getSymbol = Symbol('mongoose#Document#get');
57763
+ exports.modelSymbol = Symbol('mongoose#Model');
57764
+ exports.objectIdSymbol = Symbol('mongoose#ObjectId');
57765
+ exports.populateModelSymbol = Symbol('mongoose#PopulateOptions#Model');
57766
+ exports.schemaTypeSymbol = Symbol('mongoose#schemaType');
57767
+ exports.sessionNewDocuments = Symbol('mongoose#ClientSession#newDocuments');
57768
+ exports.scopeSymbol = Symbol('mongoose#Document#scope');
57769
+ exports.validatorErrorSymbol = Symbol('mongoose#validatorError');
57770
+
57771
+
57772
+ /***/ },
57773
+
57774
+ /***/ "./node_modules/mongoose/lib/types/objectid.js"
57775
+ /*!*****************************************************!*\
57776
+ !*** ./node_modules/mongoose/lib/types/objectid.js ***!
57777
+ \*****************************************************/
57778
+ (module, __unused_webpack_exports, __webpack_require__) {
57779
+
57780
+ "use strict";
57781
+ /**
57782
+ * ObjectId type constructor
57783
+ *
57784
+ * #### Example:
57785
+ *
57786
+ * const id = new mongoose.Types.ObjectId;
57787
+ *
57788
+ * @constructor ObjectId
57789
+ */
57790
+
57791
+
57792
+
57793
+ const ObjectId = (__webpack_require__(/*! mongodb/lib/bson */ "./node_modules/mongodb/lib/bson.js").ObjectId);
57794
+ const objectIdSymbol = (__webpack_require__(/*! ../helpers/symbols */ "./node_modules/mongoose/lib/helpers/symbols.js").objectIdSymbol);
57795
+
57796
+ /**
57797
+ * Getter for convenience with populate, see gh-6115
57798
+ * @api private
57799
+ */
57800
+
57801
+ Object.defineProperty(ObjectId.prototype, '_id', {
57802
+ enumerable: false,
57803
+ configurable: true,
57804
+ get: function() {
57805
+ return this;
57806
+ }
57807
+ });
57808
+
57809
+ /*!
57810
+ * Convenience `valueOf()` to allow comparing ObjectIds using double equals re: gh-7299
57811
+ */
57812
+
57813
+ if (!Object.hasOwn(ObjectId.prototype, 'valueOf')) {
57814
+ ObjectId.prototype.valueOf = function objectIdValueOf() {
57815
+ return this.toString();
57816
+ };
57817
+ }
57818
+
57819
+ ObjectId.prototype[objectIdSymbol] = true;
57820
+
57821
+ module.exports = ObjectId;
57822
+
57823
+
57169
57824
  /***/ },
57170
57825
 
57171
57826
  /***/ "./node_modules/bson/lib/bson.mjs"
@@ -63035,7 +63690,7 @@ var src_default = VueToastificationPlugin;
63035
63690
  (module) {
63036
63691
 
63037
63692
  "use strict";
63038
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.4.0","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","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","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":{"mongoose":"7.x || 8.x || ^9.0.0"},"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","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"}}');
63693
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.4.1","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","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","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":{"mongoose":"7.x || 8.x || ^9.0.0"},"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","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"}}');
63039
63694
 
63040
63695
  /***/ }
63041
63696