@mongoosejs/studio 0.3.3 → 0.3.5

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,65 @@ 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 SHOW_ROW_NUMBERS_STORAGE_KEY = 'studio:model-show-row-numbers';
7069
+ const PROJECTION_MODE_QUERY_KEY = 'projectionMode';
7061
7070
  const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
7062
7071
  const MAX_RECENT_MODELS = 4;
7063
7072
 
7073
+ /** Parse `fields` from the route (JSON array or inclusion projection object only). */
7074
+ function parseFieldsQueryParam(fields) {
7075
+ if (fields == null || fields === '') {
7076
+ return [];
7077
+ }
7078
+ const s = typeof fields === 'string' ? fields : String(fields);
7079
+ const trimmed = s.trim();
7080
+ if (!trimmed) {
7081
+ return [];
7082
+ }
7083
+ let parsed;
7084
+ try {
7085
+ parsed = JSON.parse(trimmed);
7086
+ } catch (e) {
7087
+ return [];
7088
+ }
7089
+ if (Array.isArray(parsed)) {
7090
+ return parsed.map(x => String(x).trim()).filter(Boolean);
7091
+ }
7092
+ if (parsed != null && typeof parsed === 'object') {
7093
+ return Object.keys(parsed).filter(k =>
7094
+ Object.prototype.hasOwnProperty.call(parsed, k) && parsed[k]
7095
+ );
7096
+ }
7097
+ return [];
7098
+ }
7099
+
7100
+ /** Pass through a valid JSON `fields` string for Model.getDocuments / getDocumentsStream. */
7101
+ function normalizeFieldsParamForApi(fieldsStr) {
7102
+ if (fieldsStr == null || fieldsStr === '') {
7103
+ return null;
7104
+ }
7105
+ const s = typeof fieldsStr === 'string' ? fieldsStr : String(fieldsStr);
7106
+ const trimmed = s.trim();
7107
+ if (!trimmed) {
7108
+ return null;
7109
+ }
7110
+ try {
7111
+ const parsed = JSON.parse(trimmed);
7112
+ if (Array.isArray(parsed)) {
7113
+ return trimmed;
7114
+ }
7115
+ if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {
7116
+ return trimmed;
7117
+ }
7118
+ } catch (e) {
7119
+ return null;
7120
+ }
7121
+ return null;
7122
+ }
7123
+
7064
7124
  module.exports = app => app.component('models', {
7065
7125
  template: template,
7066
7126
  props: ['model', 'user', 'roles'],
@@ -7076,6 +7136,7 @@ module.exports = app => app.component('models', {
7076
7136
  mongoDBIndexes: [],
7077
7137
  schemaIndexes: [],
7078
7138
  status: 'loading',
7139
+ loadingMore: false,
7079
7140
  loadedAllDocs: false,
7080
7141
  edittingDoc: null,
7081
7142
  docEdits: null,
@@ -7084,7 +7145,10 @@ module.exports = app => app.component('models', {
7084
7145
  searchText: '',
7085
7146
  shouldShowExportModal: false,
7086
7147
  shouldShowCreateModal: false,
7087
- shouldShowFieldModal: false,
7148
+ projectionText: '',
7149
+ isProjectionMenuSelected: false,
7150
+ addFieldFilterText: '',
7151
+ showAddFieldDropdown: false,
7088
7152
  shouldShowIndexModal: false,
7089
7153
  shouldShowCollectionInfoModal: false,
7090
7154
  shouldShowUpdateMultipleModal: false,
@@ -7106,27 +7170,44 @@ module.exports = app => app.component('models', {
7106
7170
  collectionInfo: null,
7107
7171
  modelSearch: '',
7108
7172
  recentlyViewedModels: [],
7109
- showModelSwitcher: false
7173
+ showModelSwitcher: false,
7174
+ showRowNumbers: true,
7175
+ suppressScrollCheck: false,
7176
+ scrollTopToRestore: null
7110
7177
  }),
7111
7178
  created() {
7112
7179
  this.currentModel = this.model;
7113
7180
  this.setSearchTextFromRoute();
7114
7181
  this.loadOutputPreference();
7115
7182
  this.loadSelectedGeoField();
7183
+ this.loadShowRowNumbersPreference();
7116
7184
  this.loadRecentlyViewedModels();
7185
+ this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
7117
7186
  },
7118
7187
  beforeDestroy() {
7119
- document.removeEventListener('scroll', this.onScroll, true);
7120
7188
  window.removeEventListener('popstate', this.onPopState, true);
7121
7189
  document.removeEventListener('click', this.onOutsideActionsMenuClick, true);
7190
+ document.removeEventListener('click', this.onOutsideAddFieldDropdownClick, true);
7122
7191
  document.documentElement.removeEventListener('studio-theme-changed', this.onStudioThemeChanged);
7123
7192
  document.removeEventListener('keydown', this.onCtrlP, true);
7124
7193
  this.destroyMap();
7125
7194
  },
7126
7195
  async mounted() {
7196
+ // Persist scroll restoration across remounts.
7197
+ // This component is keyed by `$route.fullPath`, so query changes (e.g. projection updates)
7198
+ // recreate the component and reset scroll position.
7199
+ if (typeof window !== 'undefined') {
7200
+ if (typeof window.__studioModelsScrollTopToRestore === 'number') {
7201
+ this.scrollTopToRestore = window.__studioModelsScrollTopToRestore;
7202
+ }
7203
+ if (window.__studioModelsSuppressScrollCheck === true) {
7204
+ this.suppressScrollCheck = true;
7205
+ }
7206
+ delete window.__studioModelsScrollTopToRestore;
7207
+ delete window.__studioModelsSuppressScrollCheck;
7208
+ }
7209
+
7127
7210
  window.pageState = this;
7128
- this.onScroll = () => this.checkIfScrolledToBottom();
7129
- document.addEventListener('scroll', this.onScroll, true);
7130
7211
  this.onPopState = () => this.initSearchFromUrl();
7131
7212
  window.addEventListener('popstate', this.onPopState, true);
7132
7213
  this.onOutsideActionsMenuClick = event => {
@@ -7138,7 +7219,18 @@ module.exports = app => app.component('models', {
7138
7219
  this.closeActionsMenu();
7139
7220
  }
7140
7221
  };
7222
+ this.onOutsideAddFieldDropdownClick = event => {
7223
+ if (!this.showAddFieldDropdown) {
7224
+ return;
7225
+ }
7226
+ const container = this.$refs.addFieldContainer;
7227
+ if (container && !container.contains(event.target)) {
7228
+ this.showAddFieldDropdown = false;
7229
+ this.addFieldFilterText = '';
7230
+ }
7231
+ };
7141
7232
  document.addEventListener('click', this.onOutsideActionsMenuClick, true);
7233
+ document.addEventListener('click', this.onOutsideAddFieldDropdownClick, true);
7142
7234
  this.onStudioThemeChanged = () => this.updateMapTileLayer();
7143
7235
  document.documentElement.addEventListener('studio-theme-changed', this.onStudioThemeChanged);
7144
7236
  this.onCtrlP = (event) => {
@@ -7149,6 +7241,8 @@ module.exports = app => app.component('models', {
7149
7241
  };
7150
7242
  document.addEventListener('keydown', this.onCtrlP, true);
7151
7243
  this.query = Object.assign({}, this.$route.query);
7244
+ // Keep UI mode in sync with the URL on remounts.
7245
+ this.isProjectionMenuSelected = this.$route?.query?.[PROJECTION_MODE_QUERY_KEY] === '1';
7152
7246
  const { models, readyState } = await api.Model.listModels();
7153
7247
  this.models = models;
7154
7248
  await this.loadModelCounts();
@@ -7164,8 +7258,27 @@ module.exports = app => app.component('models', {
7164
7258
  }
7165
7259
 
7166
7260
  await this.initSearchFromUrl();
7261
+ if (this.isProjectionMenuSelected && this.outputType === 'map') {
7262
+ // Projection input is not rendered in map view.
7263
+ this.setOutputType('json');
7264
+ }
7265
+ this.$nextTick(() => {
7266
+ if (!this.isProjectionMenuSelected) return;
7267
+ const input = this.$refs.projectionInput;
7268
+ if (input && typeof input.focus === 'function') {
7269
+ input.focus();
7270
+ }
7271
+ });
7167
7272
  },
7168
7273
  watch: {
7274
+ model(newModel) {
7275
+ if (newModel !== this.currentModel) {
7276
+ this.currentModel = newModel;
7277
+ if (this.currentModel != null) {
7278
+ this.initSearchFromUrl();
7279
+ }
7280
+ }
7281
+ },
7169
7282
  documents: {
7170
7283
  handler() {
7171
7284
  if (this.outputType === 'map' && this.mapInstance) {
@@ -7270,6 +7383,19 @@ module.exports = app => app.component('models', {
7270
7383
  }
7271
7384
 
7272
7385
  return geoFields;
7386
+ },
7387
+ availablePathsToAdd() {
7388
+ const currentPaths = new Set(this.filteredPaths.map(p => p.path));
7389
+ return this.schemaPaths.filter(p => !currentPaths.has(p.path));
7390
+ },
7391
+ filteredPathsToAdd() {
7392
+ const available = this.availablePathsToAdd;
7393
+ const query = (this.addFieldFilterText || '').trim().toLowerCase();
7394
+ if (!query) return available;
7395
+ return available.filter(p => p.path.toLowerCase().includes(query));
7396
+ },
7397
+ tableDisplayPaths() {
7398
+ return this.filteredPaths.length > 0 ? this.filteredPaths : this.schemaPaths;
7273
7399
  }
7274
7400
  },
7275
7401
  methods: {
@@ -7334,6 +7460,24 @@ module.exports = app => app.component('models', {
7334
7460
  this.selectedGeoField = storedField;
7335
7461
  }
7336
7462
  },
7463
+ loadShowRowNumbersPreference() {
7464
+ if (typeof window === 'undefined' || !window.localStorage) {
7465
+ return;
7466
+ }
7467
+ const stored = window.localStorage.getItem(SHOW_ROW_NUMBERS_STORAGE_KEY);
7468
+ if (stored === '0') {
7469
+ this.showRowNumbers = false;
7470
+ } else if (stored === '1') {
7471
+ this.showRowNumbers = true;
7472
+ }
7473
+ },
7474
+ toggleRowNumbers() {
7475
+ this.showRowNumbers = !this.showRowNumbers;
7476
+ if (typeof window !== 'undefined' && window.localStorage) {
7477
+ window.localStorage.setItem(SHOW_ROW_NUMBERS_STORAGE_KEY, this.showRowNumbers ? '1' : '0');
7478
+ }
7479
+ this.showActionsMenu = false;
7480
+ },
7337
7481
  setOutputType(type) {
7338
7482
  if (type !== 'json' && type !== 'table' && type !== 'map') {
7339
7483
  return;
@@ -7529,6 +7673,22 @@ module.exports = app => app.component('models', {
7529
7673
  params.searchText = this.searchText;
7530
7674
  }
7531
7675
 
7676
+ // Prefer explicit URL projection (`query.fields`) so the first fetch after
7677
+ // mount/remount respects deep-linked projections before `filteredPaths`
7678
+ // is rehydrated from schema paths.
7679
+ let fieldsParam = normalizeFieldsParamForApi(this.query?.fields);
7680
+ if (!fieldsParam && this.isProjectionMenuSelected === true) {
7681
+ const fieldPaths = this.filteredPaths && this.filteredPaths.length > 0
7682
+ ? this.filteredPaths.map(p => p.path).filter(Boolean)
7683
+ : null;
7684
+ if (fieldPaths && fieldPaths.length > 0) {
7685
+ fieldsParam = JSON.stringify(fieldPaths);
7686
+ }
7687
+ }
7688
+ if (fieldsParam) {
7689
+ params.fields = fieldsParam;
7690
+ }
7691
+
7532
7692
  return params;
7533
7693
  },
7534
7694
  setSearchTextFromRoute() {
@@ -7542,20 +7702,34 @@ module.exports = app => app.component('models', {
7542
7702
  this.status = 'loading';
7543
7703
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
7544
7704
  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);
7705
+ // Avoid eval() on user-controlled query params.
7706
+ // Use explicit sortKey + sortDirection query params.
7707
+ const sortKey = this.$route.query?.sortKey;
7708
+ const sortDirectionRaw = this.$route.query?.sortDirection;
7709
+ const sortDirection = typeof sortDirectionRaw === 'string' ? Number(sortDirectionRaw) : sortDirectionRaw;
7710
+
7711
+ if (typeof sortKey === 'string' && sortKey.trim().length > 0 &&
7712
+ (sortDirection === 1 || sortDirection === -1)) {
7713
+ for (const key in this.sortBy) {
7714
+ delete this.sortBy[key];
7715
+ }
7716
+ this.sortBy[sortKey] = sortDirection;
7717
+ // Normalize to new params and remove legacy key if present.
7718
+ this.query.sortKey = sortKey;
7719
+ this.query.sortDirection = sortDirection;
7720
+ delete this.query.sort;
7550
7721
  }
7551
-
7552
-
7553
7722
  if (this.currentModel != null) {
7554
7723
  await this.getDocuments();
7555
7724
  }
7556
7725
  if (this.$route.query?.fields) {
7557
- const filter = this.$route.query.fields.split(',');
7558
- this.filteredPaths = this.filteredPaths.filter(x => filter.includes(x.path));
7726
+ const urlPaths = parseFieldsQueryParam(this.$route.query.fields);
7727
+ if (urlPaths.length > 0) {
7728
+ this.filteredPaths = urlPaths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
7729
+ if (this.filteredPaths.length > 0) {
7730
+ this.syncProjectionFromPaths();
7731
+ }
7732
+ }
7559
7733
  }
7560
7734
  this.status = 'loaded';
7561
7735
 
@@ -7575,10 +7749,8 @@ module.exports = app => app.component('models', {
7575
7749
  this.shouldShowCreateModal = false;
7576
7750
  await this.getDocuments();
7577
7751
  },
7578
- initializeDocumentData() {
7579
- this.shouldShowCreateModal = true;
7580
- },
7581
7752
  filterDocument(doc) {
7753
+ if (this.filteredPaths.length === 0) return doc;
7582
7754
  const filteredDoc = {};
7583
7755
  for (let i = 0; i < this.filteredPaths.length; i++) {
7584
7756
  const path = this.filteredPaths[i].path;
@@ -7600,23 +7772,47 @@ module.exports = app => app.component('models', {
7600
7772
  if (this.status === 'loading' || this.loadedAllDocs) {
7601
7773
  return;
7602
7774
  }
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 });
7775
+ // Infinite scroll only applies to table/json views.
7776
+ if (this.outputType !== 'table' && this.outputType !== 'json') {
7777
+ return;
7778
+ }
7779
+ if (this.documents.length === 0) {
7780
+ return;
7781
+ }
7782
+ const container = this.outputType === 'table'
7783
+ ? this.$refs.documentsScrollContainer
7784
+ : this.$refs.documentsContainerScroll;
7785
+ if (!container || container.scrollHeight <= 0) {
7786
+ return;
7787
+ }
7788
+ const threshold = 150;
7789
+ const nearBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - threshold;
7790
+ if (!nearBottom) {
7791
+ return;
7792
+ }
7793
+ this.loadingMore = true;
7794
+ this.status = 'loading';
7795
+ try {
7796
+ const skip = this.documents.length;
7797
+ const params = this.buildDocumentFetchParams({ skip });
7607
7798
  const { docs } = await api.Model.getDocuments(params);
7608
7799
  if (docs.length < limit) {
7609
7800
  this.loadedAllDocs = true;
7610
7801
  }
7611
7802
  this.documents.push(...docs);
7803
+ } finally {
7804
+ this.loadingMore = false;
7612
7805
  this.status = 'loaded';
7613
7806
  }
7807
+ this.$nextTick(() => this.checkIfScrolledToBottom());
7614
7808
  },
7615
7809
  async sortDocs(num, path) {
7616
7810
  let sorted = false;
7617
7811
  if (this.sortBy[path] == num) {
7618
7812
  sorted = true;
7619
7813
  delete this.query.sort;
7814
+ delete this.query.sortKey;
7815
+ delete this.query.sortDirection;
7620
7816
  this.$router.push({ query: this.query });
7621
7817
  }
7622
7818
  for (const key in this.sortBy) {
@@ -7624,9 +7820,13 @@ module.exports = app => app.component('models', {
7624
7820
  }
7625
7821
  if (!sorted) {
7626
7822
  this.sortBy[path] = num;
7627
- this.query.sort = `{${path}:${num}}`;
7823
+ this.query.sortKey = path;
7824
+ this.query.sortDirection = num;
7825
+ delete this.query.sort;
7628
7826
  this.$router.push({ query: this.query });
7629
7827
  }
7828
+ this.documents = [];
7829
+ this.loadedAllDocs = false;
7630
7830
  await this.loadMoreDocuments();
7631
7831
  },
7632
7832
  async search(searchText) {
@@ -7663,6 +7863,27 @@ module.exports = app => app.component('models', {
7663
7863
  closeActionsMenu() {
7664
7864
  this.showActionsMenu = false;
7665
7865
  },
7866
+ toggleProjectionMenu() {
7867
+ const next = !this.isProjectionMenuSelected;
7868
+ this.isProjectionMenuSelected = next;
7869
+
7870
+ // Because the route-view is keyed on `$route.fullPath`, query changes remount this component.
7871
+ // Persist projection UI state in the URL so Reset/Suggest don't turn the mode off.
7872
+ if (next) {
7873
+ this.query[PROJECTION_MODE_QUERY_KEY] = '1';
7874
+ if (this.outputType === 'map') {
7875
+ this.setOutputType('json');
7876
+ }
7877
+ } else {
7878
+ delete this.query[PROJECTION_MODE_QUERY_KEY];
7879
+ delete this.query.fields;
7880
+ this.filteredPaths = [];
7881
+ this.selectedPaths = [];
7882
+ this.projectionText = '';
7883
+ }
7884
+
7885
+ this.$router.push({ query: this.query });
7886
+ },
7666
7887
  async openCollectionInfo() {
7667
7888
  this.closeActionsMenu();
7668
7889
  this.shouldShowCollectionInfoModal = true;
@@ -7763,160 +7984,345 @@ module.exports = app => app.component('models', {
7763
7984
  }
7764
7985
  return formatValue(value / 1000000000, 'B');
7765
7986
  },
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
7987
  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;
7988
+ this.loadingMore = false;
7989
+ this.status = 'loading';
7990
+ try {
7991
+ // Track recently viewed model
7992
+ this.trackRecentModel(this.currentModel);
7993
+
7994
+ // Clear previous data
7995
+ this.documents = [];
7996
+ this.schemaPaths = [];
7997
+ this.numDocuments = null;
7998
+ this.loadedAllDocs = false;
7999
+ this.lastSelectedIndex = null;
7785
8000
 
7786
- let docsCount = 0;
7787
- let schemaPathsReceived = false;
8001
+ let docsCount = 0;
8002
+ let schemaPathsReceived = false;
7788
8003
 
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) {
8004
+ // Use async generator to stream SSEs
8005
+ const params = this.buildDocumentFetchParams();
8006
+ for await (const event of api.Model.getDocumentsStream(params)) {
8007
+ if (event.schemaPaths && !schemaPathsReceived) {
7793
8008
  // 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;
8009
+ this.schemaPaths = Object.keys(event.schemaPaths).sort((k1, k2) => {
8010
+ if (k1 === '_id' && k2 !== '_id') {
8011
+ return -1;
8012
+ }
8013
+ if (k1 !== '_id' && k2 === '_id') {
8014
+ return 1;
8015
+ }
8016
+ return 0;
8017
+ }).map(key => event.schemaPaths[key]);
8018
+ this.shouldExport = {};
8019
+ for (const { path } of this.schemaPaths) {
8020
+ this.shouldExport[path] = true;
8021
+ }
8022
+ const isProjectionModeOn = this.isProjectionMenuSelected === true;
8023
+ if (isProjectionModeOn) {
8024
+ this.applyDefaultProjection(event.suggestedFields);
8025
+ } else {
8026
+ this.filteredPaths = [];
8027
+ this.selectedPaths = [];
8028
+ this.projectionText = '';
7800
8029
  }
7801
- return 0;
7802
- }).map(key => event.schemaPaths[key]);
7803
- this.shouldExport = {};
7804
- for (const { path } of this.schemaPaths) {
7805
- this.shouldExport[path] = true;
8030
+ this.selectedPaths = [...this.filteredPaths];
8031
+ this.syncProjectionFromPaths();
8032
+ schemaPathsReceived = true;
8033
+ }
8034
+ if (event.numDocs !== undefined) {
8035
+ this.numDocuments = event.numDocs;
8036
+ }
8037
+ if (event.document) {
8038
+ this.documents.push(event.document);
8039
+ docsCount++;
8040
+ }
8041
+ if (event.message) {
8042
+ throw new Error(event.message);
7806
8043
  }
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
8044
  }
7814
- if (event.document) {
7815
- this.documents.push(event.document);
7816
- docsCount++;
8045
+
8046
+ if (docsCount < limit) {
8047
+ this.loadedAllDocs = true;
7817
8048
  }
7818
- if (event.message) {
7819
- this.status = 'loaded';
7820
- throw new Error(event.message);
8049
+ } finally {
8050
+ this.status = 'loaded';
8051
+ }
8052
+ this.$nextTick(() => {
8053
+ this.restoreScrollPosition();
8054
+ if (!this.suppressScrollCheck) {
8055
+ this.checkIfScrolledToBottom();
7821
8056
  }
8057
+ this.suppressScrollCheck = false;
8058
+ });
8059
+ },
8060
+ async loadMoreDocuments() {
8061
+ const isLoadingMore = this.documents.length > 0;
8062
+ if (isLoadingMore) {
8063
+ this.loadingMore = true;
7822
8064
  }
8065
+ this.status = 'loading';
8066
+ try {
8067
+ let docsCount = 0;
8068
+ let numDocsReceived = false;
7823
8069
 
7824
- if (docsCount < limit) {
7825
- this.loadedAllDocs = true;
8070
+ // Use async generator to stream SSEs
8071
+ const params = this.buildDocumentFetchParams({ skip: this.documents.length });
8072
+ for await (const event of api.Model.getDocumentsStream(params)) {
8073
+ if (event.numDocs !== undefined && !numDocsReceived) {
8074
+ this.numDocuments = event.numDocs;
8075
+ numDocsReceived = true;
8076
+ }
8077
+ if (event.document) {
8078
+ this.documents.push(event.document);
8079
+ docsCount++;
8080
+ }
8081
+ if (event.message) {
8082
+ throw new Error(event.message);
8083
+ }
8084
+ }
8085
+
8086
+ if (docsCount < limit) {
8087
+ this.loadedAllDocs = true;
8088
+ }
8089
+ } finally {
8090
+ this.loadingMore = false;
8091
+ this.status = 'loaded';
7826
8092
  }
8093
+ this.$nextTick(() => this.checkIfScrolledToBottom());
7827
8094
  },
7828
- async loadMoreDocuments() {
7829
- let docsCount = 0;
7830
- let numDocsReceived = false;
8095
+ applyDefaultProjection(suggestedFields) {
8096
+ if (Array.isArray(suggestedFields) && suggestedFields.length > 0) {
8097
+ this.filteredPaths = suggestedFields
8098
+ .map(path => this.schemaPaths.find(p => p.path === path))
8099
+ .filter(Boolean);
8100
+ }
8101
+ if (!this.filteredPaths || this.filteredPaths.length === 0) {
8102
+ this.filteredPaths = this.schemaPaths.slice(0, DEFAULT_FIRST_N_FIELDS);
8103
+ }
8104
+ if (this.filteredPaths.length === 0) {
8105
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8106
+ }
8107
+ },
8108
+ clearProjection() {
8109
+ // Keep current filter input in sync with the URL so projection reset
8110
+ // does not unintentionally wipe the filter on remount.
8111
+ this.syncFilterToQuery();
8112
+ this.filteredPaths = [];
8113
+ this.selectedPaths = [];
8114
+ this.projectionText = '';
8115
+ this.updateProjectionQuery();
8116
+ },
8117
+ resetFilter() {
8118
+ // Reuse the existing "apply filter + update URL" flow.
8119
+ this.search('');
8120
+ },
8121
+ syncFilterToQuery() {
8122
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
8123
+ this.query.search = this.searchText;
8124
+ } else {
8125
+ delete this.query.search;
8126
+ }
8127
+ },
8128
+ applyDefaultProjectionColumns() {
8129
+ if (!this.schemaPaths || this.schemaPaths.length === 0) return;
8130
+ const pathNames = this.schemaPaths.map(p => p.path);
8131
+ this.applyDefaultProjection(pathNames.slice(0, DEFAULT_FIRST_N_FIELDS));
8132
+ this.selectedPaths = [...this.filteredPaths];
8133
+ this.syncProjectionFromPaths();
8134
+ this.updateProjectionQuery();
8135
+ },
8136
+ initProjection(ev) {
8137
+ if (!this.projectionText || !this.projectionText.trim()) {
8138
+ this.projectionText = '';
8139
+ this.$nextTick(() => {
8140
+ if (ev && ev.target) {
8141
+ ev.target.setSelectionRange(0, 0);
8142
+ }
8143
+ });
8144
+ }
8145
+ },
8146
+ syncProjectionFromPaths() {
8147
+ if (this.filteredPaths.length === 0) {
8148
+ this.projectionText = '';
8149
+ return;
8150
+ }
8151
+ // String-only projection syntax: `field1 field2` and `-field` for exclusions.
8152
+ // Since `filteredPaths` represents the final include set, we serialize as space-separated fields.
8153
+ this.projectionText = this.filteredPaths.map(p => p.path).join(' ');
8154
+ },
8155
+ parseProjectionInput(text) {
8156
+ if (!text || typeof text !== 'string') {
8157
+ return [];
8158
+ }
8159
+ const trimmed = text.trim();
8160
+ if (!trimmed) {
8161
+ return [];
8162
+ }
8163
+ const normalizeKey = (key) => String(key).trim();
7831
8164
 
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;
7838
- }
7839
- if (event.document) {
7840
- this.documents.push(event.document);
7841
- docsCount++;
8165
+ // String-only projection syntax:
8166
+ // name email
8167
+ // -password (exclusion-only)
8168
+ // +email (inclusion-only)
8169
+ //
8170
+ // Brace/object syntax is intentionally NOT supported.
8171
+ if (trimmed.startsWith('{') || trimmed.endsWith('}')) {
8172
+ return null;
8173
+ }
8174
+
8175
+ const tokens = trimmed.split(/[,\s]+/).filter(Boolean);
8176
+ if (tokens.length === 0) return [];
8177
+
8178
+ const includeKeys = [];
8179
+ const excludeKeys = [];
8180
+
8181
+ for (const rawToken of tokens) {
8182
+ const token = rawToken.trim();
8183
+ if (!token) continue;
8184
+
8185
+ const prefix = token[0];
8186
+ if (prefix === '-') {
8187
+ const path = token.slice(1).trim();
8188
+ if (!path) return null;
8189
+ excludeKeys.push(path);
8190
+ } else if (prefix === '+') {
8191
+ const path = token.slice(1).trim();
8192
+ if (!path) return null;
8193
+ includeKeys.push(path);
8194
+ } else {
8195
+ includeKeys.push(token);
7842
8196
  }
7843
- if (event.message) {
7844
- this.status = 'loaded';
7845
- throw new Error(event.message);
8197
+ }
8198
+
8199
+ if (includeKeys.length > 0 && excludeKeys.length > 0) {
8200
+ // Support subtractive edits on an existing projection string, e.g.
8201
+ // `name email createdAt -email` -> `name createdAt`.
8202
+ const includeSet = new Set(includeKeys.map(normalizeKey));
8203
+ for (const path of excludeKeys) {
8204
+ includeSet.delete(normalizeKey(path));
7846
8205
  }
8206
+ return Array.from(includeSet);
7847
8207
  }
7848
8208
 
7849
- if (docsCount < limit) {
7850
- this.loadedAllDocs = true;
8209
+ if (excludeKeys.length > 0) {
8210
+ const excludeSet = new Set(excludeKeys.map(normalizeKey));
8211
+ return this.schemaPaths.map(p => p.path).filter(p => !excludeSet.has(p));
7851
8212
  }
8213
+
8214
+ return includeKeys.map(normalizeKey);
7852
8215
  },
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;
7865
- }
7866
- return 0;
7867
- }).map(key => this.selectedPaths[key]);
8216
+ applyProjectionFromInput() {
8217
+ const paths = this.parseProjectionInput(this.projectionText);
8218
+ if (paths === null) {
8219
+ this.syncProjectionFromPaths();
8220
+ return;
7868
8221
  }
7869
- },
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] });
8222
+ if (paths.length === 0) {
8223
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8224
+ if (this.filteredPaths.length === 0 && this.schemaPaths.length > 0) {
8225
+ const idPath = this.schemaPaths.find(p => p.path === '_id');
8226
+ this.filteredPaths = idPath ? [idPath] : [this.schemaPaths[0]];
7877
8227
  }
7878
8228
  } else {
7879
- this.selectedPaths = [{ path: '_id' }];
8229
+ this.filteredPaths = paths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
8230
+ const validPaths = new Set(this.schemaPaths.map(p => p.path));
8231
+ for (const path of paths) {
8232
+ if (validPaths.has(path) && !this.filteredPaths.find(p => p.path === path)) {
8233
+ this.filteredPaths.push(this.schemaPaths.find(p => p.path === path));
8234
+ }
8235
+ }
8236
+ if (this.filteredPaths.length === 0) {
8237
+ this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
8238
+ }
7880
8239
  }
7881
- this.shouldShowFieldModal = true;
8240
+ this.selectedPaths = [...this.filteredPaths];
8241
+ this.syncProjectionFromPaths();
8242
+ this.updateProjectionQuery();
7882
8243
  },
7883
- filterDocuments() {
7884
- if (this.selectedPaths.length > 0) {
7885
- this.filteredPaths = [...this.selectedPaths];
8244
+ updateProjectionQuery() {
8245
+ const paths = this.filteredPaths.map(x => x.path).filter(Boolean);
8246
+ if (paths.length > 0) {
8247
+ this.query.fields = JSON.stringify(paths);
7886
8248
  } else {
7887
- this.filteredPaths.length = 0;
8249
+ delete this.query.fields;
7888
8250
  }
7889
- this.shouldShowFieldModal = false;
7890
- const selectedParams = this.filteredPaths.map(x => x.path).join(',');
7891
- this.query.fields = selectedParams;
7892
8251
  this.$router.push({ query: this.query });
7893
8252
  },
7894
- resetDocuments() {
7895
- this.selectedPaths = [...this.filteredPaths];
7896
- this.query.fields = {};
7897
- this.$router.push({ query: this.query });
7898
- this.shouldShowFieldModal = false;
7899
- },
7900
- deselectAll() {
7901
- this.selectedPaths = [];
8253
+ removeField(schemaPath) {
8254
+ if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
8255
+ this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
8256
+ this.suppressScrollCheck = true;
8257
+ // Persist for remount caused by query changes.
8258
+ if (typeof window !== 'undefined') {
8259
+ window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
8260
+ window.__studioModelsSuppressScrollCheck = true;
8261
+ }
8262
+ }
8263
+ const index = this.filteredPaths.findIndex(p => p.path === schemaPath.path);
8264
+ if (index !== -1) {
8265
+ this.filteredPaths.splice(index, 1);
8266
+ if (this.filteredPaths.length === 0) {
8267
+ const idPath = this.schemaPaths.find(p => p.path === '_id');
8268
+ this.filteredPaths = idPath ? [idPath] : [];
8269
+ }
8270
+ this.syncProjectionFromPaths();
8271
+ this.updateProjectionQuery();
8272
+ }
8273
+ },
8274
+ addField(schemaPath) {
8275
+ if (!this.filteredPaths.find(p => p.path === schemaPath.path)) {
8276
+ if (this.outputType === 'table' && this.$refs.documentsScrollContainer) {
8277
+ this.scrollTopToRestore = this.$refs.documentsScrollContainer.scrollTop;
8278
+ this.suppressScrollCheck = true;
8279
+ // Persist for remount caused by query changes.
8280
+ if (typeof window !== 'undefined') {
8281
+ window.__studioModelsScrollTopToRestore = this.scrollTopToRestore;
8282
+ window.__studioModelsSuppressScrollCheck = true;
8283
+ }
8284
+ }
8285
+ this.filteredPaths.push(schemaPath);
8286
+ this.filteredPaths.sort((a, b) => {
8287
+ if (a.path === '_id') return -1;
8288
+ if (b.path === '_id') return 1;
8289
+ return 0;
8290
+ });
8291
+ this.syncProjectionFromPaths();
8292
+ this.updateProjectionQuery();
8293
+ this.showAddFieldDropdown = false;
8294
+ this.addFieldFilterText = '';
8295
+ }
7902
8296
  },
7903
- selectAll() {
7904
- this.selectedPaths = [...this.schemaPaths];
8297
+ restoreScrollPosition() {
8298
+ if (this.outputType !== 'table') return;
8299
+ if (this.scrollTopToRestore == null) return;
8300
+ const container = this.$refs.documentsScrollContainer;
8301
+ if (!container) return;
8302
+ container.scrollTop = this.scrollTopToRestore;
8303
+ this.scrollTopToRestore = null;
7905
8304
  },
7906
- isSelected(path) {
7907
- return this.selectedPaths.find(x => x.path == path);
8305
+ toggleAddFieldDropdown() {
8306
+ this.showAddFieldDropdown = !this.showAddFieldDropdown;
8307
+ if (this.showAddFieldDropdown) {
8308
+ this.addFieldFilterText = '';
8309
+ this.$nextTick(() => this.$refs.addFieldFilterInput?.focus());
8310
+ }
7908
8311
  },
7909
8312
  getComponentForPath(schemaPath) {
8313
+ if (!schemaPath || typeof schemaPath !== 'object') {
8314
+ return 'list-mixed';
8315
+ }
7910
8316
  if (schemaPath.instance === 'Array') {
7911
8317
  return 'list-array';
7912
8318
  }
7913
8319
  if (schemaPath.instance === 'String') {
7914
8320
  return 'list-string';
7915
8321
  }
7916
- if (schemaPath.instance == 'Embedded') {
8322
+ if (schemaPath.instance === 'Embedded') {
7917
8323
  return 'list-subdocument';
7918
8324
  }
7919
- if (schemaPath.instance == 'Mixed') {
8325
+ if (schemaPath.instance === 'Mixed') {
7920
8326
  return 'list-mixed';
7921
8327
  }
7922
8328
  return 'list-default';
@@ -7941,6 +8347,31 @@ module.exports = app => app.component('models', {
7941
8347
  this.edittingDoc = null;
7942
8348
  this.$toast.success('Document updated!');
7943
8349
  },
8350
+ copyCellValue(value) {
8351
+ const text = value == null ? '' : (typeof value === 'object' ? JSON.stringify(value) : String(value));
8352
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
8353
+ navigator.clipboard.writeText(text).then(() => {
8354
+ this.$toast.success('Copied to clipboard');
8355
+ }).catch(() => {
8356
+ this.fallbackCopyText(text);
8357
+ });
8358
+ } else {
8359
+ this.fallbackCopyText(text);
8360
+ }
8361
+ },
8362
+ fallbackCopyText(text) {
8363
+ try {
8364
+ const el = document.createElement('textarea');
8365
+ el.value = text;
8366
+ document.body.appendChild(el);
8367
+ el.select();
8368
+ document.execCommand('copy');
8369
+ document.body.removeChild(el);
8370
+ this.$toast.success('Copied to clipboard');
8371
+ } catch (err) {
8372
+ this.$toast.error('Copy failed');
8373
+ }
8374
+ },
7944
8375
  handleDocumentClick(document, event) {
7945
8376
  if (this.selectMultiple) {
7946
8377
  this.handleDocumentSelection(document, event);
@@ -8308,7 +8739,7 @@ module.exports = app => app.component('navbar', {
8308
8739
  showFlyout: false,
8309
8740
  darkMode: typeof localStorage !== 'undefined' && localStorage.getItem('studio-theme') === 'dark'
8310
8741
  }),
8311
- mounted: function () {
8742
+ mounted: function() {
8312
8743
  window.navbar = this;
8313
8744
  const mobileMenuMask = document.querySelector('#mobile-menu-mask');
8314
8745
  const mobileMenu = document.querySelector('#mobile-menu');
@@ -9985,7 +10416,7 @@ __webpack_require__.r(__webpack_exports__);
9985
10416
  /* harmony export */ });
9986
10417
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
9987
10418
  /**
9988
- * @vue/reactivity v3.5.30
10419
+ * @vue/reactivity v3.5.31
9989
10420
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
9990
10421
  * @license MIT
9991
10422
  **/
@@ -11578,16 +12009,16 @@ function toRefs(object) {
11578
12009
  return ret;
11579
12010
  }
11580
12011
  class ObjectRefImpl {
11581
- constructor(_object, _key, _defaultValue) {
12012
+ constructor(_object, key, _defaultValue) {
11582
12013
  this._object = _object;
11583
- this._key = _key;
11584
12014
  this._defaultValue = _defaultValue;
11585
12015
  this["__v_isRef"] = true;
11586
12016
  this._value = void 0;
12017
+ this._key = (0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isSymbol)(key) ? key : String(key);
11587
12018
  this._raw = toRaw(_object);
11588
12019
  let shallow = true;
11589
12020
  let obj = _object;
11590
- if (!(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isArray)(_object) || !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_0__.isIntegerKey)(String(_key))) {
12021
+ 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
12022
  do {
11592
12023
  shallow = !isProxy(obj) || isShallow(obj);
11593
12024
  } while (shallow && (obj = obj["__v_raw"]));
@@ -12132,7 +12563,7 @@ __webpack_require__.r(__webpack_exports__);
12132
12563
  /* harmony import */ var _vue_reactivity__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @vue/reactivity */ "./node_modules/@vue/reactivity/dist/reactivity.esm-bundler.js");
12133
12564
  /* harmony import */ var _vue_shared__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
12134
12565
  /**
12135
- * @vue/runtime-core v3.5.30
12566
+ * @vue/runtime-core v3.5.31
12136
12567
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
12137
12568
  * @license MIT
12138
12569
  **/
@@ -12579,6 +13010,13 @@ function checkRecursiveUpdates(seen, fn) {
12579
13010
  }
12580
13011
 
12581
13012
  let isHmrUpdating = false;
13013
+ const setHmrUpdating = (v) => {
13014
+ try {
13015
+ return isHmrUpdating;
13016
+ } finally {
13017
+ isHmrUpdating = v;
13018
+ }
13019
+ };
12582
13020
  const hmrDirtyComponents = /* @__PURE__ */ new Map();
12583
13021
  if (true) {
12584
13022
  (0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.getGlobalThis)().__VUE_HMR_RUNTIME__ = {
@@ -13165,9 +13603,10 @@ const TeleportImpl = {
13165
13603
  mount(container, mainAnchor);
13166
13604
  updateCssVars(n2, true);
13167
13605
  }
13168
- if (isTeleportDeferred(n2.props)) {
13606
+ if (isTeleportDeferred(n2.props) || parentSuspense && parentSuspense.pendingBranch) {
13169
13607
  n2.el.__isMounted = false;
13170
13608
  queuePostRenderEffect(() => {
13609
+ if (n2.el.__isMounted !== false) return;
13171
13610
  mountToTarget();
13172
13611
  delete n2.el.__isMounted;
13173
13612
  }, parentSuspense);
@@ -13175,7 +13614,12 @@ const TeleportImpl = {
13175
13614
  mountToTarget();
13176
13615
  }
13177
13616
  } else {
13178
- if (isTeleportDeferred(n2.props) && n1.el.__isMounted === false) {
13617
+ n2.el = n1.el;
13618
+ n2.targetStart = n1.targetStart;
13619
+ const mainAnchor = n2.anchor = n1.anchor;
13620
+ const target = n2.target = n1.target;
13621
+ const targetAnchor = n2.targetAnchor = n1.targetAnchor;
13622
+ if (n1.el.__isMounted === false) {
13179
13623
  queuePostRenderEffect(() => {
13180
13624
  TeleportImpl.process(
13181
13625
  n1,
@@ -13192,11 +13636,6 @@ const TeleportImpl = {
13192
13636
  }, parentSuspense);
13193
13637
  return;
13194
13638
  }
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
13639
  const wasDisabled = isTeleportDisabled(n1.props);
13201
13640
  const currentContainer = wasDisabled ? container : target;
13202
13641
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor;
@@ -13661,7 +14100,7 @@ function resolveTransitionHooks(vnode, props, state, instance, postClone) {
13661
14100
  callHook(hook, [el]);
13662
14101
  },
13663
14102
  enter(el) {
13664
- if (leavingVNodesCache[key] === vnode) return;
14103
+ if (!isHmrUpdating && leavingVNodesCache[key] === vnode) return;
13665
14104
  let hook = onEnter;
13666
14105
  let afterHook = onAfterEnter;
13667
14106
  let cancelHook = onEnterCancelled;
@@ -16909,11 +17348,12 @@ function hasPropValueChanged(nextProps, prevProps, key) {
16909
17348
  }
16910
17349
  return nextProp !== prevProp;
16911
17350
  }
16912
- function updateHOCHostEl({ vnode, parent }, el) {
17351
+ function updateHOCHostEl({ vnode, parent, suspense }, el) {
16913
17352
  while (parent) {
16914
17353
  const root = parent.subTree;
16915
17354
  if (root.suspense && root.suspense.activeBranch === vnode) {
16916
- root.el = vnode.el;
17355
+ root.suspense.vnode.el = root.el = el;
17356
+ vnode = root;
16917
17357
  }
16918
17358
  if (root === vnode) {
16919
17359
  (vnode = parent.vnode).el = el;
@@ -16922,6 +17362,9 @@ function updateHOCHostEl({ vnode, parent }, el) {
16922
17362
  break;
16923
17363
  }
16924
17364
  }
17365
+ if (suspense && suspense.activeBranch === vnode) {
17366
+ suspense.vnode.el = el;
17367
+ }
16925
17368
  }
16926
17369
 
16927
17370
  const internalObjectProto = {};
@@ -17790,10 +18233,17 @@ function baseCreateRenderer(options, createHydrationFns) {
17790
18233
  }
17791
18234
  hostInsert(el, container, anchor);
17792
18235
  if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) {
18236
+ const isHmr = true && isHmrUpdating;
17793
18237
  queuePostRenderEffect(() => {
17794
- vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
17795
- needCallTransitionHooks && transition.enter(el);
17796
- dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
18238
+ let prev;
18239
+ if (true) prev = setHmrUpdating(isHmr);
18240
+ try {
18241
+ vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
18242
+ needCallTransitionHooks && transition.enter(el);
18243
+ dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
18244
+ } finally {
18245
+ if (true) setHmrUpdating(prev);
18246
+ }
17797
18247
  }, parentSuspense);
17798
18248
  }
17799
18249
  };
@@ -18713,7 +19163,8 @@ function baseCreateRenderer(options, createHydrationFns) {
18713
19163
  shapeFlag,
18714
19164
  patchFlag,
18715
19165
  dirs,
18716
- cacheIndex
19166
+ cacheIndex,
19167
+ memo
18717
19168
  } = vnode;
18718
19169
  if (patchFlag === -2) {
18719
19170
  optimized = false;
@@ -18775,10 +19226,14 @@ function baseCreateRenderer(options, createHydrationFns) {
18775
19226
  remove(vnode);
18776
19227
  }
18777
19228
  }
18778
- if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs) {
19229
+ const shouldInvalidateMemo = memo != null && cacheIndex == null;
19230
+ if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted) || shouldInvokeDirs || shouldInvalidateMemo) {
18779
19231
  queuePostRenderEffect(() => {
18780
19232
  vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
18781
19233
  shouldInvokeDirs && invokeDirectiveHook(vnode, null, parentComponent, "unmounted");
19234
+ if (shouldInvalidateMemo) {
19235
+ vnode.el = null;
19236
+ }
18782
19237
  }, parentSuspense);
18783
19238
  }
18784
19239
  };
@@ -19332,6 +19787,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19332
19787
  pendingId: suspenseId++,
19333
19788
  timeout: typeof timeout === "number" ? timeout : -1,
19334
19789
  activeBranch: null,
19790
+ isFallbackMountPending: false,
19335
19791
  pendingBranch: null,
19336
19792
  isInFallback: !isHydrating,
19337
19793
  isHydrating,
@@ -19381,7 +19837,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19381
19837
  }
19382
19838
  };
19383
19839
  }
19384
- if (activeBranch) {
19840
+ if (activeBranch && !suspense.isFallbackMountPending) {
19385
19841
  if (parentNode(activeBranch.el) === container2) {
19386
19842
  anchor = next(activeBranch);
19387
19843
  }
@@ -19394,6 +19850,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19394
19850
  move(pendingBranch, container2, anchor, 0);
19395
19851
  }
19396
19852
  }
19853
+ suspense.isFallbackMountPending = false;
19397
19854
  setActiveBranch(suspense, pendingBranch);
19398
19855
  suspense.pendingBranch = null;
19399
19856
  suspense.isInFallback = false;
@@ -19429,6 +19886,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19429
19886
  triggerEvent(vnode2, "onFallback");
19430
19887
  const anchor2 = next(activeBranch);
19431
19888
  const mountFallback = () => {
19889
+ suspense.isFallbackMountPending = false;
19432
19890
  if (!suspense.isInFallback) {
19433
19891
  return;
19434
19892
  }
@@ -19448,6 +19906,7 @@ function createSuspenseBoundary(vnode, parentSuspense, parentComponent, containe
19448
19906
  };
19449
19907
  const delayEnter = fallbackVNode.transition && fallbackVNode.transition.mode === "out-in";
19450
19908
  if (delayEnter) {
19909
+ suspense.isFallbackMountPending = true;
19451
19910
  activeBranch.transition.afterLeave = mountFallback;
19452
19911
  }
19453
19912
  suspense.isInFallback = true;
@@ -19998,6 +20457,10 @@ function mergeProps(...args) {
19998
20457
  const incoming = toMerge[key];
19999
20458
  if (incoming && existing !== incoming && !((0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isArray)(existing) && existing.includes(incoming))) {
20000
20459
  ret[key] = existing ? [].concat(existing, incoming) : incoming;
20460
+ } else if (incoming == null && existing == null && // mergeProps({ 'onUpdate:modelValue': undefined }) should not retain
20461
+ // the model listener.
20462
+ !(0,_vue_shared__WEBPACK_IMPORTED_MODULE_1__.isModelListener)(key)) {
20463
+ ret[key] = incoming;
20001
20464
  }
20002
20465
  } else if (key !== "") {
20003
20466
  ret[key] = toMerge[key];
@@ -20684,7 +21147,7 @@ function isMemoSame(cached, memo) {
20684
21147
  return true;
20685
21148
  }
20686
21149
 
20687
- const version = "3.5.30";
21150
+ const version = "3.5.31";
20688
21151
  const warn = true ? warn$1 : 0;
20689
21152
  const ErrorTypeStrings = ErrorTypeStrings$1 ;
20690
21153
  const devtools = true ? devtools$1 : 0;
@@ -20895,7 +21358,7 @@ __webpack_require__.r(__webpack_exports__);
20895
21358
  /* 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
21359
  /* harmony import */ var _vue_runtime_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @vue/shared */ "./node_modules/@vue/shared/dist/shared.esm-bundler.js");
20897
21360
  /**
20898
- * @vue/runtime-dom v3.5.30
21361
+ * @vue/runtime-dom v3.5.31
20899
21362
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
20900
21363
  * @license MIT
20901
21364
  **/
@@ -22472,7 +22935,8 @@ const vModelText = {
22472
22935
  if (elValue === newValue) {
22473
22936
  return;
22474
22937
  }
22475
- if (document.activeElement === el && el.type !== "range") {
22938
+ const rootNode = el.getRootNode();
22939
+ if ((rootNode instanceof Document || rootNode instanceof ShadowRoot) && rootNode.activeElement === el && el.type !== "range") {
22476
22940
  if (lazy && value === oldValue) {
22477
22941
  return;
22478
22942
  }
@@ -22966,7 +23430,7 @@ __webpack_require__.r(__webpack_exports__);
22966
23430
  /* harmony export */ toTypeString: () => (/* binding */ toTypeString)
22967
23431
  /* harmony export */ });
22968
23432
  /**
22969
- * @vue/shared v3.5.30
23433
+ * @vue/shared v3.5.31
22970
23434
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
22971
23435
  * @license MIT
22972
23436
  **/
@@ -48489,7 +48953,7 @@ __webpack_require__.r(__webpack_exports__);
48489
48953
  /* 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
48954
  /* 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
48955
  /**
48492
- * vue v3.5.30
48956
+ * vue v3.5.31
48493
48957
  * (c) 2018-present Yuxi (Evan) You and Vue contributors
48494
48958
  * @license MIT
48495
48959
  **/
@@ -50160,7 +50624,7 @@ module.exports = ".list-mixed pre {\n max-height: 6.5em;\n max-width: 30em;\n}
50160
50624
  (module) {
50161
50625
 
50162
50626
  "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 ";
50627
+ module.exports = "<div class=\"list-mixed tooltip\">\n <pre>\n <code ref=\"MixedCode\" class=\"language-javascript\">{{shortenValue}}</code>\n </pre>\n</div>\n ";
50164
50628
 
50165
50629
  /***/ },
50166
50630
 
@@ -50182,7 +50646,7 @@ module.exports = ".list-string {\n display: inline;\n max-width: 300px;\n}";
50182
50646
  (module) {
50183
50647
 
50184
50648
  "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>";
50649
+ module.exports = "<div class=\"list-string tooltip\" ref=\"itemData\">\n {{displayValue}}\n</div>";
50186
50650
 
50187
50651
  /***/ },
50188
50652
 
@@ -50259,7 +50723,7 @@ module.exports = "<div v-if=\"show\" class=\"fixed inset-0 z-[9999]\">\n <div c
50259
50723
  (module) {
50260
50724
 
50261
50725
  "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";
50726
+ 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
50727
 
50264
50728
  /***/ },
50265
50729
 
@@ -50270,7 +50734,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
50270
50734
  (module) {
50271
50735
 
50272
50736
  "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";
50737
+ 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 Projection\n </async-button>\n </div>\n </div>\n </div>\n <span class=\"isolate inline-flex rounded-md shadow-sm\">\n <button\n @click=\"setOutputType('table')\"\n type=\"button\"\n class=\"relative inline-flex items-center rounded-none rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/table.svg\">\n </button>\n <button\n @click=\"setOutputType('json')\"\n type=\"button\"\n class=\"relative -ml-px inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-page focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-surface'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n <button\n @click=\"setOutputType('map')\"\n :disabled=\"geoJsonFields.length === 0\"\n type=\"button\"\n :title=\"geoJsonFields.length > 0 ? 'Map view' : 'No GeoJSON fields detected'\"\n class=\"relative -ml-px inline-flex items-center rounded-none rounded-r-md px-2 py-2 ring-1 ring-inset ring-gray-300 focus:z-10\"\n :class=\"[\n geoJsonFields.length === 0 ? 'text-gray-300 cursor-not-allowed bg-muted' : 'text-gray-400 hover:bg-page',\n outputType === 'map' ? 'bg-gray-200' : (geoJsonFields.length > 0 ? 'bg-surface' : '')\n ]\">\n <svg class=\"h-5 w-5\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 6.75V15m6-6v8.25m.503 3.498 4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 0 0-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0Z\" />\n </svg>\n </button>\n </span>\n </div>\n <div v-if=\"isProjectionMenuSelected && (outputType === 'table' || outputType === 'json')\" class=\"flex items-center gap-2 w-full mt-2 flex-shrink-0\">\n <input\n ref=\"projectionInput\"\n v-model=\"projectionText\"\n type=\"text\"\n placeholder=\"Projection: name email, or -password\"\n class=\"flex-1 min-w-0 rounded border border-edge bg-surface px-2 py-1.5 text-sm font-mono placeholder:text-content-tertiary focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary\"\n @focus=\"initProjection($event)\"\n @keydown.enter=\"applyProjectionFromInput()\"\n />\n </div>\n </div>\n <!-- In JSON view, this container is the scrollable element used for infinite scroll. -->\n <div class=\"documents-container relative\" ref=\"documentsContainerScroll\" @scroll=\"checkIfScrolledToBottom\">\n <div v-if=\"error\">\n <div class=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md\" role=\"alert\">\n <span class=\"block font-bold\">Error</span>\n <span class=\"block\">{{ error }}</span>\n </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
50738
 
50275
50739
  /***/ },
50276
50740
 
@@ -50314,7 +50778,7 @@ module.exports = "<div class=\"w-full h-full flex items-center justify-center\">
50314
50778
  (module) {
50315
50779
 
50316
50780
  "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";
50781
+ 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
50782
 
50319
50783
  /***/ },
50320
50784
 
@@ -50325,7 +50789,7 @@ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'
50325
50789
  (module) {
50326
50790
 
50327
50791
  "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";
50792
+ 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
50793
 
50330
50794
  /***/ },
50331
50795
 
@@ -50336,7 +50800,7 @@ module.exports = "<div class=\"p-4 space-y-6\">\n <div v-if=\"status === 'init'
50336
50800
  (module) {
50337
50801
 
50338
50802
  "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";
50803
+ 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
50804
 
50341
50805
  /***/ },
50342
50806
 
@@ -61672,7 +62136,7 @@ var src_default = VueToastificationPlugin;
61672
62136
  (module) {
61673
62137
 
61674
62138
  "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"}}');
62139
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.3.5","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
62140
 
61677
62141
  /***/ }
61678
62142