@mongoosejs/studio 0.0.127 → 0.0.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/backend/actions/Model/addField.js +54 -0
  2. package/backend/actions/Model/exportQueryResults.js +0 -1
  3. package/backend/actions/Model/getDocument.js +3 -1
  4. package/backend/actions/Model/getDocuments.js +14 -5
  5. package/backend/actions/Model/getDocumentsStream.js +14 -5
  6. package/backend/actions/Model/index.js +1 -0
  7. package/backend/actions/Model/updateDocument.js +23 -6
  8. package/backend/actions/Model/updateDocuments.js +23 -5
  9. package/frontend/public/app.js +613 -45
  10. package/frontend/public/tw.css +219 -2
  11. package/frontend/src/api.js +6 -0
  12. package/frontend/src/document/document.css +8 -0
  13. package/frontend/src/document/document.html +102 -8
  14. package/frontend/src/document/document.js +34 -1
  15. package/frontend/src/document-details/document-details.css +98 -0
  16. package/frontend/src/document-details/document-details.html +231 -19
  17. package/frontend/src/document-details/document-details.js +328 -3
  18. package/frontend/src/document-details/document-property/document-property.css +15 -0
  19. package/frontend/src/document-details/document-property/document-property.html +75 -31
  20. package/frontend/src/document-details/document-property/document-property.js +43 -2
  21. package/frontend/src/edit-boolean/edit-boolean.html +47 -0
  22. package/frontend/src/edit-boolean/edit-boolean.js +38 -0
  23. package/frontend/src/models/models.js +79 -30
  24. package/frontend/src/mothership.js +4 -0
  25. package/frontend/src/navbar/navbar.html +1 -1
  26. package/frontend/src/team/team.html +63 -4
  27. package/frontend/src/team/team.js +47 -1
  28. package/package.json +1 -1
@@ -108,6 +108,34 @@ module.exports = app => app.component('models', {
108
108
  this.autocompleteTrie.bulkInsert(paths, 10);
109
109
  }
110
110
  },
