@mongoosejs/studio 0.0.128 → 0.0.130
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.
- package/backend/actions/Model/addField.js +54 -0
- package/backend/actions/Model/getDocument.js +3 -1
- package/backend/actions/Model/getDocuments.js +14 -5
- package/backend/actions/Model/getDocumentsStream.js +14 -5
- package/backend/actions/Model/index.js +1 -0
- package/backend/actions/Model/updateDocument.js +23 -6
- package/backend/actions/Model/updateDocuments.js +23 -5
- package/frontend/public/app.js +613 -45
- package/frontend/public/tw.css +219 -2
- package/frontend/src/api.js +6 -0
- package/frontend/src/document/document.css +8 -0
- package/frontend/src/document/document.html +102 -8
- package/frontend/src/document/document.js +34 -1
- package/frontend/src/document-details/document-details.css +98 -0
- package/frontend/src/document-details/document-details.html +231 -19
- package/frontend/src/document-details/document-details.js +328 -3
- package/frontend/src/document-details/document-property/document-property.css +15 -0
- package/frontend/src/document-details/document-property/document-property.html +75 -31
- package/frontend/src/document-details/document-property/document-property.js +43 -2
- package/frontend/src/edit-boolean/edit-boolean.html +47 -0
- package/frontend/src/edit-boolean/edit-boolean.js +38 -0
- package/frontend/src/models/models.js +79 -30
- package/frontend/src/mothership.js +4 -0
- package/frontend/src/navbar/navbar.html +1 -1
- package/frontend/src/team/team.html +63 -4
- package/frontend/src/team/team.js +47 -1
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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 +
|
|
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-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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">×</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 =
|
|
216
|
+
<div class="modal-exit" @click="showRemoveModal = null">×</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
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "0.0.130",
|
|
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": {
|