@mongoosejs/studio 0.3.4 → 0.3.6

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.
@@ -719,10 +719,6 @@ video {
719
719
  right: 0.5rem;
720
720
  }
721
721
 
722
- .right-4 {
723
- right: 1rem;
724
- }
725
-
726
722
  .top-0 {
727
723
  top: 0px;
728
724
  }
@@ -3328,6 +3324,11 @@ video {
3328
3324
  color: var(--color-primary);
3329
3325
  }
3330
3326
 
3327
+ .hover\:text-red-900:hover {
3328
+ --tw-text-opacity: 1;
3329
+ color: rgb(127 29 29 / var(--tw-text-opacity));
3330
+ }
3331
+
3331
3332
  .hover\:text-slate-700:hover {
3332
3333
  --tw-text-opacity: 1;
3333
3334
  color: rgb(51 65 85 / var(--tw-text-opacity));
@@ -10,17 +10,6 @@
10
10
  <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" />
11
11
  </svg>
12
12
  </button>
13
- <button
14
- class="fixed top-[65px] right-4 z-10 p-2 rounded-md shadow bg-surface"
15
- :class="hasWorkspace ? 'text-content-secondary hover:bg-muted' : 'text-gray-300 cursor-not-allowed bg-page'"
16
- @click="toggleShareThread"
17
- :disabled="!hasWorkspace || !chatThreadId || sharingThread"
18
- aria-label="Share thread with workspace"
19
- title="Share thread with workspace"
20
- >
21
- <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
22
- <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
23
- </button>
24
13
  <!-- Sidebar: Chat Threads -->
25
14
  <aside
26
15
  class="bg-page border-r overflow-hidden h-full transition-all duration-300 ease-in-out z-20 w-64 fixed lg:relative shrink-0 flex flex-col"
@@ -84,6 +73,18 @@
84
73
  <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" />
85
74
  </svg>
86
75
  </button>
76
+ <button
77
+ type="button"
78
+ @click="hasWorkspace ? toggleShareThread() : (showProUpgradeModal = true)"
79
+ class="rounded p-1.5"
80
+ :class="hasWorkspace && chatThreadId ? 'text-gray-400 hover:text-gray-600 hover:bg-muted' : !hasWorkspace ? 'text-gray-400 hover:text-gray-600 hover:bg-muted' : 'text-gray-300 cursor-not-allowed'"
81
+ :disabled="hasWorkspace && !chatThreadId"
82
+ aria-label="Share thread with workspace"
83
+ :title="'Share thread with workspace' + (!hasWorkspace ? ' (requires a pro workspace)' : !chatThreadId ? ': Open a thread first!' : '')"
84
+ >
85
+ <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
86
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
87
+ </button>
87
88
  </div>
88
89
  </aside>
89
90
 
@@ -118,7 +119,7 @@
118
119
  @input="adjustTextareaHeight"
119
120
  @keydown.enter.exact.prevent="handleEnter"
120
121
  ></textarea>
121
- <button class="bg-blue-600 text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
122
+ <button class="bg-primary hover:bg-primary-hover text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
122
123
  <svg v-if="sendingMessage" style="height: 1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
123
124
  <g>
124
125
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity="0.3" />
@@ -132,4 +133,10 @@
132
133
  </form>
133
134
  </div>
134
135
  </main>
136
+
137
+ <pro-upgrade-modal
138
+ :show="showProUpgradeModal"
139
+ feature-description="Sharing threads lets you collaborate with your team by sharing chat threads within your workspace."
140
+ @close="showProUpgradeModal = false"
141
+ ></pro-upgrade-modal>
135
142
  </div>
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
+ const getCurrentDateTimeContext = require('../getCurrentDateTimeContext');
4
5
  const template = require('./chat.html');
5
6
 
6
7
  module.exports = {
@@ -15,7 +16,8 @@ module.exports = {
15
16
  chatMessages: [],
16
17
  hideSidebar: null,
17
18
  sharingThread: false,
18
- threadSearch: ''
19
+ threadSearch: '',
20
+ showProUpgradeModal: false
19
21
  }),
20
22
  methods: {
21
23
  async sendMessage() {
@@ -44,7 +46,11 @@ module.exports = {
44
46
  }
45
47
  });
46
48
 
47
- const params = { chatThreadId: this.chatThreadId, content };
49
+ const params = {
50
+ chatThreadId: this.chatThreadId,
51
+ content,
52
+ currentDateTime: getCurrentDateTimeContext()
53
+ };
48
54
  let userChatMessage = null;
49
55
  let assistantChatMessage = null;
50
56
  for await (const event of api.ChatThread.streamChatMessage(params)) {
@@ -151,7 +157,7 @@ module.exports = {
151
157
  },
152
158
  async toggleShareThread() {
153
159
  if (!this.chatThreadId || !this.hasWorkspace) {
154
- return;
160
+ throw new Error('Cannot share thread: chatThreadId or hasWorkspace is missing');
155
161
  }
156
162
  this.sharingThread = true;
157
163
  try {
@@ -11,6 +11,7 @@ const ObjectId = new Proxy(BSON.ObjectId, {
11
11
  });
12
12
 
13
13
  const appendCSS = require('../appendCSS');
14
+ const getCurrentDateTimeContext = require('../getCurrentDateTimeContext');
14
15
 
15
16
  appendCSS(require('./create-document.css'));
16
17
 
@@ -49,7 +50,8 @@ module.exports = app => app.component('create-document', {
49
50
  for await (const event of api.Model.streamChatMessage({
50
51
  model: this.currentModel,
51
52
  content: prompt,
52
- documentData: this.aiOriginalDocument
53
+ documentData: this.aiOriginalDocument,
54
+ currentDateTime: getCurrentDateTimeContext()
53
55
  })) {
54
56
  if (event?.textPart) {
55
57
  this.aiSuggestion += event.textPart;
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const time = require('time-commando');
4
+
5
+ module.exports = function getCurrentDateTimeContext() {
6
+ const date = time.now();
7
+ const components = [
8
+ date.getFullYear(),
9
+ date.getMonth() + 1,
10
+ date.getDate(),
11
+ date.getHours(),
12
+ date.getMinutes(),
13
+ date.getSeconds()
14
+ ].map(num => num.toString().padStart(2, '0'));
15
+ const [yyyy, mm, dd, hh, mi, ss] = components;
16
+ return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}`;
17
+ };
@@ -28,13 +28,6 @@ module.exports = app => app.component('list-json', {
28
28
  topLevelExpanded: false
29
29
  };
30
30
  },
31
- watch: {
32
- value: {
33
- handler() {
34
- this.resetCollapse();
35
- }
36
- }
37
- },
38
31
  created() {
39
32
  this.resetCollapse();
40
33
  for (const field of this.expandedFields) {
@@ -7,5 +7,29 @@ appendCSS(require('./modal.css'));
7
7
 
8
8
  module.exports = app => app.component('modal', {
9
9
  template,
10
- props: ['containerClass']
10
+ props: ['containerClass'],
11
+ mounted() {
12
+ window.addEventListener('keydown', this.onEscape);
13
+ },
14
+ beforeUnmount() {
15
+ window.removeEventListener('keydown', this.onEscape);
16
+ },
17
+ methods: {
18
+ onEscape(event) {
19
+ if (event.key !== 'Escape') {
20
+ return;
21
+ }
22
+
23
+ const modalMasks = Array.from(document.querySelectorAll('.modal-mask'));
24
+ const currentMask = this.$el?.classList?.contains('modal-mask') ? this.$el : this.$el?.querySelector('.modal-mask') || this.$el;
25
+ const isTopMostModal = modalMasks.length > 0 && modalMasks[modalMasks.length - 1] === currentMask;
26
+
27
+ if (!isTopMostModal) {
28
+ return;
29
+ }
30
+
31
+ const closeButton = currentMask.querySelector('.modal-exit, [data-modal-close]');
32
+ closeButton?.click();
33
+ }
34
+ }
11
35
  });
@@ -41,6 +41,9 @@ module.exports = app => app.component('document-search', {
41
41
  created() {
42
42
  this.buildAutocompleteTrie();
43
43
  },
44
+ mounted() {
45
+ this.$refs.searchInput.focus();
46
+ },
44
47
  methods: {
45
48
  emitSearch() {
46
49
  this.$emit('input', this.searchText);
@@ -226,7 +226,7 @@
226
226
  @click="applyDefaultProjectionColumns()"
227
227
  class="block w-full px-4 py-2 text-left text-sm text-content-secondary hover:bg-muted"
228
228
  >
229
- Default
229
+ Default Projection
230
230
  </async-button>
231
231
  </div>
232
232
  </div>
@@ -280,6 +280,24 @@
280
280
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md" role="alert">
281
281
  <span class="block font-bold">Error</span>
282
282
  <span class="block">{{ error }}</span>
283
+ <span class="block mt-2">
284
+ Need help?
285
+ <a
286
+ href="https://discord.gg/P3YCfKYxpy"
287
+ target="_blank"
288
+ rel="noopener noreferrer"
289
+ class="underline font-medium text-red-800 hover:text-red-900"
290
+ >Ask in Discord</a>
291
+ or
292
+ <a
293
+ href="https://github.com/mongoosejs/studio/issues"
294
+ target="_blank"
295
+ rel="noopener noreferrer"
296
+ class="underline font-medium text-red-800 hover:text-red-900"
297
+ >
298
+ open a GitHub issue.
299
+ </a>
300
+ </span>
283
301
  </div>
284
302
  </div>
285
303
  <div v-else-if="outputType === 'table'" class="flex-1 min-h-0 flex flex-col overflow-hidden">
@@ -442,13 +460,15 @@
442
460
  selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:shadow-sm hover:border-slate-300 bg-surface'
443
461
  ]"
444
462
  >
445
- <button
446
- type="button"
463
+ <router-link
447
464
  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"
448
- @click.stop="openDocument(document)"
465
+ :to="{ path: '/model/' + currentModel + '/document/' + document._id, query: $route.query }"
466
+ target="_blank"
467
+ rel="noopener noreferrer"
468
+ @click.stop
449
469
  >
450
- Open this Document
451
- </button>
470
+ Open
471
+ </router-link>
452
472
  <list-json :value="filterDocument(document)" :references="referenceMap">
453
473
  </list-json>
454
474
  </div>
@@ -14,7 +14,6 @@ const limit = 20;
14
14
  const DEFAULT_FIRST_N_FIELDS = 6;
15
15
  const OUTPUT_TYPE_STORAGE_KEY = 'studio:model-output-type';
16
16
  const SELECTED_GEO_FIELD_STORAGE_KEY = 'studio:model-selected-geo-field';
17
- const PROJECTION_STORAGE_KEY_PREFIX = 'studio:model-projection:';
18
17
  const SHOW_ROW_NUMBERS_STORAGE_KEY = 'studio:model-show-row-numbers';
19
18
  const PROJECTION_MODE_QUERY_KEY = 'projectionMode';
20
19
  const RECENTLY_VIEWED_MODELS_KEY = 'studio:recently-viewed-models';
@@ -262,7 +261,7 @@ module.exports = app => app.component('models', {
262
261
  computed: {
263
262
  referenceMap() {
264
263
  const map = {};
265
- for (const path of this.filteredPaths) {
264
+ for (const path of this.schemaPaths) {
266
265
  if (path?.ref) {
267
266
  map[path.path] = path.ref;
268
267
  }
@@ -627,7 +626,7 @@ module.exports = app => app.component('models', {
627
626
  // mount/remount respects deep-linked projections before `filteredPaths`
628
627
  // is rehydrated from schema paths.
629
628
  let fieldsParam = normalizeFieldsParamForApi(this.query?.fields);
630
- if (!fieldsParam) {
629
+ if (!fieldsParam && this.isProjectionMenuSelected === true) {
631
630
  const fieldPaths = this.filteredPaths && this.filteredPaths.length > 0
632
631
  ? this.filteredPaths.map(p => p.path).filter(Boolean)
633
632
  : null;
@@ -678,7 +677,6 @@ module.exports = app => app.component('models', {
678
677
  this.filteredPaths = urlPaths.map(path => this.schemaPaths.find(p => p.path === path)).filter(Boolean);
679
678
  if (this.filteredPaths.length > 0) {
680
679
  this.syncProjectionFromPaths();
681
- this.saveProjectionPreference();
682
680
  }
683
681
  }
684
682
  }
@@ -827,6 +825,10 @@ module.exports = app => app.component('models', {
827
825
  }
828
826
  } else {
829
827
  delete this.query[PROJECTION_MODE_QUERY_KEY];
828
+ delete this.query.fields;
829
+ this.filteredPaths = [];
830
+ this.selectedPaths = [];
831
+ this.projectionText = '';
830
832
  }
831
833
 
832
834
  this.$router.push({ query: this.query });
@@ -966,34 +968,13 @@ module.exports = app => app.component('models', {
966
968
  for (const { path } of this.schemaPaths) {
967
969
  this.shouldExport[path] = true;
968
970
  }
969
- const shouldUseSavedProjection = this.isProjectionMenuSelected === true;
970
- const savedPaths = shouldUseSavedProjection ? this.loadProjectionPreference() : null;
971
- if (savedPaths === null) {
971
+ const isProjectionModeOn = this.isProjectionMenuSelected === true;
972
+ if (isProjectionModeOn) {
972
973
  this.applyDefaultProjection(event.suggestedFields);
973
- if (shouldUseSavedProjection) {
974
- this.saveProjectionPreference();
975
- }
976
- } else if (Array.isArray(savedPaths) && savedPaths.length === 0) {
974
+ } else {
977
975
  this.filteredPaths = [];
976
+ this.selectedPaths = [];
978
977
  this.projectionText = '';
979
- if (shouldUseSavedProjection) {
980
- this.saveProjectionPreference();
981
- }
982
- } else if (savedPaths && savedPaths.length > 0) {
983
- this.filteredPaths = savedPaths
984
- .map(path => this.schemaPaths.find(p => p.path === path))
985
- .filter(Boolean);
986
- if (this.filteredPaths.length === 0) {
987
- this.applyDefaultProjection(event.suggestedFields);
988
- if (shouldUseSavedProjection) {
989
- this.saveProjectionPreference();
990
- }
991
- }
992
- } else {
993
- this.applyDefaultProjection(event.suggestedFields);
994
- if (shouldUseSavedProjection) {
995
- this.saveProjectionPreference();
996
- }
997
978
  }
998
979
  this.selectedPaths = [...this.filteredPaths];
999
980
  this.syncProjectionFromPaths();
@@ -1073,36 +1054,6 @@ module.exports = app => app.component('models', {
1073
1054
  this.filteredPaths = this.schemaPaths.filter(p => p.path === '_id');
1074
1055
  }
1075
1056
  },
1076
- loadProjectionPreference() {
1077
- if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
1078
- return null;
1079
- }
1080
- const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
1081
- const stored = window.localStorage.getItem(key);
1082
- if (stored === null || stored === undefined) {
1083
- return null;
1084
- }
1085
- if (stored === '') {
1086
- return [];
1087
- }
1088
- try {
1089
- const parsed = JSON.parse(stored);
1090
- if (Array.isArray(parsed)) {
1091
- return parsed.map(x => String(x).trim()).filter(Boolean);
1092
- }
1093
- } catch (e) {
1094
- return null;
1095
- }
1096
- return null;
1097
- },
1098
- saveProjectionPreference() {
1099
- if (typeof window === 'undefined' || !window.localStorage || !this.currentModel) {
1100
- return;
1101
- }
1102
- const key = PROJECTION_STORAGE_KEY_PREFIX + this.currentModel;
1103
- const paths = this.filteredPaths.map(p => p.path);
1104
- window.localStorage.setItem(key, JSON.stringify(paths));
1105
- },
1106
1057
  clearProjection() {
1107
1058
  // Keep current filter input in sync with the URL so projection reset
1108
1059
  // does not unintentionally wipe the filter on remount.
@@ -1111,7 +1062,6 @@ module.exports = app => app.component('models', {
1111
1062
  this.selectedPaths = [];
1112
1063
  this.projectionText = '';
1113
1064
  this.updateProjectionQuery();
1114
- this.saveProjectionPreference();
1115
1065
  },
1116
1066
  resetFilter() {
1117
1067
  // Reuse the existing "apply filter + update URL" flow.
@@ -1131,7 +1081,6 @@ module.exports = app => app.component('models', {
1131
1081
  this.selectedPaths = [...this.filteredPaths];
1132
1082
  this.syncProjectionFromPaths();
1133
1083
  this.updateProjectionQuery();
1134
- this.saveProjectionPreference();
1135
1084
  },
1136
1085
  initProjection(ev) {
1137
1086
  if (!this.projectionText || !this.projectionText.trim()) {
@@ -1240,7 +1189,6 @@ module.exports = app => app.component('models', {
1240
1189
  this.selectedPaths = [...this.filteredPaths];
1241
1190
  this.syncProjectionFromPaths();
1242
1191
  this.updateProjectionQuery();
1243
- this.saveProjectionPreference();
1244
1192
  },
1245
1193
  updateProjectionQuery() {
1246
1194
  const paths = this.filteredPaths.map(x => x.path).filter(Boolean);
@@ -1270,7 +1218,6 @@ module.exports = app => app.component('models', {
1270
1218
  }
1271
1219
  this.syncProjectionFromPaths();
1272
1220
  this.updateProjectionQuery();
1273
- this.saveProjectionPreference();
1274
1221
  }
1275
1222
  },
1276
1223
  addField(schemaPath) {
@@ -1292,7 +1239,6 @@ module.exports = app => app.component('models', {
1292
1239
  });
1293
1240
  this.syncProjectionFromPaths();
1294
1241
  this.updateProjectionQuery();
1295
- this.saveProjectionPreference();
1296
1242
  this.showAddFieldDropdown = false;
1297
1243
  this.addFieldFilterText = '';
1298
1244
  }
@@ -42,7 +42,8 @@
42
42
  </svg>
43
43
  </span>
44
44
  <a
45
- :href="hasTaskVisualizer"
45
+ v-if="hasTaskVisualizer"
46
+ href="#/tasks"
46
47
  class="inline-flex items-center px-1 border-b-2 text-sm font-medium"
47
48
  :class="taskView ? 'text-content border-primary' : 'border-transparent text-content-tertiary hover:text-content'">
48
49
  Tasks
@@ -210,6 +211,10 @@
210
211
  <path fill-rule="evenodd" d="M10 2a4 4 0 00-4 4v2H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-1V6a4 4 0 00-4-4zm-3 6V6a3 3 0 116 0v2H7z" clip-rule="evenodd" />
211
212
  </svg>
212
213
  </span>
214
+ <a v-if="hasTaskVisualizer"
215
+ href="#/tasks"
216
+ class="block px-3 py-2 rounded-md text-base font-medium"
217
+ :class="taskView ? 'text-content bg-primary-subtle' : 'text-content-secondary hover:bg-muted'">Tasks</a>
213
218
  <div v-if="!user && hasAPIKey" class="mt-4">
214
219
  <button
215
220
  type="button"
@@ -67,12 +67,7 @@ module.exports = app => app.component('navbar', {
67
67
  return this.roles && this.roles[0] === 'dashboards' ? 'dashboards' : 'root';
68
68
  },
69
69
  hasTaskVisualizer() {
70
- if (window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer) {
71
- return '#/tasks';
72
- } else {
73
- return 'https://www.npmjs.com/package/@mongoosejs/task';
74
- }
75
-
70
+ return !!window.MONGOOSE_STUDIO_CONFIG.enableTaskVisualizer;
76
71
  }
77
72
  },
78
73
  methods: {
@@ -0,0 +1,38 @@
1
+ <modal v-if="show" containerClass="!max-w-md">
2
+ <template v-slot:body>
3
+ <div @keydown.esc="$emit('close')" tabindex="0" ref="overlay">
4
+ <div class="flex items-center justify-between mb-4">
5
+ <h3 class="text-lg font-semibold text-gray-900">Pro Feature</h3>
6
+ <button
7
+ type="button"
8
+ @click="$emit('close')"
9
+ class="text-gray-400 hover:text-gray-600 cursor-pointer"
10
+ aria-label="Close"
11
+ >
12
+ <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
14
+ </svg>
15
+ </button>
16
+ </div>
17
+ <p class="text-gray-600 mb-6">
18
+ {{ featureDescription }} Upgrade to a Pro workspace to unlock this feature.
19
+ </p>
20
+ <div class="flex justify-end gap-3">
21
+ <button
22
+ type="button"
23
+ @click="$emit('close')"
24
+ class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 cursor-pointer"
25
+ >
26
+ Cancel
27
+ </button>
28
+ <a
29
+ href="https://studio.mongoosejs.io/pro"
30
+ target="_blank"
31
+ class="px-4 py-2 text-sm font-medium text-white bg-primary rounded-md hover:bg-primary-hover inline-flex items-center"
32
+ >
33
+ Upgrade to Pro
34
+ </a>
35
+ </div>
36
+ </div>
37
+ </template>
38
+ </modal>
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ const template = require('./pro-upgrade-modal.html');
4
+
5
+ module.exports = app => app.component('pro-upgrade-modal', {
6
+ template,
7
+ props: {
8
+ show: { type: Boolean, default: false },
9
+ featureDescription: { type: String, default: 'This feature is available on the Pro plan.' }
10
+ },
11
+ emits: ['close'],
12
+ watch: {
13
+ show(val) {
14
+ if (val) {
15
+ this.$nextTick(() => {
16
+ if (this.$refs.overlay) {
17
+ this.$refs.overlay.focus();
18
+ }
19
+ });
20
+ }
21
+ }
22
+ }
23
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
5
5
  "homepage": "https://mongoosestudio.app/",
6
6
  "repository": {
@@ -25,6 +25,7 @@
25
25
  "vue": "3.x",
26
26
  "vue-toastification": "^2.0.0-rc.5",
27
27
  "webpack": "5.x",
28
+ "time-commando": "1.0.1",
28
29
  "xss": "^1.0.15"
29
30
  },
30
31
  "peerDependencies": {