111
+ buildDocumentFetchParams(options = {}) {
112
+ const params = {
113
+ model: this.currentModel,
114
+ limit
115
+ };
116
+
117
+ if (typeof options.skip === 'number') {
118
+ params.skip = options.skip;
119
+ }
120
+
121
+ const sortKeys = Object.keys(this.sortBy);
122
+ if (sortKeys.length > 0) {
123
+ const key = sortKeys[0];
124
+ if (typeof key === 'string' && key.length > 0) {
125
+ params.sortKey = key;
126
+ const direction = this.sortBy[key];
127
+ if (direction !== undefined && direction !== null) {
128
+ params.sortDirection = direction;
129
+ }
130
+ }
131
+ }
132
+
133
+ if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
134
+ params.searchText = this.searchText;
135
+ }
136
+
137
+ return params;
138
+ },
111
139
  async initSearchFromUrl() {
112
140
  this.status = 'loading';
113
141
  this.query = Object.assign({}, this.$route.query); // important that this is here before the if statements
@@ -151,13 +179,48 @@ module.exports = app => app.component('models', {
151
179
  const before = this.searchText.slice(0, cursorPos);
152
180
  const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
153
181
  if (match && match[1]) {
154
- const term = match[1].replace(/["']/g, '');
182
+ const token = match[1];
183
+ const leadingQuoteMatch = token.match(/^["']/);
184
+ const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
185
+ ? token[token.length - 1]
186
+ : '';
187
+ const term = token
188
+ .replace(/^["']/, '')
189
+ .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
190
+ .trim();
155
191
  if (!term) {
156
192
  this.autocompleteSuggestions = [];
157
193
  return;
158
194
  }
159
195
  if (this.autocompleteTrie) {
160
- this.autocompleteSuggestions = this.autocompleteTrie.getSuggestions(term, 10);
196
+ const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10);
197
+ const suggestionsSet = new Set(primarySuggestions);
198
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
199
+ for (const schemaPath of this.schemaPaths) {
200
+ const path = schemaPath?.path;
201
+ if (
202
+ typeof path === 'string' &&
203
+ path.startsWith(`${term}.`) &&
204
+ !suggestionsSet.has(path)
205
+ ) {
206
+ suggestionsSet.add(path);
207
+ if (suggestionsSet.size >= 10) {
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ let suggestions = Array.from(suggestionsSet);
214
+ if (leadingQuoteMatch) {
215
+ const leadingQuote = leadingQuoteMatch[0];
216
+ suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
217
+ }
218
+ if (trailingQuoteMatch) {
219
+ suggestions = suggestions.map(suggestion =>
220
+ suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
221
+ );
222
+ }
223
+ this.autocompleteSuggestions = suggestions;
161
224
  this.autocompleteIndex = 0;
162
225
  return;
163
226
  }
@@ -194,9 +257,18 @@ module.exports = app => app.component('models', {
194
257
  }
195
258
  const token = match[1];
196
259
  const start = cursorPos - token.length;
197
- this.searchText = this.searchText.slice(0, start) + suggestion + after;
260
+ let replacement = suggestion;
261
+ const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
262
+ const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
263
+ if (leadingQuote && !replacement.startsWith(leadingQuote)) {
264
+ replacement = `${leadingQuote}${replacement}`;
265
+ }
266
+ if (trailingQuote && !replacement.endsWith(trailingQuote)) {
267
+ replacement = `${replacement}${trailingQuote}`;
268
+ }
269
+ this.searchText = this.searchText.slice(0, start) + replacement + after;
198
270
  this.$nextTick(() => {
199
- const pos = start + suggestion.length;
271
+ const pos = start + replacement.length;
200
272
  input.setSelectionRange(pos, pos);
201
273
  });
202
274
  this.autocompleteSuggestions = [];
@@ -245,15 +317,7 @@ module.exports = app => app.component('models', {
245
317
  const container = this.$refs.documentsList;
246
318
  if (container.scrollHeight - container.clientHeight - 100 < container.scrollTop) {
247
319
  this.status = 'loading';
248
- const params = {
249
- model: this.currentModel,
250
- sort: this.sortBy,
251
- skip: this.documents.length,
252
- limit
253
- };
254
- if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
255
- params.searchText = this.searchText;
256
- }
320
+ const params = this.buildDocumentFetchParams({ skip: this.documents.length });
257
321
  const { docs } = await api.Model.getDocuments(params);
258
322
  if (docs.length < limit) {
259
323
  this.loadedAllDocs = true;
@@ -322,14 +386,7 @@ module.exports = app => app.component('models', {
322
386
  let schemaPathsReceived = false;
323
387
 
324
388
  // Use async generator to stream SSEs
325
- const params = {
326
- model: this.currentModel,
327
- sort: this.sortBy,
328
- limit
329
- };
330
- if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
331
- params.searchText = this.searchText;
332
- }
389
+ const params = this.buildDocumentFetchParams();
333
390
  for await (const event of api.Model.getDocumentsStream(params)) {
334
391
  if (event.schemaPaths && !schemaPathsReceived) {
335
392
  // Sort schemaPaths with _id first
@@ -373,15 +430,7 @@ module.exports = app => app.component('models', {
373
430
  let numDocsReceived = false;
374
431
 
375
432
  // Use async generator to stream SSEs
376
- const params = {
377
- model: this.currentModel,
378
- sort: this.sortBy,
379
- skip: this.documents.length,
380
- limit
381
- };
382
- if (typeof this.searchText === 'string' && this.searchText.trim().length > 0) {
383
- params.searchText = this.searchText;
384
- }
433
+ const params = this.buildDocumentFetchParams({ skip: this.documents.length });
385
434
  for await (const event of api.Model.getDocumentsStream(params)) {
386
435
  if (event.numDocs !== undefined && !numDocsReceived) {
387
436
  this.numDocuments = event.numDocs;
@@ -54,4 +54,8 @@ exports.removeFromWorkspace = function removeFromWorkspace(params) {
54
54
  return client.post('/removeFromWorkspace', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id, ...params }).then(res => res.data);
55
55
  };
56
56
 
57
+ exports.updateWorkspaceMember = function updateWorkspaceMember(params) {
58
+ return client.post('/updateWorkspaceMember', { workspaceId: window.MONGOOSE_STUDIO_CONFIG.workspace._id, ...params }).then(res => res.data);
59
+ };
60
+
57
61
  exports.hasAPIKey = client.hasAPIKey;
@@ -57,7 +57,7 @@
57
57
  </button>
58
58
  </div>
59
59
 
60
- <div v-if="showFlyout" class="absolute right-0 z-10 top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
60
+ <div v-if="showFlyout" class="absolute right-0 z-[100] top-[90%] w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
61
61
  <router-link to="/team" v-if="hasAccess(roles, 'team')" @click="showFlyout = false" class="cursor-pointer block px-4 py-2 text-sm text-gray-700 hover:bg-ultramarine-200" role="menuitem" tabindex="-1" id="user-menu-item-2">Team</router-link>
62
62
  <span v-else class="block px-4 py-2 text-sm text-gray-300 cursor-not-allowed" role="menuitem" tabindex="-1" id="user-menu-item-2">
63
63
  Team
@@ -64,9 +64,13 @@
64
64
  <div class="hidden shrink-0 sm:flex sm:flex-col sm:items-end">
65
65
  <p class="text-sm/6 text-gray-900 capitalize">{{getRolesForUser(user).join(', ')}}</p>
66
66
  <div class="flex gap-3">
67
- <p class="mt-1 text-xs/5 text-gray-500 cursor-pointer">
67
+ <button
68
+ type="button"
69
+ class="mt-1 text-xs/5 text-gray-500 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300"
70
+ :disabled="getRolesForUser(user).includes('owner')"
71
+ @click="openEditModal(user)">
68
72
  Edit
69
- </p>
73
+ </button>
70
74
  <button
71
75
  class="mt-1 text-xs/5 text-valencia-500 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300"
72
76
  :disabled="getRolesForUser(user).includes('owner')"
@@ -152,15 +156,70 @@
152
156
  </template>
153
157
  </modal>
154
158
 
159
+ <modal v-if="showEditModal">
160
+ <template v-slot:body>
161
+ <div class="modal-exit" @click="closeEditModal">&times;</div>
162
+ <div class="p-1 space-y-4">
163
+ <div class="text-lg font-bold">
164
+ Edit Member
165
+ </div>
166
+
167
+ <div>
168
+ <div class="text-sm/6 font-semibold text-gray-900">
169
+ {{showEditModal.user.name || showEditModal.user.githubUsername}}
170
+ </div>
171
+ <div class="text-xs/5 text-gray-500">
172
+ {{showEditModal.user.email ?? 'No Email'}}
173
+ </div>
174
+ </div>
175
+
176
+ <div>
177
+ <label for="editRole" class="block text-sm/6 font-medium text-gray-900">Role</label>
178
+ <div class="mt-2 grid grid-cols-1">
179
+ <select
180
+ id="editRole"
181
+ name="editRole"
182
+ v-model="showEditModal.role"
183
+ class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pl-3 pr-8 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 focus:outline focus:outline-2 focus:-outline-offset-2 focus:outline-ultramarine-600 sm:text-sm/6">
184
+ <option value="admin" :disabled="disableRoleOption('admin')">Admin</option>
185
+ <option value="member" :disabled="disableRoleOption('member')">Member</option>
186
+ <option value="readonly" :disabled="disableRoleOption('readonly')">Read-only</option>
187
+ <option value="dashboards" :disabled="disableRoleOption('dashboards')">Dashboards Only</option>
188
+ </select>
189
+ <svg class="pointer-events-none col-start-1 row-start-1 mr-2 size-5 self-center justify-self-end text-gray-500 sm:size-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true" data-slot="icon">
190
+ <path fill-rule="evenodd" d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" />
191
+ </svg>
192
+ </div>
193
+ <div v-if="!workspace?.subscriptionTier" class="mt-2 text-sm text-gray-700">
194
+ You can only assign the "Dashboards Only" role until you activate a subscription.
195
+ </div>
196
+ </div>
197
+
198
+ <div class="mt-6 grid grid-cols-2 gap-4">
199
+ <async-button
200
+ @click="updateWorkspaceMember"
201
+ :disabled="showEditModal.role === showEditModal.originalRole"
202
+ class="border-0 mt-0 flex w-full items-center justify-center gap-3 rounded-md bg-ultramarine-600 hover:bg-ultramarine-500 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-500">
203
+ <span class="text-sm font-semibold leading-6">Save</span>
204
+ </async-button>
205
+
206
+ <span @click="closeEditModal" class="cursor-pointer flex w-full items-center justify-center gap-3 rounded-md bg-slate-500 hover:bg-slate-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-slate-400">
207
+ <span class="text-sm font-semibold leading-6">Cancel</span>
208
+ </span>
209
+ </div>
210
+ </div>
211
+ </template>
212
+ </modal>
213
+
155
214
  <modal v-if="showRemoveModal">
156
215
  <template v-slot:body>
157
- <div class="modal-exit" @click="showRemoveModal = false">&times;</div>
216
+ <div class="modal-exit" @click="showRemoveModal = null">&times;</div>
158
217
  <div>
159
218
  Are you sure you want to remove user <span class="font-bold">{{showRemoveModal.githubUsername}}</span> from this workspace?
160
219
  </div>
161
220
  <div class="mt-6 grid grid-cols-2 gap-4">
162
221
  <async-button
163
- @click="removeFromWorkspace(showConfirmDeleteModal)"
222
+ @click="removeFromWorkspace"
164
223
  class="border-0 mt-0 flex w-full items-center justify-center gap-3 rounded-md bg-valencia-500 hover:bg-valencia-400 px-3 py-1.5 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-400">
165
224
  <span class="text-sm font-semibold leading-6">Yes, Remove</span>
166
225
  </async-button>
@@ -11,6 +11,7 @@ module.exports = app => app.component('team', {
11
11
  invitations: null,
12
12
  showNewInvitationModal: false,
13
13
  showRemoveModal: null,
14
+ showEditModal: null,
14
15
  status: 'loading'
15
16
  }),
16
17
  async mounted() {
@@ -32,15 +33,60 @@ module.exports = app => app.component('team', {
32
33
  getRolesForUser(user) {
33
34
  return this.workspace.members.find(member => member.userId === user._id)?.roles ?? [];
34
35
  },
36
+ openEditModal(user) {
37
+ if (this.getRolesForUser(user).includes('owner')) {
38
+ return;
39
+ }
40
+
41
+ const roles = this.getRolesForUser(user);
42
+ const nonOwnerRoles = roles.filter(role => role !== 'owner');
43
+ const currentRole = nonOwnerRoles[0] ?? null;
44
+ const editableRole = currentRole ?? (this.workspace?.subscriptionTier ? 'member' : 'dashboards');
45
+
46
+ this.showEditModal = {
47
+ user,
48
+ role: editableRole,
49
+ originalRole: currentRole
50
+ };
51
+ },
52
+ closeEditModal() {
53
+ this.showEditModal = null;
54
+ },
55
+ async updateWorkspaceMember() {
56
+ if (this.showEditModal.role === this.showEditModal.originalRole) {
57
+ this.closeEditModal();
58
+ return;
59
+ }
60
+
61
+ const { workspace, users } = await mothership.updateWorkspaceMember({
62
+ userId: this.showEditModal.user._id,
63
+ roles: [this.showEditModal.role]
64
+ });
65
+
66
+ this.workspace = workspace;
67
+ this.users = users;
68
+ this.closeEditModal();
69
+ },
35
70
  async removeFromWorkspace() {
36
71
  const { workspace, users } = await mothership.removeFromWorkspace({ userId: this.showRemoveModal._id });
37
72
  this.workspace = workspace;
38
73
  this.users = users;
39
- this.showRemoveModal = false;
74
+ this.showRemoveModal = null;
40
75
  },
41
76
  async getWorkspaceCustomerPortalLink() {
42
77
  const { url } = await mothership.getWorkspaceCustomerPortalLink();
43
78
  window.open(url, '_self');
79
+ },
80
+ disableRoleOption(option) {
81
+ if (this.workspace?.subscriptionTier) {
82
+ return false;
83
+ }
84
+
85
+ if (this.showEditModal?.originalRole === option) {
86
+ return false;
87
+ }
88
+
89
+ return option !== 'dashboards';
44
90
  }
45
91
  }
46
92
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.127",
3
+ "version": "0.0.129",
4
4
  "description": "A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.",
5
5
  "homepage": "https://studio.mongoosejs.io/",
6
6
  "repository": {