@mongoosejs/studio 0.4.0 → 0.5.0

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 (39) hide show
  1. package/README.md +9 -0
  2. package/backend/actions/ChatMessage/executeScript.js +17 -25
  3. package/backend/actions/Model/getDocuments.js +7 -4
  4. package/backend/actions/Model/getDocumentsStream.js +7 -4
  5. package/backend/actions/getCapabilities.js +42 -0
  6. package/backend/actions/index.js +1 -1
  7. package/backend/db/chatMessageSchema.js +2 -1
  8. package/backend/helpers/isBindIPConnection.js +8 -0
  9. package/backend/helpers/isLocalhostConnection.js +8 -0
  10. package/backend/helpers/normalizeBindIPOption.js +16 -0
  11. package/backend/helpers/parseProjectionParam.js +180 -0
  12. package/backend/sandbox/createSandbox.js +83 -0
  13. package/backend/sandbox/createScriptDb.js +201 -0
  14. package/express.js +16 -0
  15. package/frontend/public/app.js +845 -114
  16. package/frontend/public/tw.css +4 -0
  17. package/frontend/src/api.js +6 -0
  18. package/frontend/src/chat/chat-message/chat-message.html +1 -0
  19. package/frontend/src/chat/chat-message/chat-message.js +4 -3
  20. package/frontend/src/chat/chat-message-script/chat-message-script.html +98 -16
  21. package/frontend/src/chat/chat-message-script/chat-message-script.js +77 -17
  22. package/frontend/src/chat/chat.html +5 -1
  23. package/frontend/src/chat/chat.js +11 -2
  24. package/frontend/src/detail-objectid/detail-objectid.html +1 -0
  25. package/frontend/src/detail-objectid/detail-objectid.js +67 -0
  26. package/frontend/src/document-details/document-property/document-property.html +8 -3
  27. package/frontend/src/document-details/document-property/document-property.js +7 -0
  28. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.html +12 -0
  29. package/frontend/src/document-details/objectid-view-mode-picker/objectid-view-mode-picker.js +19 -0
  30. package/frontend/src/modal/modal.css +0 -1
  31. package/frontend/src/models/document-search/document-search.js +6 -0
  32. package/frontend/src/models/models.html +6 -8
  33. package/frontend/src/models/models.js +257 -85
  34. package/frontend/src/models/projection-search/projection-search.html +27 -0
  35. package/frontend/src/models/projection-search/projection-search.js +226 -0
  36. package/index.d.ts +1 -1
  37. package/package.json +2 -2
  38. package/backend/actions/Script/execute.js +0 -20
  39. package/backend/actions/Script/index.js +0 -3
@@ -1826,6 +1826,10 @@ video {
1826
1826
  border-style: none;
1827
1827
  }
1828
1828
 
1829
+ .border-black\/10 {
1830
+ border-color: rgb(0 0 0 / 0.1);
1831
+ }
1832
+
1829
1833
  .border-blue-300 {
1830
1834
  --tw-border-opacity: 1;
1831
1835
  border-color: rgb(147 197 253 / var(--tw-border-opacity));
@@ -29,6 +29,9 @@ client.interceptors.response.use(
29
29
  );
30
30
 
31
31
  if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
32
+ exports.getCapabilities = function getCapabilities() {
33
+ return client.post('', { action: 'getCapabilities' }).then(res => res.data);
34
+ };
32
35
  exports.status = function status() {
33
36
  return client.post('', { action: 'status' }).then(res => res.data);
34
37
  };
@@ -205,6 +208,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
205
208
  }
206
209
  };
