@mongoosejs/studio 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -750,9 +750,12 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
750
750
  getDocuments: function getDocuments(params) {
751
751
  return client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
752
752
  },
753
+ getSuggestedProjection: function getSuggestedProjection(params) {
754
+ return client.post('', { action: 'Model.getSuggestedProjection', ...params }).then(res => res.data);
755
+ },
753
756
  getDocumentsStream: async function* getDocumentsStream(params) {
754
757
  const data = await client.post('', { action: 'Model.getDocuments', ...params }).then(res => res.data);
755
- yield { schemaPaths: data.schemaPaths };
758
+ yield { schemaPaths: data.schemaPaths, suggestedFields: data.suggestedFields };
756
759
  yield { numDocs: data.numDocs };
757
760
  for (const doc of data.docs) {
758
761
  yield { document: doc };
@@ -965,6 +968,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
965
968
  getDocuments: function getDocuments(params) {
966
969
  return client.post('/Model/getDocuments', params).then(res => res.data);
967
970
  },
971
+ getSuggestedProjection: function getSuggestedProjection(params) {
972
+ return client.post('/Model/getSuggestedProjection', params).then(res => res.data);
973
+ },
968
974
  getDocumentsStream: async function* getDocumentsStream(params) {
969
975
  const accessToken = window.localStorage.getItem('_mongooseStudioAccessToken') || null;
970
976
  const url = window.MONGOOSE_STUDIO_CONFIG.baseURL + '/Model/getDocumentsStream?' + new URLSearchParams(params).toString();
@@ -2732,7 +2738,7 @@ module.exports = {
2732
2738
  return this.dashboardResults.length > 0 ? this.dashboardResults[0] : null;
2733
2739
  }
2734
2740
  },
2735
- mounted: async function () {
2741
+ mounted: async function() {
2736
2742
  window.pageState = this;
2737
2743
 
2738
2744
  document.addEventListener('click', this.handleDocumentClick);
@@ -7056,11 +7062,66 @@ const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/append
7056
7062
  appendCSS(__webpack_require__(/*! ./models.css */ "./frontend/src/models/models.css"));
7057
7063
 
7058
7064
  const limit = 20;
7065
+ const DEFAULT_FIRST_N_FIELDS = 6;
7059
7066
  const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
7060
7067
  const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
7068
+ const PROJECTION_STORAGE_KEY_PREFIX = 'studio:model-projection:';
7069
+ const SHOW_ROW_NUMBERS_STORAGE_KEY = 'studio:model-show-row-numbers';
7070
+ const PROJECTION_MODE_QUERY_KEY = 'projectionMode';
7061
7071
  const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
7062
7072
  const MAX_RECENT_MODELS = 4;
7063
7073
 
7074
+ /** Parse `fields` from the route (JSON array or inclusion projection object only). */
7075
+ function parseFieldsQueryParam(fields) {
7076
+ if (fields == null || fields === '') {
7077
+ return [];
7078
+ }
7079
+ const s = typeof fields === 'string' ? fields : String(fields);
7080
+ const trimmed = s.trim();
7081
+ if (!trimmed) {
7082
+ return [];
7083
+ }
7084
+ let parsed;
7085
+ try {
7086
+ parsed = JSON.parse(trimmed);
7087
+ } catch (e) {
7088
+ return [];
7089
+ }
7090
+ if (Array.isArray(parsed)) {
7091
+ return parsed.map(x => String(x).trim()).filter(Boolean);
7092
+ }
7093
+ if (parsed != null && typeof parsed === 'object') {
7094
+ return Object.keys(parsed).filter(k =>
7095
+ Object.prototype.hasOwnProperty.call(parsed, k) && parsed[k]
7096
+ );
7097
+ }
7098
+ return [];
7099
+ }
7100
+
7101
+ /** Pass through a valid JSON `fields` string for Model.getDocuments / getDocumentsStream. */
7102
+ function normalizeFieldsParamForApi(fieldsStr) {
7103
+ if (fieldsStr == null || fieldsStr === '') {
7104
+ return null;
7105
+ }
7106
+ const s = typeof fieldsStr === 'string' ? fieldsStr : String(fieldsStr);
7107
+ const trimmed = s.trim();
7108
+ if (!trimmed) {
7109
+ return null;
7110
+ }
7111
+ try {
7112
+ const parsed = JSON.parse(trimmed);
7113
+ if (Array.isArray(parsed)) {
7114
+ return trimmed;
7115
+ }
7116
+ if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
7117
+ return trimmed;
7118
+ }
7119
+ } catch (e) {
7120
+ return null;
7121
+ }
7122
+ return null;
7123
+ }
7124
+
7064
7125
  module.exports = app => app.component('models', {
7065
7126
  template: template,
7066
7127
  props: ['model', 'user', 'roles'],
@@ -7076,6 +7137,7 @@ module.exports = app => app.component('models', {
7076
7137
  mongoDBIndexes: [],
7077
7138
  schemaIndexes: [],
7078
7139
  status: 'loading',
7140
+ loadingMore: false,
7079
7141
  loadedAllDocs: false,
7080
7142
  edittingDoc: null,
7081
7143
  docEdits: null,
@@ -7084,7 +7146,10 @@ module.exports = app => app.component('models', {
7084
7146
  searchText: '',
7085
7147
  shouldShowExportModal: false,
7086
7148
  shouldShowCreateModal: false,
7087
- shouldShowFieldModal: false,
7149
+ projectionText: '',
7150
+ isProjectionMenuSelected: false,
7151
+ addFieldFilterText: '',
7152
+ showAddFieldDropdown: false,
7088
7153
  shouldShowIndexModal: false,
7089
7154
  shouldShowCollectionInfoModal: false,
7090
7155
  shouldShowUpdateMultipleModal: false,
@@ -7106,27 +7171,44 @@ module.exports = app => app.component('models', {
7106
7171
  collectionInfo: null,
7107
7172
  modelSearch: '',
7108
7173
  recentlyViewedModels: [],
7109
- showModelSwitcher: false
7174
+ showModelSwitcher: false,
7175
+ showRowNumbers: true,
7176
+ suppressScrollCheck: false,
7177
+ scrollTopToRestore: null
7110
7178
  }),
7111
7179
  created() {
7112
7180
  this.currentModel = this.model;
7113
7181
  this.setSearchTextFromRoute();
7114
7182
  this.loadOutputPreference();
7115
7183
  this.loadSelectedGeoField();
7184
+ this.loadShowRowNumbersPreference();
7116
7185
  this.loadRecentlyViewedModels();
7186
+ this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
7117
7187
  },
7118
7188
  beforeDestroy() {
7119
- document.removeEventListener('scroll', this.onScroll, true);
7120
7189
  window.removeEventListener('popstate', this.onPopState, true);
7121
7190
  document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
7191
+ document.removeEventListener('click', this.onOutsideAddFieldDropdownClick, true);
7122
7192
  document.documentElement.removeEventListener('studio-theme-changed', this.onStudioThemeChanged);
7123
7193
  document.removeEventListener('keydown', this.onCtrlP, true);
7124
7194
  this.destroyMap();
7125
7195
  },
7126
7196
  async mounted() {
7197
+ // Persist scroll restoration across remounts.
7198
+ // This component is keyed by `$route.fullPath`, so query changes (e.g. projection updates)
7199
+ // recreate the component and reset scroll position.
7200
+ if (typeof window !== 'undefined') {
7201
+ if (typeof window.__studioModelsScrollTopToRestore === 'number') {
7202
+ this.scrollTopToRestore = window.__studioModelsScrollTopToRestore;
7203
+ }
7204
+ if (window.__studioModelsSuppressScrollCheck === true) {
7205
+ this.suppressScrollCheck = true;
7206
+ }
7207
+ delete window.__studioModelsScrollTopToRestore;
7208
+ delete window.__studioModelsSuppressScrollCheck;
7209
+ }
7210
+
7127
7211
  window.pageState = this;
7128
- this.onScroll = () => this.checkIfScrolledToBottom();
7129
- document.addEventListener('scroll', this.onScroll, true);
7130
7212
  this.onPopState = () => this.initSearchFromUrl();
7131
7213
  window.addEventListener('popstate', this.onPopState, true);
7132
7214
  this.onOutsideActionsMenuClick = event => {
@@ -7138,7 +7220,18 @@ module.exports = app => app.component('models', {
7138
7220
  this.closeActionsMenu();
7139
7221
  }
7140
7222
  };
7223
+ this.onOutsideAddFieldDropdownClick = event => {
7224
+ if (!this.showAddFieldDropdown) {
7225
+ return;
7226
+ }
7227
+ const container = this.$refs.addFieldContainer;
7228
+ if (container && !container.contains(event.target)) {
7229
+ this.showAddFieldDropdown = false;
7230
+ this.addFieldFilterText = '';
7231
+ }
7232
+ };
7141
7233
  document.addEventListener('click', this.onOutsideActionsMenuClick, true);
7234
+ document.addEventListener('click', this.onOutsideAddFieldDropdownClick, true);
7142
7235
  this.onStudioThemeChanged = () => this.updateMapTileLayer();
7143
7236
  document.documentElement.addEventListener('studio-theme-changed', this.onStudioThemeChanged);
7144
7237
  this.onCtrlP = (event) => {
@@ -7149,6 +7242,8 @@ module.exports = app => app.component('models', {
7149
7242
  };
7150
7243
  document.addEventListener('keydown', this.onCtrlP, true);
7151
7244
  this.query = Object.assign({}, this.$route.query);
7245
+ // Keep UI mode in sync with the URL on remounts.
7246
+ this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
7152
7247
  const { models, readyState } = await api.Model.listModels();
7153
7248
  this.models = models;
7154
7249
  await this.loadModelCounts();
@@ -7164,8 +7259,27 @@ module.exports = app => app.component('models', {
7164
7259
  }
7165
7260
 
7166
7261
  await this.initSearchFromUrl();
7262
+ if (this.isProjectionMenuSelected && this.outputType === 'map') {
7263
+ // Projection input is not rendered in map view.
7264
+ this.setOutputType('json');
7265
+ }
7266
+ this.$nextTick(() => {
7267
+ if (!this.isProjectionMenuSelected) return;
7268
+ const input = this.$refs.projectionInput;
7269
+ if (input && typeof input.focus === 'function') {
7270
+ input.focus();
7271
+ }
7272
+ });
7167
7273
  },
7168
7274
  watch: {
7275
+ model(newModel) {
7276
+ if (newModel !== this.currentModel) {
7277
+ this.currentModel = newModel;
7278
+ if (this.currentModel != null) {
7279
+ this.initSearchFromUrl();
7280
+ }
7281
+ }
7282
+ },
7169
7283
  documents: {
7170
7284
  handler() {
7171
7285
  if (this.outputType === 'map' && this.mapInstance) {
@@ -7270,6 +7384,19 @@ module.exports = app => app.component('models', {
7270
7384
  }
7271
7385
 
7272
7386
  return geoFields;
7387
+ },
7388
+ availablePathsToAdd() {
7389
+ const currentPaths = new Set(this.filteredPaths.map(p => p.path));
7390
+ return this.schemaPaths.filter(p => !currentPaths.has(p.path));
7391
+ },
7392
+ filteredPathsToAdd() {
7393
+ const available = this.availablePathsToAdd;
7394
+ const query = (this.addFieldFilterText || '').trim().toLowerCase();
7395
+ if (!query) return available;
7396
+ return available.filter(p => p.path.toLowerCase().includes(query));
7397
+ },
7398
+ tableDisplayPaths() {
7399
+ return this.filteredPaths.length > 0 ? this.filteredPaths : this.schemaPaths;
7273
7400
  }
7274
7401
  },
7275
7402
  methods: {
@@ -7334,6 +7461,24 @@ module.exports = app => app.component('models', {
7334
7461
  this.selectedGeoField = storedField;
7335
7462
  }
7336
7463
  },
7464
+ loadShowRowNumbersPreference() {
7465
+ if (typeof window === 'undefined' || !window.localStorage) {
7466
+ return;
7467
+ }
7468
+ const stored = window.localStorage.getItem(SHOW_ROW_NUMBERS_STORAGE_KEY);
7469
+ if (stored === '0') {
7470
+ this.showRowNumbers = false;
7471
+ } else if (stored === '1') {
7472
+ this.showRowNumbers = true;
7473
+ }
7474
+ },
7475
+ toggleRowNumbers() {
7476
+ this.showRowNumbers = !this.showRowNumbers;
7477
+ if (typeof window !== 'undefined' && window.localStorage) {
7478
+ window.localStorage.setItem(SHOW_ROW_NUMBERS_STORAGE_KEY, this.showRowNumbers ? '1' : '0');
7479
+ }
7480
+ this.showActionsMenu = false;
7481
+ },
7337
7482
  setOutputType(type) {
7338
7483
  if (type !== 'json' && type !== 'table' && type !== 'map') {
7339
7484
  return;
@@ -7529,6 +7674,22 @@ module.exports = app => app.component('models', {
7529
7674
  params.searchText = this.searchText;
7530
7675
  }
7531
7676
 
7677
+ // Prefer explicit URL projection (`query.fields`) so the first fetch after
7678
+ // mount/remount respects deep-linked projections before `filteredPaths`
7679
+ // is rehydrated from schema paths.
7680
+ let fieldsParam = normalizeFieldsParamForApi(this.query?.fields);
7681
+ if (!fieldsParam) {
7682
+ const fieldPaths = this.filteredPaths && this.filteredPaths.length > 0
7683
+ ? this.filteredPaths.map(p => p.path).filter(Boolean)
7684
+ : null;
7685
+ if (fieldPaths && fieldPaths.length > 0) {
7686
+ fieldsParam = JSON.stringify(fieldPaths);
7687
+ }
7688
+ }
7689
+ if (fieldsParam) {
7690
+ params.fields = fieldsParam;
7691
+ }
7692
+
7532
7693
  return params;
7533
7694
  },
7534
7695
  setSearchTextFromRoute() {
@@ -7542,20 +7703,35 @@ module.exports = app => app.component('models', {
7542
7703
  this.status = 'loading';
7543
7704
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
7544
7705
  this.setSearchTextFromRoute();
7545
- if (this.$route.query?.sort) {
7546
- const sort = eval(`(${this.$route.query.sort})`);
7547
- const path = Object.keys(sort)[0];
7548
- const num = Object.values(sort)[0];
7549
- this.sortDocs(num, path);
7706
+ // Avoid eval() on user-controlled query params.
7707
+ // Use explicit sortKey + sortDirection query params.
7708
+ const sortKey = this.$route.query?.sortKey;
7709
+ const sortDirectionRaw = this.$route.query?.sortDirection;
7710
+ const sortDirection = typeof sortDirectionRaw === 'string' ? Number(sortDirectionRaw) : sortDirectionRaw;
7711
+
7712
+ if (typeof sortKey === 'string' && sortKey.trim().length > 0 &&
7713
+ (sortDirection === 1 || sortDirection === -1)) {
7714
+ for (const key in this.sortBy) {
7715
+ delete this.sortBy[key];
7716
+ }
7717
+ this.sortBy[sortKey] = sortDirection;
7718
+ // Normalize to new params and remove legacy key if present.
7719
+ this.query.sortKey = sortKey;
7720
+ this.query.sortDirection = sortDirection;
7721
+ delete this.query.sort;
7550
7722
  }
7551
-
7552
-
7553
7723
  if (this.currentModel != null) {
7554
7724
  await this.getDocuments();
7555
7725
  }
7556
7726
  if (this.$route.query?.fields) {
7557
- const filter = this.$route.query.fields.split(',');
7558
- this.filteredPaths = this.filteredPaths.filter(x => filter.includes(x.path));
7727
+ const urlPaths = parseFieldsQueryParam(this.$route.query.fields);
7728
+ if (urlPaths.length > 0) {
7729
+ this.filteredPaths = urlPaths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
7730
+ if (this.filteredPaths.length > 0) {
7731
+ this.syncProjectionFromPaths();
7732
+ this.saveProjectionPreference();
7733
+ }
7734
+ }
7559
7735
  }
7560
7736
  this.status = 'loaded';
7561
7737
 
@@ -7575,10 +7751,8 @@ module.exports = app => app.component('models', {
7575
7751
  this.shouldShowCreateModal = false;
7576
7752
  await this.getDocuments();
7577
7753
  },
7578
- initializeDocumentData() {
7579
- this.shouldShowCreateModal = true;
7580
- },
7581
7754
  filterDocument(doc) {
7755
+ if (this.filteredPaths.length === 0) return doc;
7582
7756
  const filteredDoc = {};
7583
7757
  for (let i = 0; i < this.filteredPaths.length; i++) {
7584
7758
  const path = this.filteredPaths[i].path;
@@ -7600,23 +7774,47 @@ module.exports = app => app.component('models', {
7600
7774
  if (this.status === 'loading' || this.loadedAllDocs) {
7601
7775
  return;
7602
7776
  }
7603
- const container = this.$refs.documentsList;
7604
- if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
7605
- this.status = 'loading';
7606
- const params = this.buildDocumentFetchParams({ skip: this.documents.length });
7777
+ // Infinite scroll only applies to table/json views.
7778
+ if (this.outputType !== 'table' && this.outputType !== 'json') {
7779
+ return;
7780
+ }
7781
+ if (this.documents.length === 0) {
7782
+ return;
7783
+ }
7784
+ const container = this.outputType === 'table'
7785
+ ? this.$refs.documentsScrollContainer
7786
+ : this.$refs.documentsContainerScroll;
7787
+ if (!container || container.scrollHeight <= 0) {
7788
+ return;
7789
+ }
7790
+ const threshold = 150;
7791
+ const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - threshold;
7792
+ if (!nearBottom) {
7793
+ return;
7794
+ }
7795
+ this.loadingMore = true;
7796
+ this.status = 'loading';
7797
+ try {
7798
+ const skip = this.documents.length;
7799
+ const params = this.buildDocumentFetchParams({ skip });
7607
7800
  const { docs } = await api.Model.getDocuments(params);
7608
7801
  if (docs.length < limit) {
7609
7802
  this.loadedAllDocs = true;
7610
7803
  }
7611
7804
  this.documents.push(...docs);
7805
+ } finally {
7806
+ this.loadingMore = false;
7612
7807
  this.status = 'loaded';
7613
7808
  }
7809
+ this.$nextTick(() => this.checkIfScrolledToBottom());
7614
7810
  },
7615
7811
  async sortDocs(num, path) {
7616
7812
  let sorted = false;
7617
7813
  if (this.sortBy[path] == num) {
7618
7814
  sorted = true;
7619
7815
  delete this.query.sort;
7816
+ delete this.query.sortKey;
7817
+ delete this.query.sortDirection;
7620
7818
  this.$router.push({ query: this.query });
7621
7819
  }
7622
7820
  for (const key in this.sortBy) {
@@ -7624,9 +7822,13 @@ module.exports = app => app.component('models', {
7624
7822
  }
7625
7823
  if (!sorted) {
7626
7824
  this.sortBy[path] = num;
7627
- this.query.sort = `{${path}:${num}}`;
7825
+ this.query.sortKey = path;
7826
+ this.query.sortDirection = num;
7827
+ delete this.query.sort;
7628
7828
  this.$router.push({ query: this.query });
7629
7829
  }
7830
+ this.documents = [];
7831
+ this.loadedAllDocs = false;
7630
7832
  await this.loadMoreDocuments();
7631
7833
  },
7632
7834
  async search(searchText) {
@@ -7663,6 +7865,23 @@ module.exports = app => app.component('models', {
7663
7865
  closeActionsMenu() {
7664
7866
  this.showActionsMenu = false;
7665
7867
  },
7868
+ toggleProjectionMenu() {
7869
+ const next = !this.isProjectionMenuSelected;
7870
+ this.isProjectionMenuSelected = next;
7871
+
7872
+ // Because the route-view is keyed on `$route.fullPath`, query changes remount this component.
7873
+ // Persist projection UI state in the URL so Reset/Suggest don't turn the mode off.
7874
+ if (next) {
7875
+ this.query[PROJECTION_MODE_QUERY_KEY] = '1';
7876
+ if (this.outputType === 'map') {
7877
+ this.setOutputType('json');
7878
+ }
7879
+ } else {
7880
+ delete this.query[PROJECTION_MODE_QUERY_KEY];
7881
+ }
7882
+
7883
+ this.$router.push({ query: this.query });
7884
+ },
7666
7885
  async openCollectionInfo() {
7667
7886
  this.closeActionsMenu();
7668
7887
  this.shouldShowCollectionInfoModal = true;
@@ -7763,160 +7982,401 @@ module.exports = app => app.component('models', {
7763
7982
  }
7764
7983
  return formatValue(value / 1000000000, 'B');
7765
7984
  },
7766
- checkIndexLocation(indexName) {
7767
- if (this.schemaIndexes.find(x => x.name == indexName) && this.mongoDBIndexes.find(x => x.name == indexName)) {
7768
- return 'text-gray-500';
7769
- } else if (this.schemaIndexes.find(x => x.name == indexName)) {
7770
- return 'text-forest-green-500';
7771
- } else {
7772
- return 'text-valencia-500';
7773
- }
7774
- },
7775
7985
  async getDocuments() {
7776
- // Track recently viewed model
7777
- this.trackRecentModel(this.currentModel);
7778
-
7779
- // Clear previous data
7780
- this.documents = [];
7781
- this.schemaPaths = [];
7782
- this.numDocuments = null;
7783
- this.loadedAllDocs = false;
7784
- this.lastSelectedIndex = null;
7986
+ this.loadingMore = false;
7987
+ this.status = 'loading';
7988
+ try {
7989
+ // Track recently viewed model
7990
+ this.trackRecentModel(this.currentModel);
7991
+
7992
+ // Clear previous data
7993
+ this.documents = [];
7994
+ this.schemaPaths = [];
7995
+ this.numDocuments = null;
7996
+ this.loadedAllDocs = false;
7997
+ this.lastSelectedIndex = null;
7785
7998
 
7786
- let docsCount = 0;
7787
- let schemaPathsReceived = false;
7999
+ let docsCount = 0;
8000
+ let schemaPathsReceived = false;
7788
8001
 
7789
- // Use async generator to stream SSEs
7790
- const params = this.buildDocumentFetchParams();
7791
- for await (const event of api.Model.getDocumentsStream(params)) {
7792
- if (event.schemaPaths && !schemaPathsReceived) {
8002
+ // Use async generator to stream SSEs
8003
+ const params = this.buildDocumentFetchParams();
8004
+ for await (const event of api.Model.getDocumentsStream(params)) {
8005
+ if (event.schemaPaths && !schemaPathsReceived) {
7793
8006
  // Sort schemaPaths with _id first
7794
- this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
7795
- if (k1 === '_id' && k2 !== '_id') {
7796
- return -1;
7797
- }
7798
- if (k1 !== '_id' && k2 === '_id') {
7799
- return 1;
8007
+ this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
8008
+ if (k1 === '_id' && k2 !== '_id') {
8009
+ return -1;
8010
+ }
8011
+ if (k1 !== '_id' && k2 === '_id') {
8012
+ return 1;
8013
+ }
8014
+ return 0;
8015
+ }).map(key => event.schemaPaths[key]);
8016
+ this.shouldExport = {};
8017
+ for (const { path } of this.schemaPaths) {
8018
+ this.shouldExport[path] = true;
8019
+ }
8020
+ const shouldUseSavedProjection = this.isProjectionMenuSelected === true;
8021
+ const savedPaths = shouldUseSavedProjection ? this.loadProjectionPreference() : null;
8022
+ if (savedPaths === null) {
8023
+ this.applyDefaultProjection(event.suggestedFields);
8024
+ if (shouldUseSavedProjection) {
8025
+ this.saveProjectionPreference();
8026
+ }
8027
+ } else if (Array.isArray(savedPaths) && savedPaths.length === 0) {
8028
+ this.filteredPaths = [];
8029
+ this.projectionText = '';
8030
+ if (shouldUseSavedProjection) {
8031
+ this.saveProjectionPreference();
8032
+ }
8033
+ } else if (savedPaths && savedPaths.length > 0) {
8034
+ this.filteredPaths = savedPaths
8035
+ .map(path => this.schemaPaths.find(p => p.path === path))
8036
+ .filter(Boolean);
8037
+ if (this.filteredPaths.length === 0) {
8038
+ this.applyDefaultProjection(event.suggestedFields);
8039
+ if (shouldUseSavedProjection) {
8040
+ this.saveProjectionPreference();
8041
+ }
8042
+ }
8043
+ } else {
8044
+ this.applyDefaultProjection(event.suggestedFields);
8045
+ if (shouldUseSavedProjection) {
8046
+ this.saveProjectionPreference();
8047
+ }
7800
8048
  }
7801
- return 0;
7802
- }).map(key => event.schemaPaths[key]);
7803
- this.shouldExport = {};
7804
- for (const { path } of this.schemaPaths) {
7805
- this.shouldExport[path] = true;
8049
+ this.selectedPaths = [...this.filteredPaths];
8050
+ this.syncProjectionFromPaths();
8051
+ schemaPathsReceived = true;
8052
+ }
8053
+ if (event.numDocs !== undefined) {
8054
+ this.numDocuments = event.numDocs;
8055
+ }
8056
+ if (event.document) {
8057
+ this.documents.push(event.document);
8058
+ docsCount++;
8059
+ }
8060
+ if (event.message) {
8061
+ throw new Error(event.message);
7806
8062
  }
7807
- this.filteredPaths = [...this.schemaPaths];
7808
- this.selectedPaths = [...this.schemaPaths];
7809
- schemaPathsReceived = true;
7810
- }
7811
- if (event.numDocs !== undefined) {
7812
- this.numDocuments = event.numDocs;
7813
- }
7814
- if (event.document) {
7815
- this.documents.push(event.document);
7816
- docsCount++;
7817
- }
7818
- if (event.message) {
7819
- this.status = 'loaded';
7820
- throw new Error(event.message);
7821
8063
  }
7822
- }
7823
8064
 
7824
- if (docsCount < limit) {
7825
- this.loadedAllDocs = true;
8065
+ if (docsCount < limit) {
8066
+ this.loadedAllDocs = true;
8067
+ }
8068
+ } finally {
8069
+ this.status = 'loaded';
7826
8070
  }
8071
+ this.$nextTick(() => {
8072
+ this.restoreScrollPosition();
8073
+ if (!this.suppressScrollCheck) {
8074
+ this.checkIfScrolledToBottom();
8075
+ }
8076
+ this.suppressScrollCheck = false;
8077
+ });
7827
8078
  },
7828
8079
  async loadMoreDocuments() {
7829
- let docsCount = 0;
7830
- let numDocsReceived = false;
8080
+ const isLoadingMore = this.documents.length > 0;
8081
+ if (isLoadingMore) {
8082
+ this.loadingMore = true;
8083
+ }
8084
+ this.status = 'loading';
8085
+ try {
8086
+ let docsCount = 0;
8087
+ let numDocsReceived = false;
7831
8088
 
7832
- // Use async generator to stream SSEs
7833
- const params = this.buildDocumentFetchParams({ skip: this.documents.length });
7834
- for await (const event of api.Model.getDocumentsStream(params)) {
7835
- if (event.numDocs !== undefined && !numDocsReceived) {
7836
- this.numDocuments = event.numDocs;
7837
- numDocsReceived = true;
8089
+ // Use async generator to stream SSEs
8090
+ const params = this.buildDocumentFetchParams({ skip: this.documents.length });
8091
+ for await (const event of api.Model.getDocumentsStream(params)) {
8092
+ if (event.numDocs !== undefined && !numDocsReceived) {
8093
+ this.numDocuments = event.numDocs;
8094
+ numDocsReceived = true;
8095
+ }
8096
+ if (event.document) {
8097
+ this.documents.push(event.document);
8098
+ docsCount++;
8099
+ }
8100
+ if (event.message) {
8101
+ throw new Error(event.message);
8102
+ }
7838
8103
  }
7839
- if (event.document) {
7840
- this.documents.push(event.document);
7841
- docsCount++;
8104
+
8105
+ if (docsCount < limit) {
8106
+ this.loadedAllDocs = true;
7842
8107
  }
7843
- if (event.message) {
7844
- this.status = 'loaded';
7845
- throw new Error(event.message);
8108
+ } finally {
8109
+ this.loadingMore = false;
8110
+ this.status = 'loaded';
8111
+ }
8112
+ this.$nextTick(() => this.checkIfScrolledToBottom());
8113
+ },
8114
+ applyDefaultProjection(suggestedFields) {
8115
+ if (Array.isArray(suggestedFields) && suggestedFields.length > 0) {
8116
+ this.filteredPaths = suggestedFields
8117
+ .map(path => this.schemaPaths.find(p => p.path === path))
8118
+ .filter(Boolean);
8119
+ }
8120
+ if (!this.filteredPaths || this.filteredPaths.length === 0) {
8121
+ this.filteredPaths = this.schemaPaths.slice(0, DEFAULT_FIRST_N_FIELDS);
8122
+ }
8123
+ if (this.filteredPaths.length === 0) {
8124
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8125
+ }
8126
+ },
8127
+ loadProjectionPreference() {
8128
+ if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
8129
+ return null;
8130
+ }
8131
+ const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
8132
+ const stored = window.localStorage.getItem(key);
8133
+ if (stored === null || stored === undefined) {
8134
+ return null;
8135
+ }
8136
+ if (stored === '') {
8137
+ return [];
8138
+ }
8139
+ try {
8140
+ const parsed = JSON.parse(stored);
8141
+ if (Array.isArray(parsed)) {
8142
+ return parsed.map(x => String(x).trim()).filter(Boolean);
7846
8143
  }
8144
+ } catch (e) {
8145
+ return null;
7847
8146
  }
7848
-
7849
- if (docsCount < limit) {
7850
- this.loadedAllDocs = true;
8147
+ return null;
8148
+ },
8149
+ saveProjectionPreference() {
8150
+ if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
8151
+ return;
7851
8152
  }
8153
+ const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
8154
+ const paths = this.filteredPaths.map(p => p.path);
8155
+ window.localStorage.setItem(key, JSON.stringify(paths));
7852
8156
  },
7853
- addOrRemove(path) {
7854
- const exists = this.selectedPaths.findIndex(x => x.path == path.path);
7855
- if (exists > 0) { // remove
7856
- this.selectedPaths.splice(exists, 1);
7857
- } else { // add
7858
- this.selectedPaths.push(path);
7859
- this.selectedPaths = Object.keys(this.selectedPaths).sort((k1, k2) => {
7860
- if (k1 === '_id' && k2 !== '_id') {
7861
- return -1;
7862
- }
7863
- if (k1 !== '_id' && k2 === '_id') {
7864
- return 1;
8157
+ clearProjection() {
8158
+ // Keep current filter input in sync with the URL so projection reset
8159
+ // does not unintentionally wipe the filter on remount.
8160
+ this.syncFilterToQuery();
8161
+ this.filteredPaths = [];
8162
+ this.selectedPaths = [];
8163
+ this.projectionText = '';
8164
+ this.updateProjectionQuery();
8165
+ this.saveProjectionPreference();
8166
+ },
8167
+ resetFilter() {
8168
+ // Reuse the existing "apply filter + update URL" flow.
8169
+ this.search('');
8170
+ },
8171
+ syncFilterToQuery() {
8172
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
8173
+ this.query.search = this.searchText;
8174
+ } else {
8175
+ delete this.query.search;
8176
+ }
8177
+ },
8178
+ applyDefaultProjectionColumns() {
8179
+ if (!this.schemaPaths || this.schemaPaths.length === 0) return;
8180
+ const pathNames = this.schemaPaths.map(p => p.path);
8181
+ this.applyDefaultProjection(pathNames.slice(0, DEFAULT_FIRST_N_FIELDS));
8182
+ this.selectedPaths = [...this.filteredPaths];
8183
+ this.syncProjectionFromPaths();
8184
+ this.updateProjectionQuery();
8185
+ this.saveProjectionPreference();
8186
+ },
8187
+ initProjection(ev) {
8188
+ if (!this.projectionText || !this.projectionText.trim()) {
8189
+ this.projectionText = '';
8190
+ this.$nextTick(() => {
8191
+ if (ev && ev.target) {
8192
+ ev.target.setSelectionRange(0, 0);
7865
8193
  }
7866
- return 0;
7867
- }).map(key => this.selectedPaths[key]);
8194
+ });
7868
8195
  }
7869
8196
  },
7870
- openFieldSelection() {
7871
- if (this.$route.query?.fields) {
7872
- this.selectedPaths.length = 0;
7873
- console.log('there are fields in play', this.$route.query.fields);
7874
- const fields = this.$route.query.fields.split(',');
7875
- for (let i = 0; i < fields.length; i++) {
7876
- this.selectedPaths.push({ path: fields[i] });
8197
+ syncProjectionFromPaths() {
8198
+ if (this.filteredPaths.length === 0) {
8199
+ this.projectionText = '';
8200
+ return;
8201
+ }
8202
+ // String-only projection syntax: `field1 field2` and `-field` for exclusions.
8203
+ // Since `filteredPaths` represents the final include set, we serialize as space-separated fields.
8204
+ this.projectionText = this.filteredPaths.map(p => p.path).join(' ');
8205
+ },
8206
+ parseProjectionInput(text) {
8207
+ if (!text || typeof text !== 'string') {
8208
+ return [];
8209
+ }
8210
+ const trimmed = text.trim();
8211
+ if (!trimmed) {
8212
+ return [];
8213
+ }
8214
+ const normalizeKey = (key) => String(key).trim();
8215
+
8216
+ // String-only projection syntax:
8217
+ // name email
8218
+ // -password (exclusion-only)
8219
+ // +email (inclusion-only)
8220
+ //
8221
+ // Brace/object syntax is intentionally NOT supported.
8222
+ if (trimmed.startsWith('{') || trimmed.endsWith('}')) {
8223
+ return null;
8224
+ }
8225
+
8226
+ const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
8227
+ if (tokens.length === 0) return [];
8228
+
8229
+ const includeKeys = [];
8230
+ const excludeKeys = [];
8231
+
8232
+ for (const rawToken of tokens) {
8233
+ const token = rawToken.trim();
8234
+ if (!token) continue;
8235
+
8236
+ const prefix = token[0];
8237
+ if (prefix === '-') {
8238
+ const path = token.slice(1).trim();
8239
+ if (!path) return null;
8240
+ excludeKeys.push(path);
8241
+ } else if (prefix === '+') {
8242
+ const path = token.slice(1).trim();
8243
+ if (!path) return null;
8244
+ includeKeys.push(path);
8245
+ } else {
8246
+ includeKeys.push(token);
7877
8247
  }
7878
- } else {
7879
- this.selectedPaths = [{ path: '_id' }];
7880
8248
  }
7881
- this.shouldShowFieldModal = true;
8249
+
8250
+ if (includeKeys.length > 0 && excludeKeys.length > 0) {
8251
+ // Support subtractive edits on an existing projection string, e.g.
8252
+ // `name email createdAt -email` -> `name createdAt`.
8253
+ const includeSet = new Set(includeKeys.map(normalizeKey));
8254
+ for (const path of excludeKeys) {
8255
+ includeSet.delete(normalizeKey(path));
8256
+ }
8257
+ return Array.from(includeSet);
8258
+ }
8259
+
8260
+ if (excludeKeys.length > 0) {
8261
+ const excludeSet = new Set(excludeKeys.map(normalizeKey));
8262
+ return this.schemaPaths.map(p => p.path).filter(p => !excludeSet.has(p));
8263
+ }
8264
+
8265
+ return includeKeys.map(normalizeKey);
7882
8266
  },
7883
- filterDocuments() {
7884
- if (this.selectedPaths.length > 0) {
7885
- this.filteredPaths = [...this.selectedPaths];
8267
+ applyProjectionFromInput() {
8268
+ const paths = this.parseProjectionInput(this.projectionText);
8269
+ if (paths === null) {
8270
+ this.syncProjectionFromPaths();
8271
+ return;
8272
+ }
8273
+ if (paths.length === 0) {
8274
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8275
+ if (this.filteredPaths.length === 0 && this.schemaPaths.length > 0) {
8276
+ const idPath = this.schemaPaths.find(p => p.path === '_id');
8277
+ this.filteredPaths = idPath ? [idPath] : [this.schemaPaths[0]];
8278
+ }
7886
8279
  } else {
7887
- this.filteredPaths.length = 0;
8280
+ this.filteredPaths = paths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
8281
+ const validPaths = new Set(this.schemaPaths.map(p => p.path));
8282
+ for (const path of paths) {
8283
+ if (validPaths.has(path) && !this.filteredPaths.find(p => p.path === path)) {
8284
+ this.filteredPaths.push(this.schemaPaths.find(p => p.path === path));
8285
+ }
8286
+ }
8287
+ if (this.filteredPaths.length === 0) {
8288
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8289
+ }
7888
8290
  }
7889
- this.shouldShowFieldModal = false;
7890
- const selectedParams = this.filteredPaths.map(x => x.path).join(',');
7891
- this.query.fields = selectedParams;
7892
- this.$router.push({ query: this.query });
7893
- },
7894
- resetDocuments() {
7895
8291
  this.selectedPaths = [...this.filteredPaths];
7896
- this.query.fields = {};
8292
+ this.syncProjectionFromPaths();
8293
+ this.updateProjectionQuery();
8294
+ this.saveProjectionPreference();
8295
+ },
8296
+ updateProjectionQuery() {
8297
+ const paths = this.filteredPaths.map(x => x.path).filter(Boolean);
8298
+ if (paths.length > 0) {
8299
+ this.query.fields = JSON.stringify(paths);
8300
+ } else {
8301
+ delete this.query.fields;
8302
+ }
7897
8303
  this.$router.push({ query: this.query });
7898
- this.shouldShowFieldModal = false;
7899
8304
  },
7900
- deselectAll() {
7901
- this.selectedPaths = [];
8305
+ removeField(schemaPath) {
8306
+ if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
8307
+ this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
8308
+ this.suppressScrollCheck = true;
8309
+ // Persist for remount caused by query changes.
8310
+ if (typeof window !== 'undefined') {
8311
+ window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
8312
+ window.__studioModelsSuppressScrollCheck = true;
8313
+ }
8314
+ }
8315
+ const index = this.filteredPaths.findIndex(p => p.path === schemaPath.path);
8316
+ if (index !== -1) {
8317
+ this.filteredPaths.splice(index, 1);
8318
+ if (this.filteredPaths.length === 0) {
8319
+ const idPath = this.schemaPaths.find(p => p.path === '_id');
8320
+ this.filteredPaths = idPath ? [idPath] : [];
8321
+ }
8322
+ this.syncProjectionFromPaths();
8323
+ this.updateProjectionQuery();
8324
+ this.saveProjectionPreference();
8325
+ }
8326
+ },
8327
+ addField(schemaPath) {
8328
+ if (!this.filteredPaths.find(p => p.path === schemaPath.path)) {
8329
+ if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
8330
+ this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
8331
+ this.suppressScrollCheck = true;
8332
+ // Persist for remount caused by query changes.
8333
+ if (typeof window !== 'undefined') {
8334
+ window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
8335
+ window.__studioModelsSuppressScrollCheck = true;
8336
+ }
8337
+ }
8338
+ this.filteredPaths.push(schemaPath);
8339
+ this.filteredPaths.sort((a, b) => {
8340
+ if (a.path === '_id') return -1;
8341
+ if (b.path === '_id') return 1;
8342
+ return 0;
8343
+ });
8344
+ this.syncProjectionFromPaths();
8345
+ this.updateProjectionQuery();
8346
+ this.saveProjectionPreference();
8347
+ this.showAddFieldDropdown = false;
8348
+ this.addFieldFilterText = '';
8349
+ }
7902
8350
  },
7903
- selectAll() {
7904
- this.selectedPaths = [...this.schemaPaths];
8351
+ restoreScrollPosition() {
8352
+ if (this.outputType !== 'table') return;
8353
+ if (this.scrollTopToRestore == null) return;
8354
+ const container = this.$refs.documentsScrollContainer;
8355
+ if (!container) return;
8356
+ container.scrollTop = this.scrollTopToRestore;
8357
+ this.scrollTopToRestore = null;
7905
8358
  },
7906
- isSelected(path) {
7907
- return this.selectedPaths.find(x => x.path == path);
8359
+ toggleAddFieldDropdown() {
8360
+ this.showAddFieldDropdown = !this.showAddFieldDropdown;
8361
+ if (this.showAddFieldDropdown) {
8362
+ this.addFieldFilterText = '';
8363
+ this.$nextTick(() => this.$refs.addFieldFilterInput?.focus());
8364
+ }
7908
8365
  },
7909
8366
  getComponentForPath(schemaPath) {
8367
+ if (!schemaPath || typeof schemaPath !== 'object') {
8368
+ return 'list-mixed';
8369
+ }
7910
8370
  if (schemaPath.instance === 'Array') {
7911
8371
  return 'list-array';
7912
8372
  }
7913
8373
  if (schemaPath.instance === 'String') {
7914
8374
  return 'list-string';
7915
8375
  }
7916
- if (schemaPath.instance == 'Embedded') {
8376
+ if (schemaPath.instance === 'Embedded') {
7917
8377
  return 'list-subdocument';
7918
8378
  }
7919
- if (schemaPath.instance == 'Mixed') {
8379
+ if (schemaPath.instance === 'Mixed') {
7920
8380
  return 'list-mixed';
7921
8381
  }
7922
8382
  return 'list-default';
@@ -7941,6 +8401,31 @@ module.exports = app => app.component('models', {
7941
8401
  this.edittingDoc = null;
7942
8402
  this.$toast.success('Document updated!');
7943
8403
  },
8404
+ copyCellValue(value) {
8405
+ const text = value == null ? '' : (typeof value === 'object' ? JSON.stringify(value) : String(value));
8406
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
8407
+ navigator.clipboard.writeText(text).then(() => {
8408
+ this.$toast.success('Copied to clipboard');
8409
+ }).catch(() => {
8410
+ this.fallbackCopyText(text);
8411
+ });
8412
+ } else {
8413
+ this.fallbackCopyText(text);
8414
+ }
8415
+ },
8416
+ fallbackCopyText(text) {
8417
+ try {
8418
+ const el = document.createElement('textarea');
8419
+ el.value = text;
8420
+ document.body.appendChild(el);
8421
+ el.select();
8422
+ document.execCommand('copy');
8423
+ document.body.removeChild(el);
8424
+ this.$toast.success('Copied to clipboard');
8425
+ } catch (err) {
8426
+ this.$toast.error('Copy failed');
8427
+ }
8428
+ },
7944
8429
  handleDocumentClick(document, event) {
7945
8430
  if (this.selectMultiple) {
7946
8431
  this.handleDocumentSelection(document, event);
@@ -8308,7 +8793,7 @@ module.exports = app => app.component('navbar', {
8308
8793
  showFlyout: false,
8309
8794
  darkMode: typeof localStorage !== 'undefined' && localStorage.getItem('studio-theme') === 'dark'
8310
8795
  }),
8311
- mounted: function () {
8796
+ mounted: function() {
8312
8797
  window.navbar = this;
8313
8798
  const mobileMenuMask = document.querySelector('#mobile-menu-mask');
8314
8799
  const mobileMenu = document.querySelector('#mobile-menu');
@@ -9985,7 +10470,7 @@ __webpack_require__.r(__webpack_exports__);
9985
10470
  /* harmony export */ });
9986
10471
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
9987
10472
  /**
9988
- * @vue/reactivity v3.5.30
10473
+ * @vue/reactivity v3.5.31
9989
10474
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
9990
10475
  * @license MIT
9991
10476
  **/
@@ -11578,16 +12063,16 @@ function toRefs(object) {
11578
12063
  return ret;
11579
12064
  }
11580
12065
  class ObjectRefImpl {
11581
- constructor(_object, _key, _defaultValue) {
12066
+ constructor(_object, key, _defaultValue) {
11582
12067
  this._object = _object;
11583
- this._key = _key;
11584
12068
  this._defaultValue = _defaultValue;
11585
12069
  this["__v_isRef"] = true;
11586
12070
  this._value = void 0;
12071
+ this._key = (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isSymbol)(key) ? key : String(key);
11587
12072
  this._raw = toRaw(_object);
11588
12073
  let shallow = true;
11589
12074
  let obj = _object;
11590
- if (!(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isArray)(_object) || !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isIntegerKey)(String(_key))) {
12075
+ if (!(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isArray)(_object) || (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isSymbol)(this._key) || !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isIntegerKey)(this._key)) {
11591
12076
  do {
11592
12077
  shallow = !isProxy(obj) || isShallow(obj);
11593
12078
  } while (shallow && (obj = obj["__v_raw"]));
@@ -12132,7 +12617,7 @@ __webpack_require__.r(__webpack_exports__);
12132
12617
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
12133
12618
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
12134
12619
  /**
12135
- * @vue/runtime-core v3.5.30
12620
+ * @vue/runtime-core v3.5.31
12136
12621
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
12137
12622
  * @license MIT
12138
12623
  **/
@@ -12579,6 +13064,13 @@ function checkRecursiveUpdates(seen, fn) {
12579
13064
  }
12580
13065
 
12581
13066
  let isHmrUpdating = false;
13067
+ const setHmrUpdating = (v) => {
13068
+ try {
13069
+ return isHmrUpdating;
13070
+ } finally {
13071
+ isHmrUpdating = v;
13072
+ }
13073
+ };
12582
13074
  const hmrDirtyComponents = /* @__PURE__ */ new Map();
12583
13075
  if (true) {
12584
13076
  (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.getGlobalThis)().__VUE_HMR_RUNTIME__ = {
@@ -13165,9 +13657,10 @@ const TeleportImpl = {
13165
13657
  mount(container, mainAnchor);
13166
13658
  updateCssVars(n2, true);
13167
13659
  }
13168
- if (isTeleportDeferred(n2.props)) {
13660
+ if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) {
13169
13661
  n2.el.__isMounted = false;
13170
13662
  queuePostRenderEffect(() => {
13663
+ if (n2.el.__isMounted !== false) return;
13171
13664
  mountToTarget();
13172
13665
  delete n2.el.__isMounted;
13173
13666
  }, parentSuspense);
@@ -13175,7 +13668,12 @@ const TeleportImpl = {
13175
13668
  mountToTarget();
13176
13669
  }
13177
13670
  } else {
13178
- if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) {
13671
+ n2.el = n1.el;
13672
+ n2.targetStart = n1.targetStart;
13673
+ const mainAnchor = n2.anchor = n1.anchor;
13674
+ const target = n2.target = n1.target;
13675
+ const targetAnchor = n2.targetAnchor = n1.targetAnchor;
13676
+ if (n1.el.__isMounted === false) {
13179
13677
  queuePostRenderEffect(() => {
13180
13678
  TeleportImpl.process(
13181
13679
  n1,
@@ -13192,11 +13690,6 @@ const TeleportImpl = {
13192
13690
  }, parentSuspense);
13193
13691
  return;
13194
13692
  }
13195
- n2.el = n1.el;
13196
- n2.targetStart = n1.targetStart;
13197
- const mainAnchor = n2.anchor = n1.anchor;
13198
- const target = n2.target = n1.target;
13199
- const targetAnchor = n2.targetAnchor = n1.targetAnchor;
13200
13693
  const wasDisabled = isTeleportDisabled(n1.props);
13201
13694
  const currentContainer = wasDisabled ? container : target;
13202
13695
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor;
@@ -13661,7 +14154,7 @@ function resolveTransitionHooks(vnode, props, state, instance, postClone) {
13661
14154
  callHook(hook, [el]);
13662
14155
  },
13663
14156
  enter(el) {
13664
- if (leavingVNodesCache[key] === vnode) return;
14157
+ if (!isHmrUpdating && leavingVNodesCache[key] === vnode) return;
13665
14158
  let hook = onEnter;
13666
14159
  let afterHook = onAfterEnter;
13667
14160
  let cancelHook = onEnterCancelled;
@@ -16909,11 +17402,12 @@ function hasPropValueChanged(nextProps, prevProps, key) {
16909
17402
  }
16910
17403
  return nextProp !== prevProp;
16911
17404
  }
16912
- function updateHOCHostEl({ vnode, parent }, el) {
17405
+ function updateHOCHostEl({ vnode, parent, suspense }, el) {
16913
17406
  while (parent) {
16914
17407
  const root = parent.subTree;
16915
17408
  if (root.suspense && root.suspense.activeBranch === vnode) {
16916
- root.el = vnode.el;
17409
+ root.suspense.vnode.el = root.el = el;
17410
+ vnode = root;
16917
17411
  }
16918
17412
  if (root === vnode) {
16919
17413
  (vnode = parent.vnode).el = el;
@@ -16922,6 +17416,9 @@ function updateHOCHostEl({ vnode, parent }, el) {
16922
17416
  break;
16923
17417
  }
16924
17418
  }
17419
+ if (suspense && suspense.activeBranch === vnode) {
17420
+ suspense.vnode.el = el;
17421
+ }
16925
17422
  }
16926
17423
 
16927
17424
  const internalObjectProto = {};
@@ -17790,10 +18287,17 @@ function baseCreateRenderer(options, createHydrationFns) {
17790
18287
  }
17791
18288
  hostInsert(el, container, anchor);
17792
18289
  if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) {
18290
+ const isHmr = true && isHmrUpdating;
17793
18291
  queuePostRenderEffect(() => {
17794
- vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
17795
- needCallTransitionHooks && transition.enter(el);
17796
- dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
18292
+ let prev;
18293
+ if (true) prev = setHmrUpdating(isHmr);
18294
+ try {
18295
+ vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
18296
+ needCallTransitionHooks && transition.enter(el);
18297
+ dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
18298
+ } finally {
18299
+ if (true) setHmrUpdating(prev);
18300
+ }
17797
18301
  }, parentSuspense);
17798
18302
  }
17799
18303
  };
@@ -18713,7 +19217,8 @@ function baseCreateRenderer(options, createHydrationFns) {
18713
19217
  shapeFlag,
18714
19218
  patchFlag,
18715
19219
  dirs,
18716
- cacheIndex
19220
+ cacheIndex,
19221
+ memo
18717
19222
  } = vnode;
18718
19223
  if (patchFlag === -2) {
18719
19224
  optimized = false;
@@ -18775,10 +19280,14 @@ function baseCreateRenderer(options, createHydrationFns) {
18775
19280
  remove(vnode);
18776
19281
  }
18777
19282
  }
18778
- if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) {
19283
+ const shouldInvalidateMemo = memo != null && cacheIndex == null;
19284
+ if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs || shouldInvalidateMemo) {
18779
19285
  queuePostRenderEffect(() => {
18780
19286
  vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
18781
19287
  shouldInvokeDirs && invokeDirectiveHook(vnode, null, parentComponent, "unmounted");
19288
+ if (shouldInvalidateMemo) {
19289
+ vnode.el = null;
19290
+ }
18782
19291
  }, parentSuspense);
18783
19292
  }
18784
19293
  };
@@ -19332,6 +19841,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19332
19841
  pendingId: suspenseId++,
19333
19842
  timeout: typeof timeout === "number" ? timeout : -1,
19334
19843
  activeBranch: null,
19844
+ isFallbackMountPending: false,
19335
19845
  pendingBranch: null,
19336
19846
  isInFallback: !isHydrating,
19337
19847
  isHydrating,
@@ -19381,7 +19891,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19381
19891
  }
19382
19892
  };
19383
19893
  }
19384
- if (activeBranch) {
19894
+ if (activeBranch && !suspense.isFallbackMountPending) {
19385
19895
  if (parentNode(activeBranch.el) === container2) {
19386
19896
  anchor = next(activeBranch);
19387
19897
  }
@@ -19394,6 +19904,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19394
19904
  move(pendingBranch, container2, anchor, 0);
19395
19905
  }
19396
19906
  }
19907
+ suspense.isFallbackMountPending = false;
19397
19908
  setActiveBranch(suspense, pendingBranch);
19398
19909
  suspense.pendingBranch = null;
19399
19910
  suspense.isInFallback = false;
@@ -19429,6 +19940,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19429
19940
  triggerEvent(vnode2, "onFallback");
19430
19941
  const anchor2 = next(activeBranch);
19431
19942
  const mountFallback = () => {
19943
+ suspense.isFallbackMountPending = false;
19432
19944
  if (!suspense.isInFallback) {
19433
19945
  return;
19434
19946
  }
@@ -19448,6 +19960,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19448
19960
  };
19449
19961
  const delayEnter = fallbackVNode.transition && fallbackVNode.transition.mode === "out-in";
19450
19962
  if (delayEnter) {
19963
+ suspense.isFallbackMountPending = true;
19451
19964
  activeBranch.transition.afterLeave = mountFallback;
19452
19965
  }
19453
19966
  suspense.isInFallback = true;
@@ -19998,6 +20511,10 @@ function mergeProps(...args) {
19998
20511
  const incoming = toMerge[key];
19999
20512
  if (incoming && existing !== incoming && !((0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isArray)(existing) && existing.includes(incoming))) {
20000
20513
  ret[key] = existing ? [].concat(existing, incoming) : incoming;
20514
+ } else if (incoming == null && existing == null && // mergeProps({ 'onUpdate:modelValue': undefined }) should not retain
20515
+ // the model listener.
20516
+ !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isModelListener)(key)) {
20517
+ ret[key] = incoming;
20001
20518
  }
20002
20519
  } else if (key !== "") {
20003
20520
  ret[key] = toMerge[key];
@@ -20684,7 +21201,7 @@ function isMemoSame(cached, memo) {
20684
21201
  return true;
20685
21202
  }
20686
21203
 
20687
- const version = "3.5.30";
21204
+ const version = "3.5.31";
20688
21205
  const warn = true ? warn$1 : 0;
20689
21206
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
20690
21207
  const devtools = true ? devtools$1 : 0;
@@ -20895,7 +21412,7 @@ __webpack_require__.r(__webpack_exports__);
20895
21412
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-core */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
20896
21413
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
20897
21414
  /**
20898
- * @vue/runtime-dom v3.5.30
21415
+ * @vue/runtime-dom v3.5.31
20899
21416
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
20900
21417
  * @license MIT
20901
21418
  **/
@@ -22472,7 +22989,8 @@ const vModelText = {
22472
22989
  if (elValue === newValue) {
22473
22990
  return;
22474
22991
  }
22475
- if (document.activeElement === el && el.type !== "range") {
22992
+ const rootNode = el.getRootNode();
22993
+ if ((rootNode instanceof Document || rootNode instanceof ShadowRoot) && rootNode.activeElement === el && el.type !== "range") {
22476
22994
  if (lazy && value === oldValue) {
22477
22995
  return;
22478
22996
  }
@@ -22966,7 +23484,7 @@ __webpack_require__.r(__webpack_exports__);
22966
23484
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
22967
23485
  /* harmony export */ });
22968
23486
  /**
22969
- * @vue/shared v3.5.30
23487
+ * @vue/shared v3.5.31
22970
23488
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
22971
23489
  * @license MIT
22972
23490
  **/
@@ -48489,7 +49007,7 @@ __webpack_require__.r(__webpack_exports__);
48489
49007
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-core/dist/runtime-core.esm-bundler.js");
48490
49008
  /* harmony import */ var _vue_runtime_dom__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/runtime-dom */ "./node_modules/@vue/runtime-dom/dist/runtime-dom.esm-bundler.js");
48491
49009
  /**
48492
- * vue v3.5.30
49010
+ * vue v3.5.31
48493
49011
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
48494
49012
  * @license MIT
48495
49013
  **/
@@ -50160,7 +50678,7 @@ module.exports = ".list-mixed pre {\n max-height: 6.5em;\n max-width: 30em;\n}
50160
50678
  (module) {
50161
50679
 
50162
50680
  "use strict";
50163
- module.exports = "<div class=\"list-mixed tooltip\">\n <pre>\n <code ref=\"MixedCode\" class=\"language-javascript\">{{shortenValue}}</code>\n <span class=\"tooltiptext\" @click.stop=\"copyText(value)\">copy &#x1F4CB;</span>\n </pre>\n</div>\n ";
50681
+ module.exports = "<div class=\"list-mixed tooltip\">\n <pre>\n <code ref=\"MixedCode\" class=\"language-javascript\">{{shortenValue}}</code>\n </pre>\n</div>\n ";
50164
50682
 
50165
50683
  /***/ },
50166
50684
 
@@ -50182,7 +50700,7 @@ module.exports = ".list-string {\n display: inline;\n max-width: 300px;\n}";
50182
50700
  (module) {
50183
50701
 
50184
50702
  "use strict";
50185
- module.exports = "<div class=\"list-string tooltip\" ref=\"itemData\">\n {{displayValue}}\n <span class=\"tooltiptext\" @click.stop=\"copyText(value)\">copy &#x1F4CB;</span>\n</div>";
50703
+ module.exports = "<div class=\"list-string tooltip\" ref=\"itemData\">\n {{displayValue}}\n</div>";
50186
50704
 
50187
50705
  /***/ },
50188
50706
 
@@ -50259,7 +50777,7 @@ module.exports = "<div v-if=\"show\" class=\"fixed inset-0 z-[9999]\">\n <div c
50259
50777
  (module) {
50260
50778
 
50261
50779
  "use strict";
50262
- module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models button.gray {\n color: black;\n background-color: #eee;\n}\n\n.models .model-selector {\n flex-grow: 0;\n padding: 15px;\n padding-top: 0px;\n}\n\n.models h1 {\n margin-top: 0px;\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: scroll;\n max-height: calc(100vh - 56px);\n}\n\n.models .documents table {\n /* max-width: -moz-fit-content;\n max-width: fit-content; */\n width: 100%;\n table-layout: auto;\n font-size: small;\n padding: 0;\n margin-right: 1em;\n white-space: nowrap;\n z-index: -1;\n border-collapse: collapse;\n line-height: 1.5em;\n}\n\n.models .documents table th {\n position: sticky;\n top: 42px;\n z-index: 1;\n}\n\n.models .documents table th:after {\n content: \"\";\n position: absolute;\n left: 0;\n width: 100%;\n bottom: -1px;\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n}\n\n.models .documents table tr {\n color: black;\n border-spacing: 0px 0px;\n}\n\n.models .documents table th {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n height: 48px;\n}\n\n.models .documents table td {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n}\n\n.models textarea {\n font-size: 1.2em;\n}\n\n.models .path-type {\n color: rgba(0, 0, 0, 0.36);\n font-size: 0.8em;\n}\n\n.models .documents-menu {\n position: sticky;\n top: 0;\n z-index: 2;\n padding: 4px;\n display: flex;\n}\n\n.models .documents-menu .search-input {\n flex-grow: 1;\n align-items: center;\n}\n\n.models .search-input input {\n padding: 0.25em 0.5em;\n font-size: 1.1em;\n border: 1px solid #ddd;\n border-radius: 3px;\n width: calc(100% - 1em);\n}\n\n.models .sort-arrow {\n padding-left: 10px;\n padding-right: 10px;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .documents .buttons {\n display: inline-flex;\n justify-content: space-around;\n align-items: center;\n}\n";
50780
+ module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: visible;\n max-height: calc(100vh - 56px);\n display: flex;\n flex-direction: column;\n}\n\n.models .documents-container {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n}\n\n.models .documents-menu {\n position: sticky;\n top: 0;\n z-index: 30;\n padding: 4px;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .loader-overlay {\n position: absolute;\n inset: 0;\n z-index: 20;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--color-page);\n opacity: 0.9;\n}\n\n/* Table cell: copy icon only copies; rest of cell opens document */\n.models .table-cell-copy {\n cursor: pointer;\n}\n";
50263
50781
 
50264
50782
  /***/ },
50265
50783
 
@@ -50270,7 +50788,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
50270
50788
  (module) {
50271
50789
 
50272
50790
  "use strict";
50273
- 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=\"{ 'bg-page0 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-primary hover:bg-primary-hover' : !selectMultiple }\"\n class=\"rounded px-2 py-2 text-sm font-semibold text-primary-text 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-20\"\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=\"openFieldSelection(); 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 Projection\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 </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>\n <div class=\"documents-container relative\">\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 </div>\n </div>\n <table v-else-if=\"outputType === 'table'\">\n <thead class=\"bg-slate-50\">\n <th v-for=\"path in filteredPaths\" @click=\"addPathFilter(path.path)\" class=\"bg-slate-50 cursor-pointer p-3\">\n {{path.path}}\n <span class=\"path-type\">\n ({{(path.instance || 'unknown')}})\n </span>\n <span class=\"sort-arrow\" @click=\"sortDocs(1, path.path)\">{{sortBy[path.path] == 1 ? 'X' : '↑'}}</span>\n <span class=\"sort-arrow\" @click=\"sortDocs(-1, path.path)\">{{sortBy[path.path] == -1 ? 'X' : '↓'}}</span>\n </th>\n </thead>\n <tbody>\n <tr v-for=\"document in documents\" @click=\"handleDocumentClick(document, $event)\" :key=\"document._id\" class=\"bg-surface hover:bg-slate-50\">\n <td v-for=\"schemaPath in filteredPaths\" class=\"p-3 cursor-pointer\" :class=\"{ 'bg-blue-200': selectedDocuments.some(x => x._id.toString() === document._id.toString()) }\">\n <component\n :is=\"getComponentForPath(schemaPath)\"\n :value=\"getValueForPath(document, schemaPath.path)\"\n :allude=\"getReferenceModel(schemaPath)\">\n </component>\n </td>\n </tr>\n </tbody>\n </table>\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 <button\n type=\"button\"\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 @click.stop=\"openDocument(document)\"\n >\n Open this Document\n </button>\n <list-json :value=\"filterDocument(document)\" :references=\"referenceMap\">\n </list-json>\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'\" class=\"loader\">\n <img src=\"images/loader.gif\">\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=\"shouldShowFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"shouldShowFieldModal = false; selectedPaths = [...filteredPaths];\">&times;</div>\n <div v-for=\"(path, index) in schemaPaths\" :key=\"index\" class=\"w-5 flex items-center\">\n <input class=\"mt-0 h-4 w-4 rounded border-edge-strong text-sky-600 focus:ring-sky-600 accent-sky-600\" type=\"checkbox\" :id=\"'path.path'+index\" @change=\"addOrRemove(path)\" :value=\"path.path\" :checked=\"isSelected(path.path)\" />\n <div class=\"ml-2 text-content-secondary grow shrink text-left\">\n <label :for=\"'path.path' + index\">{{path.path}}</label>\n </div>\n </div>\n <div class=\"mt-4 flex gap-2\">\n <button type=\"button\" @click=\"filterDocuments()\" class=\"rounded-md bg-primary px-2.5 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">Filter Selection</button>\n <button type=\"button\" @click=\"selectAll()\" class=\"rounded-md bg-forest-green-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-green-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-green-600\">Select All</button>\n <button type=\"button\" @click=\"deselectAll()\" 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\">Deselect All</button>\n <button type=\"button\" @click=\"resetDocuments()\" class=\"rounded-md bg-gray-600 px-2.5 py-1.5 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-green-600\" >Cancel</button>\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";
50791
+ 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-page0 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\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 </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 <button\n type=\"button\"\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 @click.stop=\"openDocument(document)\"\n >\n Open this Document\n </button>\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";
50274
50792
 
50275
50793
  /***/ },
50276
50794
 
@@ -50314,7 +50832,7 @@ module.exports = "<div class=\"w-full h-full flex items-center justify-center\">
50314
50832
  (module) {
50315
50833
 
50316
50834
  "use strict";
50317
- module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'\">\n <img src=\"images/loader.gif\" alt=\"Loading\" />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"text-red-600\">\n {{ errorMessage }}\n </div>\n <template v-else-if=\"taskGroup\">\n <div class=\"pb-24\">\n <router-link :to=\"{ name: 'tasks' }\" class=\"inline-flex items-center gap-1 text-gray-500 hover:text-gray-700 mb-4\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Back to Task Groups\n </router-link>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Filter by Date:</label>\n <select v-model=\"selectedRange\" @change=\"updateDateRange\" class=\"border-gray-300 rounded-md shadow-sm w-full p-2 max-w-xs\">\n <option v-for=\"option in dateFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <task-details\n :task-group=\"taskGroup\"\n :back-to=\"{ name: 'tasks' }\"\n :show-back-button=\"false\"\n @task-created=\"onTaskCreated\"\n @task-cancelled=\"onTaskCancelled\"\n ></task-details>\n </div>\n <div\n v-if=\"numDocs > 0\"\n class=\"fixed bottom-0 left-0 right-0 z-10 px-4 py-4 bg-white border-t border-gray-200 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]\"\n >\n <div class=\"flex flex-wrap items-center justify-between gap-4 max-w-6xl mx-auto\">\n <div class=\"flex items-center gap-6\">\n <p class=\"text-sm text-gray-600\">\n <span class=\"font-medium text-gray-900\">{{ Math.min((page - 1) * pageSize + 1, numDocs) }}–{{ Math.min(page * pageSize, numDocs) }}</span>\n <span class=\"mx-1\">of</span>\n <span class=\"font-medium text-gray-900\">{{ numDocs }}</span>\n <span class=\"ml-1 text-gray-500\">tasks</span>\n </p>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-sm font-medium text-gray-700\">Per page</label>\n <select\n v-model.number=\"pageSize\"\n @change=\"onPageSizeChange\"\n class=\"border border-gray-300 rounded-md shadow-sm px-3 py-2 text-sm text-gray-700 bg-white hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-ultramarine-500 focus:border-ultramarine-500\"\n >\n <option v-for=\"n in pageSizeOptions\" :key=\"n\" :value=\"n\">{{ n }}</option>\n </select>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n :disabled=\"page <= 1\"\n @click=\"goToPage(page - 1)\"\n class=\"inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:border-gray-400\"\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=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Previous\n </button>\n <span class=\"px-4 py-2 text-sm text-gray-600 min-w-[7rem] text-center\">\n Page <span class=\"font-semibold text-gray-900\">{{ page }}</span> of <span class=\"font-semibold text-gray-900\">{{ Math.max(1, Math.ceil(numDocs / pageSize)) }}</span>\n </span>\n <button\n type=\"button\"\n :disabled=\"page >= Math.ceil(numDocs / pageSize)\"\n @click=\"goToPage(page + 1)\"\n class=\"inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white bg-white text-gray-700 border-gray-300 hover:bg-gray-50 hover:border-gray-400\"\n >\n Next\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=\"M9 5l7 7-7 7\"></path>\n </svg>\n </button>\n </div>\n </div>\n </div>\n </template>\n</div>\n";
50835
+ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'\">\n <img src=\"images/loader.gif\" alt=\"Loading\" />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"text-red-600\">\n {{ errorMessage }}\n </div>\n <template v-else-if=\"taskGroup\">\n <div class=\"pb-24\">\n <router-link :to=\"{ name: 'tasks' }\" class=\"inline-flex items-center gap-1 text-gray-500 hover:text-gray-700 mb-4\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Back to Task Groups\n </router-link>\n <div class=\"mb-4\">\n <label class=\"block text-sm font-medium text-gray-700 mb-1\">Filter by Date:</label>\n <select v-model=\"selectedRange\" @change=\"updateDateRange\" class=\"border-gray-300 rounded-md shadow-sm w-full p-2 max-w-xs\">\n <option v-for=\"option in dateFilters\" :key=\"option.value\" :value=\"option.value\">\n {{ option.label }}\n </option>\n </select>\n </div>\n <task-details\n :task-group=\"taskGroup\"\n :back-to=\"{ name: 'tasks' }\"\n :show-back-button=\"false\"\n @task-created=\"onTaskCreated\"\n @task-cancelled=\"onTaskCancelled\"\n ></task-details>\n </div>\n <div\n v-if=\"numDocs > 0\"\n class=\"fixed bottom-0 left-0 right-0 z-10 px-4 py-4 bg-surface border-t border-edge shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]\"\n >\n <div class=\"flex flex-wrap items-center justify-between gap-4 max-w-6xl mx-auto\">\n <div class=\"flex items-center gap-6\">\n <p class=\"text-sm text-content-tertiary\">\n <span class=\"font-medium text-content\">{{ Math.min((page - 1) * pageSize + 1, numDocs) }}–{{ Math.min(page * pageSize, numDocs) }}</span>\n <span class=\"mx-1\">of</span>\n <span class=\"font-medium text-content\">{{ numDocs }}</span>\n <span class=\"ml-1 text-content-tertiary\">tasks</span>\n </p>\n <div class=\"flex items-center gap-2\">\n <label class=\"text-sm font-medium text-content-secondary\">Per page</label>\n <select\n v-model.number=\"pageSize\"\n @change=\"onPageSizeChange\"\n class=\"border border-edge rounded-md shadow-sm px-3 py-2 text-sm text-content-secondary bg-surface hover:border-edge-strong focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary\"\n >\n <option v-for=\"n in pageSizeOptions\" :key=\"n\" :value=\"n\">{{ n }}</option>\n </select>\n </div>\n </div>\n <div class=\"flex items-center gap-1\">\n <button\n type=\"button\"\n :disabled=\"page <= 1\"\n @click=\"goToPage(page - 1)\"\n class=\"inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface bg-surface text-content-secondary border-edge hover:bg-page hover:border-edge-strong\"\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=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Previous\n </button>\n <span class=\"px-4 py-2 text-sm text-content-tertiary min-w-[7rem] text-center\">\n Page <span class=\"font-semibold text-content\">{{ page }}</span> of <span class=\"font-semibold text-content\">{{ Math.max(1, Math.ceil(numDocs / pageSize)) }}</span>\n </span>\n <button\n type=\"button\"\n :disabled=\"page >= Math.ceil(numDocs / pageSize)\"\n @click=\"goToPage(page + 1)\"\n class=\"inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md border transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface bg-surface text-content-secondary border-edge hover:bg-page hover:border-edge-strong\"\n >\n Next\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=\"M9 5l7 7-7 7\"></path>\n </svg>\n </button>\n </div>\n </div>\n </div>\n </template>\n</div>\n";
50318
50836
 
50319
50837
  /***/ },
50320
50838
 
@@ -50325,7 +50843,7 @@ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'
50325
50843
  (module) {
50326
50844
 
50327
50845
  "use strict";
50328
- module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'\">\n <img src=\"images/loader.gif\" alt=\"Loading\" />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"text-red-600\">\n {{ errorMessage }}\n </div>\n <div v-else-if=\"status === 'notfound'\" class=\"text-gray-600\">\n Task not found.\n </div>\n <div v-else-if=\"task\" class=\"max-w-4xl\">\n <button @click=\"goBack\" class=\"text-content-tertiary hover:text-content-secondary mb-4 flex items-center gap-1\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Back to {{ task.name }}\n </button>\n <h1 class=\"text-2xl font-bold text-content-secondary mb-1\">{{ task.name }}</h1>\n <p class=\"text-content-tertiary mb-6\">Task details</p>\n\n <div class=\"bg-surface rounded-lg shadow p-6 md:p-8\">\n <div class=\"flex items-center gap-3 mb-6\">\n <span class=\"text-sm font-medium text-content\">ID: {{ task.id }}</span>\n <span\n class=\"text-xs px-2 py-1 rounded-full font-medium\"\n :class=\"getStatusColor(task.status)\"\n >\n {{ task?.status }}\n </span>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6\">\n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.scheduledAt) }}</div>\n </div>\n <div v-if=\"task?.startedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Started At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.startedAt) }}</div>\n </div>\n <div v-if=\"task?.completedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Completed At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.completedAt) }}</div>\n </div>\n </div>\n\n <div v-if=\"task?.params\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Params</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.params\"></list-json>\n </div>\n </div>\n\n <div v-if=\"task?.result\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Result</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.result\"></list-json>\n </div>\n </div>\n\n <div v-if=\"task?.error\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Error</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.error\"></list-json>\n </div>\n </div>\n\n <div class=\"flex flex-wrap gap-3 pt-4 border-t border-edge\">\n <button\n @click=\"showRescheduleConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n Reschedule\n </button>\n <button\n @click=\"showRunConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M5 3l14 9-14 9V3z\"></path>\n </svg>\n Run Now\n </button>\n <button\n v-if=\"task.status === 'pending'\"\n @click=\"showCancelConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700\"\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=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n Cancel Task\n </button>\n </div>\n </div>\n </div>\n\n <!-- Reschedule Modal -->\n <modal v-if=\"showRescheduleModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRescheduleModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Reschedule Task</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Reschedule task <strong>{{ selectedTask?.id }}</strong>?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will reset the task's status and schedule it to run again.</p>\n <label for=\"newScheduledTime\" class=\"block text-sm font-medium text-content-secondary mb-2\">New Scheduled Time</label>\n <input\n id=\"newScheduledTime\"\n v-model=\"newScheduledTime\"\n type=\"datetime-local\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4\"\n />\n <div class=\"flex gap-3\">\n <button @click=\"confirmRescheduleTask\" class=\"flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 font-medium\">Reschedule</button>\n <button @click=\"showRescheduleModal = false\" class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\">Cancel</button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Run Modal -->\n <modal v-if=\"showRunModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRunModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Run Task Now</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Run task <strong>{{ selectedTask?.id }}</strong> immediately?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will execute the task right away, bypassing its scheduled time.</p>\n <div class=\"flex gap-3\">\n <button @click=\"confirmRunTask\" class=\"flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 font-medium\">Run Now</button>\n <button @click=\"showRunModal = false\" class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\">Cancel</button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Cancel Task Modal -->\n <modal v-if=\"showCancelModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showCancelModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Cancel Task</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Cancel task <strong>{{ selectedTask?.id }}</strong>?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will permanently cancel the task and it cannot be undone.</p>\n <div class=\"flex gap-3\">\n <button @click=\"confirmCancelTask\" class=\"flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium\">Cancel Task</button>\n <button @click=\"showCancelModal = false\" class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\">Keep Task</button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
50846
+ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'\">\n <img src=\"images/loader.gif\" alt=\"Loading\" />\n </div>\n <div v-else-if=\"status === 'error'\" class=\"text-red-600\">\n {{ errorMessage }}\n </div>\n <div v-else-if=\"status === 'notfound'\" class=\"text-gray-600\">\n Task not found.\n </div>\n <div v-else-if=\"task\" class=\"max-w-4xl\">\n <button @click=\"goBack\" class=\"text-content-tertiary hover:text-content-secondary mb-4 flex items-center gap-1\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n Back to {{ task.name }}\n </button>\n <h1 class=\"text-2xl font-bold text-content-secondary mb-1\">{{ task.name }}</h1>\n <p class=\"text-content-tertiary mb-6\">Task details</p>\n\n <div class=\"bg-surface rounded-lg shadow p-6 md:p-8\">\n <div class=\"flex items-center gap-3 mb-6\">\n <span class=\"text-sm font-medium text-content\">ID: {{ task.id }}</span>\n <span\n class=\"text-xs px-2 py-1 rounded-full font-medium\"\n :class=\"getStatusColor(task.status)\"\n >\n {{ task?.status }}\n </span>\n </div>\n\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6\">\n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.scheduledAt) }}</div>\n </div>\n <div v-if=\"task?.startedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Started At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.startedAt) }}</div>\n </div>\n <div v-if=\"task?.completedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Completed At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.completedAt) }}</div>\n </div>\n </div>\n\n <div v-if=\"task?.params\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Params</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.params\"></list-json>\n </div>\n </div>\n\n <div v-if=\"task?.result\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Result</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.result\"></list-json>\n </div>\n </div>\n\n <div v-if=\"task?.error\" class=\"mb-6\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Error</label>\n <div class=\"bg-page rounded-md p-4\">\n <list-json :value=\"task.error\"></list-json>\n </div>\n </div>\n\n <div class=\"flex flex-wrap gap-3 pt-4 border-t border-edge\">\n <button\n @click=\"showRescheduleConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n Reschedule\n </button>\n <button\n @click=\"showRunConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 disabled:opacity-50 disabled:cursor-not-allowed\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M5 3l14 9-14 9V3z\"></path>\n </svg>\n Run Now\n </button>\n <button\n v-if=\"task.status === 'pending'\"\n @click=\"showCancelConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700\"\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=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n Cancel Task\n </button>\n </div>\n </div>\n </div>\n\n <!-- Reschedule Modal -->\n <modal v-if=\"showRescheduleModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRescheduleModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Reschedule Task</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Reschedule task <strong>{{ selectedTask?.id }}</strong>?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will reset the task's status and schedule it to run again.</p>\n <label for=\"newScheduledTime\" class=\"block text-sm font-medium text-content-secondary mb-2\">New Scheduled Time</label>\n <input\n id=\"newScheduledTime\"\n v-model=\"newScheduledTime\"\n type=\"datetime-local\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4\"\n />\n <div class=\"flex gap-3\">\n <button @click=\"confirmRescheduleTask\" class=\"flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-md hover:from-blue-600 hover:to-blue-700 font-medium\">Reschedule</button>\n <button @click=\"showRescheduleModal = false\" class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\">Cancel</button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Run Modal -->\n <modal v-if=\"showRunModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRunModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Run Task Now</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Run task <strong>{{ selectedTask?.id }}</strong> immediately?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will execute the task right away, bypassing its scheduled time.</p>\n <div class=\"flex gap-3\">\n <button @click=\"confirmRunTask\" class=\"flex-1 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-md hover:from-green-600 hover:to-green-700 font-medium\">Run Now</button>\n <button @click=\"showRunModal = false\" class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\">Cancel</button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Cancel Task Modal -->\n <modal v-if=\"showCancelModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showCancelModal = false\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <h3 class=\"text-lg font-medium text-content mb-4\">Cancel Task</h3>\n <p class=\"text-sm text-gray-600 mb-2\">Cancel task <strong>{{ selectedTask?.id }}</strong>?</p>\n <p class=\"text-sm text-content-tertiary mb-4\">This will permanently cancel the task and it cannot be undone.</p>\n <div class=\"flex gap-3\">\n <button @click=\"confirmCancelTask\" class=\"flex-1 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-md hover:from-red-600 hover:to-red-700 font-medium\">Cancel Task</button>\n <button @click=\"showCancelModal = false\" class=\"flex-1 bg-muted hover:bg-page text-content-secondary px-4 py-2 rounded-md border border-edge-strong font-medium\">Keep Task</button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
50329
50847
 
50330
50848
  /***/ },
50331
50849
 
@@ -50336,7 +50854,7 @@ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'
50336
50854
  (module) {
50337
50855
 
50338
50856
  "use strict";
50339
- module.exports = "<div class=\"p-4 space-y-6\">\n <div class=\"flex items-center justify-between\">\n <div>\n <button v-if=\"showBackButton\" @click=\"goBack\" class=\"text-content-tertiary hover:text-content-secondary mb-2\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n {{ backLabel }}\n </button>\n <h1 class=\"text-2xl font-bold text-content-secondary\">{{ taskGroup.name }}</h1>\n <p class=\"text-content-tertiary\">Total: {{ taskGroup.totalCount }} tasks</p>\n </div>\n\n </div>\n\n <!-- Status Summary -->\n <div class=\"space-y-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium text-content-secondary\">Status</span>\n <div class=\"flex rounded-md shadow-sm\" role=\"group\">\n <button\n type=\"button\"\n @click=\"statusView = 'summary'\"\n class=\"px-3 py-1.5 text-sm font-medium rounded-l-md border transition-colors\"\n :class=\"statusView === 'summary' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'\"\n >\n Summary\n </button>\n <button\n type=\"button\"\n @click=\"statusView = 'chart'\"\n class=\"px-3 py-1.5 text-sm font-medium rounded-r-md border border-l-0 transition-colors\"\n :class=\"statusView === 'chart' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'\"\n >\n Chart\n </button>\n </div>\n </div>\n <!-- Summary view -->\n <div v-show=\"statusView === 'summary'\" class=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <button \n @click=\"filterByStatus('pending')\"\n class=\"bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }\"\n >\n <div class=\"text-xs text-yellow-600 font-medium\">Pending</div>\n <div class=\"text-lg font-bold text-yellow-700\">{{ taskGroup.statusCounts.pending || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('succeeded')\"\n class=\"bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }\"\n >\n <div class=\"text-xs text-green-600 font-medium\">Succeeded</div>\n <div class=\"text-lg font-bold text-green-700\">{{ taskGroup.statusCounts.succeeded || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('failed')\"\n class=\"bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-red-400': currentFilter === 'failed' }\"\n >\n <div class=\"text-xs text-red-600 font-medium\">Failed</div>\n <div class=\"text-lg font-bold text-red-700\">{{ taskGroup.statusCounts.failed || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('cancelled')\"\n class=\"bg-page border border-edge rounded-md p-3 text-center hover:bg-muted transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }\"\n >\n <div class=\"text-xs text-gray-600 font-medium\">Cancelled</div>\n <div class=\"text-lg font-bold text-content-secondary\">{{ taskGroup.statusCounts.cancelled || 0 }}</div>\n </button>\n </div>\n <!-- Chart view -->\n <div v-show=\"statusView === 'chart'\" class=\"flex flex-col items-center justify-center bg-surface border border-edge rounded-lg p-4 gap-3\" style=\"min-height: 280px;\">\n <div v-if=\"taskGroup.totalCount > 0\" class=\"w-[240px] h-[240px] shrink-0\">\n <canvas ref=\"statusPieChart\" width=\"240\" height=\"240\" class=\"block\"></canvas>\n </div>\n <p v-else class=\"text-content-tertiary text-sm py-8\">No tasks to display</p>\n <!-- Selection labels: show which segment is selected (click to filter) -->\n <div v-if=\"taskGroup.totalCount > 0\" class=\"flex flex-wrap justify-center gap-2\">\n <button\n v-for=\"status in statusOrderForDisplay\"\n :key=\"status\"\n type=\"button\"\n class=\"text-xs px-2 py-1 rounded-full font-medium transition-all cursor-pointer\"\n :class=\"currentFilter === status ? getStatusPillClass(status) : 'bg-muted text-content-tertiary hover:bg-muted'\"\n @click=\"filterByStatus(status)\"\n >\n {{ statusLabel(status) }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Task List -->\n <div class=\"bg-surface rounded-lg shadow\">\n <div class=\"px-6 py-6 border-b border-edge flex items-center justify-between bg-page\">\n <h2 class=\"text-xl font-bold text-content\">\n Individual Tasks\n <span v-if=\"currentFilter\" class=\"ml-3 text-base font-semibold text-primary\">\n (Filtered by {{ currentFilter }})\n </span>\n </h2>\n <button \n v-if=\"currentFilter\"\n @click=\"clearFilter\"\n class=\"text-sm font-semibold text-primary hover:text-primary\"\n >\n Show All\n </button>\n </div>\n <div class=\"divide-y divide-gray-200\">\n <div v-for=\"task in sortedTasks\" :key=\"task.id\" class=\"p-6\">\n <div class=\"flex items-start justify-between\">\n <div class=\"flex-1\">\n <div class=\"flex items-center gap-3 mb-2\">\n <span class=\"text-sm font-medium text-content\">Task ID: {{ task.id }}</span>\n <router-link\n v-if=\"backTo\"\n :to=\"taskDetailRoute(task)\"\n class=\"text-sm text-primary hover:text-primary font-medium\"\n >\n View details\n </router-link>\n <span\n class=\"text-xs px-2 py-1 rounded-full font-medium\"\n :class=\"getStatusColor(task.status)\"\n >\n {{ task.status }}\n </span>\n </div>\n \n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4\">\n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.scheduledAt) }}</div>\n </div>\n <div v-if=\"task.startedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Started At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.startedAt) }}</div>\n </div>\n <div v-if=\"task.completedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Completed At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.completedAt) }}</div>\n </div>\n <div v-if=\"task.error\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Error</label>\n <div class=\"text-sm text-red-600\">{{ task.error }}</div>\n </div>\n </div>\n\n <!-- Task Parameters -->\n <div v-if=\"task.parameters && Object.keys(task.parameters).length > 0\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Parameters</label>\n <div class=\"bg-page rounded-md p-3\">\n <pre class=\"text-sm text-gray-800 whitespace-pre-wrap\">{{ JSON.stringify(task.parameters, null, 2) }}</pre>\n </div>\n </div>\n </div>\n \n <div class=\"flex flex-col gap-3 ml-6\">\n <button \n @click=\"showRescheduleConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n Reschedule\n </button>\n <button \n @click=\"showRunConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M5 3l14 9-14 9V3z\"></path>\n </svg>\n Run Now\n </button>\n <button \n v-if=\"task.status === 'pending'\"\n @click=\"showCancelConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg\"\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=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Reschedule Confirmation Modal -->\n <modal v-if=\"showRescheduleModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRescheduleModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-blue-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Reschedule Task</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to reschedule task <strong>{{ selectedTask?.id }}</strong>?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will reset the task's status and schedule it to run again.\n </p>\n \n <div class=\"mt-4\">\n <label for=\"newScheduledTime\" class=\"block text-sm font-medium text-content-secondary mb-2\">\n New Scheduled Time\n </label>\n <input\n id=\"newScheduledTime\"\n v-model=\"newScheduledTime\"\n type=\"datetime-local\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500\"\n required\n />\n </div>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmRescheduleTask\"\n class=\"flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 font-medium\"\n >\n Reschedule\n </button>\n <button \n @click=\"showRescheduleModal = false\"\n class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Run Task Confirmation Modal -->\n <modal v-if=\"showRunModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRunModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-green-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Run Task Now</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to run task <strong>{{ selectedTask?.id }}</strong> immediately?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will execute the task right away, bypassing its scheduled time.\n </p>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmRunTask\"\n class=\"flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 font-medium\"\n >\n Run Now\n </button>\n <button \n @click=\"showRunModal = false\"\n class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Cancel Task Confirmation Modal -->\n <modal v-if=\"showCancelModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showCancelModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-red-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Cancel Task</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to cancel task <strong>{{ selectedTask?.id }}</strong>?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will permanently cancel the task and it cannot be undone.\n </p>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmCancelTask\"\n class=\"flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium\"\n >\n Cancel Task\n </button>\n <button \n @click=\"showCancelModal = false\"\n class=\"flex-1 bg-gray-300 text-content-secondary px-4 py-2 rounded-md hover:bg-gray-400 font-medium\"\n >\n Keep Task\n </button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n\n";
50857
+ module.exports = "<div class=\"p-4 space-y-6\">\n <div class=\"flex items-center justify-between\">\n <div>\n <button v-if=\"showBackButton\" @click=\"goBack\" class=\"text-content-tertiary hover:text-content-secondary mb-2\">\n <svg class=\"w-5 h-5\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\n </svg>\n {{ backLabel }}\n </button>\n <h1 class=\"text-2xl font-bold text-content-secondary\">{{ taskGroup.name }}</h1>\n <p class=\"text-content-tertiary\">Total: {{ taskGroup.totalCount }} tasks</p>\n </div>\n\n </div>\n\n <!-- Status Summary -->\n <div class=\"space-y-3\">\n <div class=\"flex items-center justify-between\">\n <span class=\"text-sm font-medium text-content-secondary\">Status</span>\n <div class=\"flex rounded-md shadow-sm\" role=\"group\">\n <button\n type=\"button\"\n @click=\"statusView = 'summary'\"\n class=\"px-3 py-1.5 text-sm font-medium rounded-l-md border transition-colors\"\n :class=\"statusView === 'summary' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'\"\n >\n Summary\n </button>\n <button\n type=\"button\"\n @click=\"statusView = 'chart'\"\n class=\"px-3 py-1.5 text-sm font-medium rounded-r-md border border-l-0 transition-colors\"\n :class=\"statusView === 'chart' ? 'bg-primary text-primary-text border-primary' : 'bg-surface text-content-secondary border-edge-strong hover:bg-page'\"\n >\n Chart\n </button>\n </div>\n </div>\n <!-- Summary view -->\n <div v-show=\"statusView === 'summary'\" class=\"grid grid-cols-2 sm:grid-cols-4 gap-4\">\n <button \n @click=\"filterByStatus('pending')\"\n class=\"bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }\"\n >\n <div class=\"text-xs text-yellow-600 font-medium\">Pending</div>\n <div class=\"text-lg font-bold text-yellow-700\">{{ taskGroup.statusCounts.pending || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('succeeded')\"\n class=\"bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }\"\n >\n <div class=\"text-xs text-green-600 font-medium\">Succeeded</div>\n <div class=\"text-lg font-bold text-green-700\">{{ taskGroup.statusCounts.succeeded || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('failed')\"\n class=\"bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-red-400': currentFilter === 'failed' }\"\n >\n <div class=\"text-xs text-red-600 font-medium\">Failed</div>\n <div class=\"text-lg font-bold text-red-700\">{{ taskGroup.statusCounts.failed || 0 }}</div>\n </button>\n <button \n @click=\"filterByStatus('cancelled')\"\n class=\"bg-page border border-edge rounded-md p-3 text-center hover:bg-muted transition-colors cursor-pointer\"\n :class=\"{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }\"\n >\n <div class=\"text-xs text-gray-600 font-medium\">Cancelled</div>\n <div class=\"text-lg font-bold text-content-secondary\">{{ taskGroup.statusCounts.cancelled || 0 }}</div>\n </button>\n </div>\n <!-- Chart view -->\n <div v-show=\"statusView === 'chart'\" class=\"flex flex-col items-center justify-center bg-surface border border-edge rounded-lg p-4 gap-3\" style=\"min-height: 280px;\">\n <div v-if=\"taskGroup.totalCount > 0\" class=\"w-[240px] h-[240px] shrink-0\">\n <canvas ref=\"statusPieChart\" width=\"240\" height=\"240\" class=\"block\"></canvas>\n </div>\n <p v-else class=\"text-content-tertiary text-sm py-8\">No tasks to display</p>\n <!-- Selection labels: show which segment is selected (click to filter) -->\n <div v-if=\"taskGroup.totalCount > 0\" class=\"flex flex-wrap justify-center gap-2\">\n <button\n v-for=\"status in statusOrderForDisplay\"\n :key=\"status\"\n type=\"button\"\n class=\"text-xs px-2 py-1 rounded-full font-medium transition-all cursor-pointer\"\n :class=\"currentFilter === status ? getStatusPillClass(status) : 'bg-muted text-content-tertiary hover:bg-muted'\"\n @click=\"filterByStatus(status)\"\n >\n {{ statusLabel(status) }}\n </button>\n </div>\n </div>\n </div>\n\n <!-- Task List -->\n <div class=\"bg-surface rounded-lg shadow\">\n <div class=\"px-6 py-6 border-b border-edge flex items-center justify-between bg-page\">\n <h2 class=\"text-xl font-bold text-content\">\n Individual Tasks\n <span v-if=\"currentFilter\" class=\"ml-3 text-base font-semibold text-primary\">\n (Filtered by {{ currentFilter }})\n </span>\n </h2>\n <button \n v-if=\"currentFilter\"\n @click=\"clearFilter\"\n class=\"text-sm font-semibold text-primary hover:text-primary\"\n >\n Show All\n </button>\n </div>\n <div class=\"divide-y divide-gray-200\">\n <div v-for=\"task in sortedTasks\" :key=\"task.id\" class=\"p-6\">\n <div class=\"flex items-start justify-between\">\n <div class=\"flex-1\">\n <div class=\"flex items-center gap-3 mb-2\">\n <span class=\"text-sm font-medium text-content\">Task ID: {{ task.id }}</span>\n <router-link\n v-if=\"backTo\"\n :to=\"taskDetailRoute(task)\"\n class=\"text-sm text-primary hover:text-primary font-medium\"\n >\n View details\n </router-link>\n <span\n class=\"text-xs px-2 py-1 rounded-full font-medium\"\n :class=\"getStatusColor(task.status)\"\n >\n {{ task.status }}\n </span>\n </div>\n \n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4 mb-4\">\n <div>\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Scheduled At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.scheduledAt) }}</div>\n </div>\n <div v-if=\"task.startedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Started At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.startedAt) }}</div>\n </div>\n <div v-if=\"task.completedAt\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Completed At</label>\n <div class=\"text-sm text-content\">{{ formatDate(task.completedAt) }}</div>\n </div>\n <div v-if=\"task.error\">\n <label class=\"block text-sm font-medium text-content-secondary mb-1\">Error</label>\n <div class=\"text-sm text-red-600\">{{ task.error }}</div>\n </div>\n </div>\n\n <!-- Task Parameters -->\n <div v-if=\"task.parameters && Object.keys(task.parameters).length > 0\">\n <label class=\"block text-sm font-medium text-content-secondary mb-2\">Parameters</label>\n <div class=\"bg-page rounded-md p-3\">\n <pre class=\"text-sm text-gray-800 whitespace-pre-wrap\">{{ JSON.stringify(task.parameters, null, 2) }}</pre>\n </div>\n </div>\n </div>\n \n <div class=\"flex flex-col gap-3 ml-6\">\n <button \n @click=\"showRescheduleConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n Reschedule\n </button>\n <button \n @click=\"showRunConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none\"\n :disabled=\"task.status === 'in_progress'\"\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=\"M5 3l14 9-14 9V3z\"></path>\n </svg>\n Run Now\n </button>\n <button \n v-if=\"task.status === 'pending'\"\n @click=\"showCancelConfirmation(task)\"\n class=\"flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700 transform hover:scale-105 transition-all duration-200 shadow-md hover:shadow-lg\"\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=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Reschedule Confirmation Modal -->\n <modal v-if=\"showRescheduleModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRescheduleModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-blue-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Reschedule Task</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to reschedule task <strong>{{ selectedTask?.id }}</strong>?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will reset the task's status and schedule it to run again.\n </p>\n \n <div class=\"mt-4\">\n <label for=\"newScheduledTime\" class=\"block text-sm font-medium text-content-secondary mb-2\">\n New Scheduled Time\n </label>\n <input\n id=\"newScheduledTime\"\n v-model=\"newScheduledTime\"\n type=\"datetime-local\"\n class=\"w-full px-3 py-2 border border-edge-strong rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500\"\n required\n />\n </div>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmRescheduleTask\"\n class=\"flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-md hover:from-blue-600 hover:to-blue-700 font-medium\"\n >\n Reschedule\n </button>\n <button \n @click=\"showRescheduleModal = false\"\n class=\"flex-1 bg-muted hover:bg-page text-content-secondary px-4 py-2 rounded-md border border-edge-strong font-medium\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Run Task Confirmation Modal -->\n <modal v-if=\"showRunModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showRunModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-green-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Run Task Now</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to run task <strong>{{ selectedTask?.id }}</strong> immediately?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will execute the task right away, bypassing its scheduled time.\n </p>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmRunTask\"\n class=\"flex-1 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-md hover:from-green-600 hover:to-green-700 font-medium\"\n >\n Run Now\n </button>\n <button \n @click=\"showRunModal = false\"\n class=\"flex-1 bg-muted hover:bg-page text-content-secondary px-4 py-2 rounded-md border border-edge-strong font-medium\"\n >\n Cancel\n </button>\n </div>\n </div>\n </template>\n </modal>\n\n <!-- Cancel Task Confirmation Modal -->\n <modal v-if=\"showCancelModal\" containerClass=\"!max-w-md\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showCancelModal = false;\" role=\"button\" aria-label=\"Close modal\">&times;</div>\n <div class=\"p-6\">\n <div class=\"flex items-center mb-4\">\n <div class=\"flex-shrink-0\">\n <svg class=\"w-6 h-6 text-red-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M6 18L18 6M6 6l12 12\"></path>\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-lg font-medium text-content\">Cancel Task</h3>\n </div>\n </div>\n <div class=\"mb-4\">\n <p class=\"text-sm text-gray-600\">\n Are you sure you want to cancel task <strong>{{ selectedTask?.id }}</strong>?\n </p>\n <p class=\"text-sm text-content-tertiary mt-2\">\n This will permanently cancel the task and it cannot be undone.\n </p>\n </div>\n <div class=\"flex gap-3\">\n <button \n @click=\"confirmCancelTask\"\n class=\"flex-1 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-md hover:from-red-600 hover:to-red-700 font-medium\"\n >\n Cancel Task\n </button>\n <button \n @click=\"showCancelModal = false\"\n class=\"flex-1 bg-muted hover:bg-page text-content-secondary px-4 py-2 rounded-md border border-edge-strong font-medium\"\n >\n Keep Task\n </button>\n </div>\n </div>\n </template>\n </modal>\n</div>\n\n";
50340
50858
 
50341
50859
  /***/ },
50342
50860
 
@@ -61672,7 +62190,7 @@ var src_default = VueToastificationPlugin;
61672
62190
  (module) {
61673
62191
 
61674
62192
  "use strict";
61675
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.3.3","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","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"}}');
62193
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.3.4","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","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","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"}}');
61676
62194
 
61677
62195
  /***/ }
61678
62196