207
210
  } else {
211
+ exports.getCapabilities = function getCapabilities() {
212
+ return client.get('/getCapabilities').then(res => res.data);
213
+ };
208
214
  exports.status = function status() {
209
215
  return client.get('/status').then(res => res.data);
210
216
  };
@@ -24,6 +24,7 @@
24
24
  :script="part.content"
25
25
  :language="part.language"
26
26
  :target-dashboard-id="targetDashboardId"
27
+ :capabilities="capabilities"
27
28
  @copyMessage="copyMessage"></chat-message-script>
28
29
  </div>
29
30
  </div>
@@ -6,7 +6,7 @@ const template = require('./chat-message.html');
6
6
 
7
7
  module.exports = app => app.component('chat-message', {
8
8
  template: template,
9
- props: ['message', 'targetDashboardId'],
9
+ props: ['message', 'targetDashboardId', 'capabilities'],
10
10
  computed: {
11
11
  styleForMessage() {
12
12
  return this.message.role === 'user' ? 'p-3 bg-muted' : 'py-3 pr-3';
@@ -62,10 +62,11 @@ module.exports = app => app.component('chat-message', {
62
62
  if (input.limit && input.limit !== 10) parts.push('limit=' + input.limit);
63
63
  return parts.length ? '(' + parts.join(', ') + ')' : '';
64
64
  },
65
- async executeScript(message, script) {
65
+ async executeScript(message, script, dryRun) {
66
66
  const { chatMessage } = await api.ChatMessage.executeScript({
67
67
  chatMessageId: message._id,
68
- script
68
+ script,
69
+ dryRun
69
70
  });
70
71
  message.executionResult = chatMessage.executionResult;
71
72
  console.log(message);
@@ -20,17 +20,19 @@
20
20
  : 'bg-muted hover:bg-muted text-gray-600'
21
21
  ]"
22
22
  @click="activeTab = 'output'">
23
- Output
23
+ <span>Output</span>
24
+ <span v-if="isDryRunResult" class="ml-1">[Dry Run]</span>
25
+ <span
26
+ v-if="isDryRunResult"
27
+ class="ml-1 inline-flex align-middle cursor-pointer"
28
+ title="This output was produced in dry run mode"
29
+ @click.stop="openDryRunInfoModal">
30
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
31
+ <path fill-rule="evenodd" d="M18 10A8 8 0 112 10a8 8 0 0116 0zm-7-3a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 102 0v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
32
+ </svg>
33
+ </span>
24
34
  </button>
25
35
  <div class="ml-auto mr-1 flex items-center">
26
- <button
27
- v-if="activeTab === 'output'"
28
- class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
29
- @click="openDetailModal">
30
- <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.2em;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
31
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
32
- </svg>
33
- </button>
34
36
  <button
35
37
  v-if="activeTab === 'code' && !isEditing"
36
38
  class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center"
@@ -39,12 +41,58 @@
39
41
  <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM4 13.5V16h2.5l7.086-7.086-2.828-2.828L4 13.5z" />
40
42
  </svg>
41
43
  </button>
42
- <async-button
43
- v-if="!isEditing"
44
- class="px-2 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-none rounded cursor-pointer transition-colors disabled:bg-gray-400"
45
- @click="executeScript">
46
- Run
47
- </async-button>
44
+ <button
45
+ v-if="activeTab === 'output'"
46
+ class="px-2 py-1 mr-1 text-xs bg-gray-700 text-white border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
47
+ @click="openDetailModal">
48
+ <svg xmlns="http://www.w3.org/2000/svg" style="height: 1.2em;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
50
+ </svg>
51
+ </button>
52
+ <div v-if="!isEditing" class="relative mr-1" ref="runDropdown">
53
+ <div class="flex items-center">
54
+ <button
55
+ class="px-2 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-none rounded-l-md cursor-pointer transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
56
+ :disabled="isExecuting"
57
+ @click="executeScript()">
58
+ {{ selectedRunLabel }}
59
+ </button>
60
+ <button
61
+ class="px-1 py-1 text-xs bg-primary hover:bg-primary-hover text-primary-text border-l border-black/10 rounded-r-md cursor-pointer transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed"
62
+ :disabled="isExecuting"
63
+ @click.stop="toggleRunDropdown">
64
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
65
+ <path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
66
+ </svg>
67
+ </button>
68
+ <button
69
+ class="ml-1 px-1 py-1 text-content-secondary hover:bg-muted rounded cursor-pointer transition-colors"
70
+ @click="openRunInfoModal"
71
+ title="How run works">
72
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
73
+ <path fill-rule="evenodd" d="M18 10A8 8 0 112 10a8 8 0 0116 0zm-7-3a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 102 0v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
74
+ </svg>
75
+ </button>
76
+ </div>
77
+ <div
78
+ v-if="showRunDropdown"
79
+ class="absolute right-0 z-10 mt-1 w-40 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5">
80
+ <button
81
+ class="block w-full text-left px-4 py-2 text-xs text-content-secondary hover:bg-muted"
82
+ :disabled="isExecuting"
83
+ @click="selectRunMode('run')">
84
+ Run
85
+ </button>
86
+ <button
87
+ class="block w-full text-left px-4 py-2 text-xs"
88
+ :class="isExecuting ? 'cursor-not-allowed text-gray-400' : canUseDryRun ? 'text-content-secondary hover:bg-muted' : 'cursor-not-allowed text-gray-400'"
89
+ :aria-disabled="!canUseDryRun"
90
+ :title="dryRunDisabledTitle"
91
+ @click="selectRunMode('dryRun')">
92
+ Dry Run
93
+ </button>
94
+ </div>
95
+ </div>
48
96
  <div class="relative ml-1" ref="dropdown">
49
97
  <button
50
98
  @click.stop="toggleDropdown"
@@ -100,7 +148,7 @@
100
148
  </button>
101
149
  <async-button
102
150
  class="px-2 py-1 text-xs bg-blue-600 text-white border-none rounded cursor-pointer hover:bg-blue-700 transition-colors disabled:bg-gray-400"
103
- @click="executeScript">
151
+ @click="executeScript(false)">
104
152
  Execute
105
153
  </async-button>
106
154
  </div>
@@ -134,6 +182,40 @@
134
182
  </div>
135
183
  </template>
136
184
  </modal>
185
+ <modal v-if="showRunInfoModal">
186
+ <template #body>
187
+ <div class="modal-exit" @click="showRunInfoModal = false">&times;</div>
188
+ <div>
189
+ <div class="mt-4 text-content font-semibold">How Run Works</div>
190
+ <p class="mt-4 text-sm text-content-secondary">
191
+ Run executes the script shown in this message on the server with access to your Mongoose models through <code class="bg-muted px-1 rounded">db.models</code>. The latest script is saved back onto the chat message.
192
+ </p>
193
+ <p class="mt-2 text-sm text-content-secondary">
194
+ Output and console logs are stored on the message, and the Output tab updates to show the latest result.
195
+ </p>
196
+ <p class="mt-2 text-sm text-content-secondary">
197
+ Dry Run executes the same script inside a MongoDB transaction. Studio attaches the transaction session to Mongoose model and collection calls, captures the output and logs, then aborts the transaction so writes are rolled back.
198
+ </p>
199
+ <p class="mt-2 text-sm text-content-secondary">
200
+ Dry Run requires MongoDB transaction support. It is intended for previewing what a script would return before persisting changes.
201
+ </p>
202
+ </div>
203
+ </template>
204
+ </modal>
205
+ <modal v-if="showDryRunInfoModal">
206
+ <template #body>
207
+ <div class="modal-exit" @click="showDryRunInfoModal = false">&times;</div>
208
+ <div>
209
+ <div class="mt-4 text-content font-semibold">Dry Run Output</div>
210
+ <p class="mt-4 text-sm text-content-secondary">
211
+ This output was produced in dry run mode. The script ran on the server inside a MongoDB transaction, and Studio aborted that transaction after the script finished.
212
+ </p>
213
+ <p class="mt-2 text-sm text-content-secondary">
214
+ No data changes from the script were persisted. The displayed output and console logs were saved on the chat message so you can review what the script returned.
215
+ </p>
216
+ </div>
217
+ </template>
218
+ </modal>
137
219
  <modal v-if="showCreateDashboardModal">
138
220
  <template #body>
139
221
  <div class="modal-exit" @click="showCreateDashboardModal = false">&times;</div>
@@ -6,15 +6,20 @@ const template = require('./chat-message-script.html');
6
6
 
7
7
  module.exports = app => app.component('chat-message-script', {
8
8
  template,
9
- props: ['message', 'script', 'language', 'targetDashboardId'],
9
+ props: ['message', 'script', 'language', 'targetDashboardId', 'capabilities'],
10
10
  emits: ['copyMessage'],
11
11
  data() {
12
12
  return {
13
13
  activeTab: 'code',
14
+ selectedRunMode: 'run',
14
15
  showDetailModal: false,
16
+ showRunInfoModal: false,
17
+ showDryRunInfoModal: false,
15
18
  showCreateDashboardModal: false,
16
19
  showOverwriteDashboardConfirmationModal: false,
20
+ showRunDropdown: false,
17
21
  showDropdown: false,
22
+ isExecuting: false,
18
23
  newDashboardTitle: '',
19
24
  dashboardCode: '',
20
25
  createError: null,
@@ -30,32 +35,60 @@ module.exports = app => app.component('chat-message-script', {
30
35
  },
31
36
  canOverwriteDashboard() {
32
37
  return !!this.targetDashboardId;
38
+ },
39
+ canUseDryRun() {
40
+ return this.capabilities?.supportsTransactions !== false;
41
+ },
42
+ dryRunDisabledTitle() {
43
+ return this.canUseDryRun ? null : 'dry run mode requires MongoDB transactions support';
44
+ },
45
+ isDryRunResult() {
46
+ return !!this.message.executionResult?.dryRun;
47
+ },
48
+ selectedRunLabel() {
49
+ return this.selectedRunMode === 'dryRun' ? 'Dry Run' : 'Run';
33
50
  }
34
51
  },
35
52
  methods: {
36
- async executeScript() {
53
+ async executeScript(dryRun = this.selectedRunMode === 'dryRun') {
37
54
  const scriptToRun = this.isEditing ? this.editedScript : this.script;
55
+ if (this.isExecuting) {
56
+ return;
57
+ }
58
+ this.showRunDropdown = false;
38
59
  this.editedScript = scriptToRun;
39
- const { chatMessage } = await api.ChatMessage.executeScript({
40
- chatMessageId: this.message._id,
41
- script: scriptToRun
42
- });
43
- this.message.executionResult = chatMessage.executionResult;
44
- this.message.script = chatMessage.script;
45
- this.message.content = chatMessage.content;
46
- this.editedScript = chatMessage.script;
47
- if (this.isEditing) {
48
- this.finishEditing();
49
- } else {
50
- this.highlightCode();
60
+ this.isExecuting = true;
61
+ try {
62
+ const { chatMessage } = await api.ChatMessage.executeScript({
63
+ chatMessageId: this.message._id,
64
+ script: scriptToRun,
65
+ dryRun
66
+ });
67
+ this.message.executionResult = chatMessage.executionResult;
68
+ this.message.script = chatMessage.script;
69
+ this.message.content = chatMessage.content;
70
+ this.editedScript = chatMessage.script;
71
+ if (this.isEditing) {
72
+ this.finishEditing();
73
+ } else {
74
+ this.highlightCode();
75
+ }
76
+ this.activeTab = 'output';
77
+ this.$toast.success('Script executed successfully!');
78
+ return chatMessage;
79
+ } finally {
80
+ this.isExecuting = false;
51
81
  }
52
- this.activeTab = 'output';
53
- this.$toast.success('Script executed successfully!');
54
- return chatMessage;
55
82
  },
56
83
  openDetailModal() {
57
84
  this.showDetailModal = true;
58
85
  },
86
+ openRunInfoModal() {
87
+ this.showRunInfoModal = true;
88
+ },
89
+ openDryRunInfoModal() {
90
+ this.showDryRunInfoModal = true;
91
+ },
59
92
  openCreateDashboardModal() {
60
93
  this.newDashboardTitle = '';
61
94
  this.dashboardCode = this.script;
@@ -72,6 +105,21 @@ module.exports = app => app.component('chat-message-script', {
72
105
  },
73
106
  toggleDropdown() {
74
107
  this.showDropdown = !this.showDropdown;
108
+ this.showRunDropdown = false;
109
+ },
110
+ toggleRunDropdown() {
111
+ this.showRunDropdown = !this.showRunDropdown;
112
+ this.showDropdown = false;
113
+ },
114
+ selectRunMode(mode) {
115
+ if (this.isExecuting) {
116
+ return;
117
+ }
118
+ if (mode === 'dryRun' && !this.canUseDryRun) {
119
+ return;
120
+ }
121
+ this.selectedRunMode = mode;
122
+ this.showRunDropdown = false;
75
123
  },
76
124
  startEditing() {
77
125
  this.isEditing = true;
@@ -97,7 +145,11 @@ module.exports = app => app.component('chat-message-script', {
97
145
  });
98
146
  },
99
147
  handleBodyClick(event) {
148
+ const runDropdown = this.$refs.runDropdown;
100
149
  const dropdown = this.$refs.dropdown;
150
+ if (runDropdown && typeof runDropdown.contains === 'function' && !runDropdown.contains(event.target)) {
151
+ this.showRunDropdown = false;
152
+ }
101
153
  if (dropdown && typeof dropdown.contains === 'function' && !dropdown.contains(event.target)) {
102
154
  this.showDropdown = false;
103
155
  }
@@ -172,6 +224,14 @@ module.exports = app => app.component('chat-message-script', {
172
224
  this.editedScript = newScript;
173
225
  this.highlightCode();
174
226
  }
227
+ },
228
+ capabilities: {
229
+ immediate: true,
230
+ handler(capabilities) {
231
+ if (capabilities?.supportsTransactions === false && this.selectedRunMode === 'dryRun') {
232
+ this.selectedRunMode = 'run';
233
+ }
234
+ }
175
235
  }
176
236
  },
177
237
  mounted() {
@@ -112,7 +112,11 @@
112
112
  </div>
113
113
  <ul role="list" class="space-y-4" v-else>
114
114
  <li v-for="message in chatMessages" :key="message._id">
115
- <chat-message :message="message" :target-dashboard-id="currentThread?.dashboardId"></chat-message>
115
+ <chat-message
116
+ :message="message"
117
+ :target-dashboard-id="currentThread?.dashboardId"
118
+ :capabilities="capabilities">
119
+ </chat-message>
116
120
  </li>
117
121
  </ul>
118
122
  </div>
@@ -23,7 +23,12 @@ module.exports = {
23
23
  sharingThread: false,
24
24
  threadSearch: '',
25
25
  showProUpgradeModal: false,
26
- agentTools: agentToolMetadata
26
+ agentTools: agentToolMetadata,
27
+ capabilities: {
28
+ supportsChangeStreams: false,
29
+ supportsTransactions: true,
30
+ supportsAI: false
31
+ }
27
32
  }),
28
33
  methods: {
29
34
  async sendMessage() {
@@ -300,8 +305,12 @@ module.exports = {
300
305
 
301
306
  this.draftAgentMode = this.getAgentModePreference();
302
307
  this.chatThreadId = this.threadId;
303
- const { chatThreads } = await api.ChatThread.listChatThreads();
308
+ const [{ chatThreads }, { capabilities }] = await Promise.all([
309
+ api.ChatThread.listChatThreads(),
310
+ api.getCapabilities()
311
+ ]);
304
312
  this.chatThreads = chatThreads;
313
+ this.capabilities = capabilities;
305
314
  if (this.chatThreadId) {
306
315
  await this.syncCurrentThreadAgentMode();
307
316
  const { chatMessages } = await api.ChatThread.getChatThread({ chatThreadId: this.chatThreadId });
@@ -0,0 +1 @@
1
+ <pre class="w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0">{{displayValue}}</pre>
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const template = require('./detail-objectid.html');
4
+ const ObjectId = require('mongoose/lib/types/objectid');
5
+
6
+ module.exports = app => app.component('detail-objectid', {
7
+ template,
8
+ props: ['value', 'viewMode'],
9
+ emits: ['updated'],
10
+ watch: {
11
+ displayValue: {
12
+ immediate: true,
13
+ handler(val) {
14
+ this.$emit('updated', val);
15
+ }
16
+ }
17
+ },
18
+ computed: {
19
+ format() {
20
+ return this.viewMode || 'hex';
21
+ },
22
+ hexValue() {
23
+ if (this.value == null) {
24
+ return null;
25
+ }
26
+ if (typeof this.value === 'string') {
27
+ return this.value;
28
+ }
29
+ if (typeof this.value === 'object') {
30
+ if (typeof this.value.toHexString === 'function') {
31
+ return this.value.toHexString();
32
+ }
33
+ if (typeof this.value.$oid === 'string') {
34
+ return this.value.$oid;
35
+ }
36
+ }
37
+ return String(this.value);
38
+ },
39
+ objectIdDate() {
40
+ const hex = this.hexValue;
41
+ return new ObjectId(hex).getTimestamp();
42
+ },
43
+ displayValue() {
44
+ if (this.value == null) {
45
+ return String(this.value);
46
+ }
47
+
48
+ const hex = this.hexValue;
49
+ if (this.format === 'object_call') {
50
+ return `ObjectId('${hex}')`;
51
+ }
52
+ if (this.format === 'unix_seconds') {
53
+ if (!this.objectIdDate) {
54
+ return 'Invalid ObjectId';
55
+ }
56
+ return String(Math.floor(this.objectIdDate.getTime() / 1000));
57
+ }
58
+ if (this.format === 'date') {
59
+ if (!this.objectIdDate) {
60
+ return 'Invalid ObjectId';
61
+ }
62
+ return this.objectIdDate.toISOString();
63
+ }
64
+ return hex;
65
+ }
66
+ }
67
+ });
@@ -20,7 +20,7 @@
20
20
  <span class="ml-2 text-sm text-content-tertiary">({{(path.instance || 'unknown').toLowerCase()}})</span>
21
21
  <div v-if="isGeoJsonGeometry" class="ml-3 inline-flex items-center gap-2">
22
22
  <div class="inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold">
23
- <button
23
+ <button
24
24
  type="button"
25
25
  class="rounded-full px-2.5 py-0.5 transition"
26
26
  :class="detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-content-secondary hover:text-content'"
@@ -76,6 +76,11 @@
76
76
  :path="path"
77
77
  @update:viewMode="dateViewMode = $event"
78
78
  ></date-view-mode-picker>
79
+ <objectid-view-mode-picker
80
+ v-if="isObjectIdPath"
81
+ :viewMode="objectIdViewMode"
82
+ @update:viewMode="objectIdViewMode = $event"
83
+ ></objectid-view-mode-picker>
79
84
  <button
80
85
  type="button"
81
86
  class="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface"
@@ -202,7 +207,7 @@
202
207
  <component
203
208
  :is="getComponentForPath(path)"
204
209
  :value="rawValue"
205
- :viewMode="isDatePath ? dateViewMode : detailViewMode"
210
+ :viewMode="isDatePath ? dateViewMode : (isObjectIdPath ? objectIdViewMode : detailViewMode)"
206
211
  @updated="renderedValue = $event"></component>
207
212
  <button
208
213
  @click="toggleValueExpansion"
@@ -219,7 +224,7 @@
219
224
  <component
220
225
  :is="getComponentForPath(path)"
221
226
  :value="rawValue"
222
- :viewMode="isDatePath ? dateViewMode : detailViewMode"
227
+ :viewMode="isDatePath ? dateViewMode : (isObjectIdPath ? objectIdViewMode : detailViewMode)"
223
228
  @updated="renderedValue = $event"></component>
224
229
  </div>
225
230
  </div>
@@ -18,6 +18,7 @@ module.exports = app => app.component('document-property', {
18
18
  return {
19
19
  dateType: 'picker', // picker, iso
20
20
  dateViewMode: 'utc_iso',
21
+ objectIdViewMode: 'hex',
21
22
  renderedValue: UNSET,
22
23
  isCollapsed: false, // Start uncollapsed by default
23
24
  isValueExpanded: false, // Track if the value is expanded
@@ -38,6 +39,9 @@ module.exports = app => app.component('document-property', {
38
39
  isDatePath() {
39
40
  return this.path?.instance === 'Date';
40
41
  },
42
+ isObjectIdPath() {
43
+ return this.path?.instance === 'ObjectID' || this.path?.instance === 'ObjectId';
44
+ },
41
45
  rawValue() {
42
46
  return this.getValueForPath(this.path.path);
43
47
  },
@@ -177,6 +181,9 @@ module.exports = app => app.component('document-property', {
177
181
  if (schemaPath.instance === 'Date') {
178
182
  return 'detail-date';
179
183
  }
184
+ if (schemaPath.instance === 'ObjectID' || schemaPath.instance === 'ObjectId') {
185
+ return 'detail-objectid';
186
+ }
180
187
  return 'detail-default';
181
188
  },
182
189
  getEditComponentForPath(path) {
@@ -0,0 +1,12 @@
1
+ <div class="flex items-center gap-2" @click.stop>
2
+ <select
3
+ :value="format"
4
+ @input="onFormatChange($event.target.value)"
5
+ class="text-xs border border-edge rounded-md bg-surface px-2 py-1"
6
+ >
7
+ <option value="hex">Hex string</option>
8
+ <option value="object_call">ObjectId('...')</option>
9
+ <option value="unix_seconds">Unix timestamp</option>
10
+ <option value="date">Date</option>
11
+ </select>
12
+ </div>
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const template = require('./objectid-view-mode-picker.html');
4
+
5
+ module.exports = app => app.component('objectid-view-mode-picker', {
6
+ template,
7
+ props: ['viewMode'],
8
+ emits: ['update:viewMode'],
9
+ computed: {
10
+ format() {
11
+ return this.viewMode || 'hex';
12
+ }
13
+ },
14
+ methods: {
15
+ onFormatChange(newFormat) {
16
+ this.$emit('update:viewMode', newFormat);
17
+ }
18
+ }
19
+ });
@@ -90,7 +90,6 @@
90
90
  display: flex;
91
91
  align-items: center;
92
92
  justify-content: center;
93
- padding-bottom: 0.25em;
94
93
  }
95
94
 
96
95
  .modal-container .modal-exit:hover {
@@ -45,6 +45,12 @@ module.exports = app => app.component('document-search', {
45
45
  this.$refs.searchInput.focus();
46
46
  },
47
47
  methods: {
48
+ focusInput() {
49
+ const input = this.$refs.searchInput;
50
+ if (input && typeof input.focus === 'function') {
51
+ input.focus();
52
+ }
53
+ },
48
54
  emitSearch() {
49
55
  this.$emit('input', this.searchText);
50
56
  this.$emit('search', this.searchText);
@@ -263,14 +263,12 @@
263
263
  </span>
264
264
  </div>
265
265
  <div v-if="isProjectionMenuSelected && (outputType === 'table' || outputType === 'json')" class="flex items-center gap-2 w-full mt-2 flex-shrink-0">
266
- <input
267
- ref="projectionInput"
268
- v-model="projectionText"
269
- type="text"
270
- placeholder="Projection: name email, or -password"
271
- 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"
272
- @focus="initProjection($event)"
273
- @keydown.enter="applyProjectionFromInput()"
266
+ <projection-search
267
+ ref="projectionSearch"
268
+ :value="projectionText"
269
+ :schema-paths="schemaPaths"
270
+ @projection-input="projectionText = $event"
271
+ @apply="applyProjectionFromInput"
274
272
  />
275
273
  </div>
276
274
  </div>