@mongoosejs/studio 0.0.139 → 0.0.140

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.
@@ -56,7 +56,9 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
56
56
  // Execute the script in the sandbox
57
57
  output = await vm.runInContext(wrappedScript(script), context);
58
58
 
59
+ const updatedContent = updateContentWithScript(chatMessage.content, chatMessage.script, script);
59
60
  chatMessage.script = script;
61
+ chatMessage.content = updatedContent;
60
62
  chatMessage.executionResult = { output, logs: logs.join('\n'), error: null };
61
63
  await chatMessage.save();
62
64
 
@@ -65,10 +67,12 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
65
67
  error = err.message;
66
68
 
67
69
  // Update the chat message with the error
70
+ const updatedContent = updateContentWithScript(chatMessage.content, chatMessage.script, script);
68
71
  await ChatMessage.updateOne(
69
72
  { _id: chatMessageId },
70
73
  {
71
74
  script,
75
+ content: updatedContent,
72
76
  executionResult: {
73
77
  output: null,
74
78
  logs: logs.join('\n'),
@@ -84,3 +88,39 @@ module.exports = ({ db, studioConnection }) => async function executeScript(para
84
88
  const wrappedScript = script => `(async () => {
85
89
  ${script}
86
90
  })()`;
91
+
92
+ function updateContentWithScript(content, previousScript, newScript) {
93
+ if (typeof content !== 'string') {
94
+ return content;
95
+ }
96
+
97
+ const matches = Array.from(content.matchAll(/```(\w*)\n([\s\S]*?)\n```/g));
98
+ let targetMatch = null;
99
+
100
+ if (matches.length > 0) {
101
+ if (previousScript != null) {
102
+ const trimmedPrevious = previousScript.trim();
103
+ targetMatch = matches.find(match => match[2].trim() === trimmedPrevious) || null;
104
+ }
105
+
106
+ if (targetMatch == null) {
107
+ targetMatch = matches[0];
108
+ }
109
+ }
110
+
111
+ if (targetMatch != null) {
112
+ const language = targetMatch[1];
113
+ const fenceStart = language ? '```' + language : '```';
114
+ const replacement = fenceStart + '\n' + newScript + '\n```';
115
+ return (
116
+ content.slice(0, targetMatch.index) +
117
+ replacement +
118
+ content.slice(targetMatch.index + targetMatch[0].length)
119
+ );
120
+ }
121
+
122
+ const trimmedContent = content.trimEnd();
123
+ const prefix = trimmedContent.length > 0 ? trimmedContent + '\n\n' : '';
124
+
125
+ return prefix + '```\n' + newScript + '\n```';
126
+ }
@@ -13,7 +13,10 @@ module.exports = ({ db }) => async function listModels(params) {
13
13
  const { roles } = new ListModelsParams(params);
14
14
  await authorize('Model.listModels', roles);
15
15
 
16
+ const readyState = db.connection?.readyState ?? db.readyState;
17
+
16
18
  return {
17
- models: Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort()
19
+ models: Object.keys(db.models).filter(key => !key.startsWith('__Studio_')).sort(),
20
+ readyState
18
21
  };
19
22
  };
@@ -605,7 +605,7 @@ module.exports = app => app.component('async-button', {
605
605
  /***/ ((module) => {
606
606
 
607
607
  "use strict";
608
- module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <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\" />\n </svg>\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript(message, script)\">\n Execute\n </async-button>\n <div class=\"relative ml-1\" ref=\"dropdown\">\n <button\n @click.stop=\"toggleDropdown\"\n class=\"px-1 py-1 text-xs hover:bg-gray-300 rounded flex items-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path d=\"M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z\" />\n </svg>\n </button>\n <div\n v-if=\"showDropdown\"\n class=\"absolute right-0 z-10 mt-1 w-64 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5\">\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"openCreateDashboardModal(); showDropdown = false\">\n Create Dashboard\n </button>\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"$emit('copyMessage'); showDropdown = false\">\n Copy Full Message\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <pre class=\"p-3 whitespace-pre-wrap max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <dashboard-map v-else-if=\"message.executionResult?.output?.$featureCollection\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">&times;</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" :responsive=\"true\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n <modal v-if=\"showCreateDashboardModal\">\n <template #body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false\">&times;</div>\n <div>\n <div class=\"mt-4 text-gray-900 font-semibold\">Create Dashboard</div>\n <div class=\"mt-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Title</label>\n <div class=\"mt-2\">\n <div class=\"w-full flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-teal-600\">\n <input type=\"text\" v-model=\"newDashboardTitle\" class=\"outline-none block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6\" placeholder=\"My Dashboard\">\n </div>\n </div>\n </div>\n <div class=\"my-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Code</label>\n <div class=\"border border-gray-200\">\n <textarea class=\"p-2 h-[300px] w-full\" ref=\"dashboardCodeEditor\"></textarea>\n </div>\n </div>\n <async-button\n @click=\"createDashboardFromScript\"\n class=\"rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n Submit\n </async-button>\n <div v-if=\"createErrors.length > 0\" class=\"rounded-md bg-red-50 p-4 mt-1\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">Error</h3>\n <div class=\"mt-2 text-sm text-red-700\">\n {{createError}}\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
608
+ module.exports = "<div class=\"relative border rounded bg-gray-100 text-black text-sm overflow-hidden\">\n <div class=\"flex border-b pt-[1px] text-xs font-medium bg-gray-200\">\n <button\n class=\"px-3 py-1 border-r border-gray-300 hover:bg-green-300\"\n :class=\"{'bg-gray-300': activeTab === 'code', 'bg-green-300': activeTab === 'code'}\"\n @click=\"activeTab = 'code'\">\n Code\n </button>\n <button\n class=\"px-3 py-1 hover:bg-green-300\"\n :class=\"{'bg-green-300': activeTab === 'output'}\"\n @click=\"activeTab = 'output'\">\n Output\n </button>\n <div class=\"ml-auto mr-1 flex\">\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click=\"copyOutput\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'output'\"\n class=\"px-2 py-1 mr-1 text-xs bg-blue-500 text-white border-none rounded cursor-pointer hover:bg-blue-600 transition-colors flex items-center\"\n @click=\"openDetailModal\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\n <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\" />\n </svg>\n </button>\n <button\n v-if=\"activeTab === 'code' && !isEditing\"\n class=\"px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center\"\n @click.stop=\"startEditing\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-3 w-3\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <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\" />\n </svg>\n </button>\n <async-button\n v-if=\"!isEditing\"\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n <div class=\"relative ml-1\" ref=\"dropdown\">\n <button\n @click.stop=\"toggleDropdown\"\n class=\"px-1 py-1 text-xs hover:bg-gray-300 rounded flex items-center\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"h-4 w-4\" viewBox=\"0 0 20 20\" fill=\"currentColor\">\n <path d=\"M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z\" />\n </svg>\n </button>\n <div\n v-if=\"showDropdown\"\n class=\"absolute right-0 z-10 mt-1 w-64 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black/5\">\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"openCreateDashboardModal(); showDropdown = false\">\n Create Dashboard\n </button>\n <button\n class=\"block w-full text-left px-4 py-2 text-xs text-gray-700 hover:bg-gray-100\"\n @click=\"$emit('copyMessage'); showDropdown = false\">\n Copy Full Message\n </button>\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"p-3 max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto\" v-show=\"activeTab === 'code'\">\n <div v-if=\"isEditing\" class=\"flex flex-col space-y-2\">\n <div class=\"border border-gray-200\">\n <textarea ref=\"scriptEditor\" class=\"w-full h-[45vh]\" @input=\"handleScriptInput\"></textarea>\n </div>\n <div class=\"flex justify-end gap-2\">\n <button\n class=\"px-2 py-1 text-xs bg-gray-300 text-gray-800 border-none rounded cursor-pointer hover:bg-gray-400 transition-colors\"\n @click.stop=\"cancelEditing\">\n Cancel\n </button>\n <async-button\n class=\"px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400\"\n @click=\"executeScript\">\n Execute\n </async-button>\n </div>\n </div>\n <pre v-else class=\"whitespace-pre-wrap\"><code v-text=\"script\" ref=\"code\" :class=\"'language-' + language\"></code></pre>\n </div>\n\n <div class=\"p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative\" v-show=\"activeTab === 'output'\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" />\n <dashboard-map v-else-if=\"message.executionResult?.output?.$featureCollection\" :value=\"message.executionResult?.output\" />\n <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>\n\n <div v-if=\"message.executionResult?.logs?.length\" class=\"mt-3 pt-3 border-t border-gray-200\">\n <div class=\"text-xs font-semibold text-gray-600 uppercase tracking-wide\">Console</div>\n <pre class=\"mt-1 bg-gray-100 text-gray-900 p-3 rounded whitespace-pre-wrap overflow-x-auto max-h-[280px]\">{{ message.executionResult.logs }}</pre>\n </div>\n </div>\n\n <modal ref=\"outputModal\" v-if=\"showDetailModal\" containerClass=\"!h-[90vh] !w-[90vw]\">\n <template #body>\n <div class=\"absolute font-mono right-1 top-1 cursor-pointer text-xl\" @click=\"showDetailModal = false;\">&times;</div>\n <div class=\"h-full overflow-auto\">\n <dashboard-chart v-if=\"message.executionResult?.output?.$chart\" :value=\"message.executionResult?.output\" :responsive=\"true\" />\n <pre v-else class=\"whitespace-pre-wrap\">{{ message.executionResult?.output || 'No output' }}</pre>\n </div>\n </template>\n </modal>\n <modal v-if=\"showCreateDashboardModal\">\n <template #body>\n <div class=\"modal-exit\" @click=\"showCreateDashboardModal = false\">&times;</div>\n <div>\n <div class=\"mt-4 text-gray-900 font-semibold\">Create Dashboard</div>\n <div class=\"mt-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Title</label>\n <div class=\"mt-2\">\n <div class=\"w-full flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-teal-600\">\n <input type=\"text\" v-model=\"newDashboardTitle\" class=\"outline-none block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6\" placeholder=\"My Dashboard\">\n </div>\n </div>\n </div>\n <div class=\"my-4\">\n <label class=\"block text-sm font-medium leading-6 text-gray-900\">Code</label>\n <div class=\"border border-gray-200\">\n <textarea class=\"p-2 h-[300px] w-full\" ref=\"dashboardCodeEditor\"></textarea>\n </div>\n </div>\n <async-button\n @click=\"createDashboardFromScript\"\n class=\"rounded-md bg-teal-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600\">\n Submit\n </async-button>\n <div v-if=\"createErrors.length > 0\" class=\"rounded-md bg-red-50 p-4 mt-1\">\n <div class=\"flex\">\n <div class=\"flex-shrink-0\">\n <svg class=\"h-5 w-5 text-red-400\" viewBox=\"0 0 20 20\" fill=\"currentColor\" aria-hidden=\"true\">\n <path fill-rule=\"evenodd\" d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z\" clip-rule=\"evenodd\" />\n </svg>\n </div>\n <div class=\"ml-3\">\n <h3 class=\"text-sm font-medium text-red-800\">Error</h3>\n <div class=\"mt-2 text-sm text-red-700\">\n {{createError}}\n </div>\n </div>\n </div>\n </div>\n </div>\n </template>\n </modal>\n</div>\n";
609
609
 
610
610
  /***/ }),
611
611
 
@@ -616,6 +616,7 @@ module.exports = "<div class=\"relative border rounded bg-gray-100 text-black te
616
616
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
617
617
 
618
618
  "use strict";
619
+ /* global CodeMirror, Prism */
619
620
 
620
621
 
621
622
  const api = __webpack_require__(/*! ../../api */ "./frontend/src/api.js");
@@ -635,7 +636,10 @@ module.exports = app => app.component('chat-message-script', {
635
636
  newDashboardTitle: '',
636
637
  dashboardCode: '',
637
638
  createError: null,
638
- dashboardEditor: null
639
+ dashboardEditor: null,
640
+ isEditing: false,
641
+ codeEditor: null,
642
+ editedScript: null
639
643
  };
640
644
  },
641
645
  computed: {
@@ -644,13 +648,27 @@ module.exports = app => app.component('chat-message-script', {
644
648
  }
645
649
  },
646
650
  methods: {
647
- async executeScript(message, script) {
651
+ async executeScript() {
652
+ let scriptToRun = this.script;
653
+ if (this.isEditing) {
654
+ scriptToRun = this.codeEditor ? this.codeEditor.getValue() : this.editedScript;
655
+ }
656
+ this.editedScript = scriptToRun;
648
657
  const { chatMessage } = await api.ChatMessage.executeScript({
649
- chatMessageId: message._id,
650
- script
658
+ chatMessageId: this.message._id,
659
+ script: scriptToRun
651
660
  });
652
- message.executionResult = chatMessage.executionResult;
661
+ this.message.executionResult = chatMessage.executionResult;
662
+ this.message.script = chatMessage.script;
663
+ this.message.content = chatMessage.content;
664
+ this.editedScript = chatMessage.script;
665
+ if (this.isEditing) {
666
+ this.finishEditing();
667
+ } else {
668
+ this.highlightCode();
669
+ }
653
670
  this.activeTab = 'output';
671
+ return chatMessage;
654
672
  },
655
673
  openDetailModal() {
656
674
  this.showDetailModal = true;
@@ -677,6 +695,52 @@ module.exports = app => app.component('chat-message-script', {
677
695
  toggleDropdown() {
678
696
  this.showDropdown = !this.showDropdown;
679
697
  },
698
+ startEditing() {
699
+ this.isEditing = true;
700
+ this.editedScript = this.script;
701
+ this.$nextTick(() => {
702
+ if (!this.$refs.scriptEditor) {
703
+ return;
704
+ }
705
+ this.$refs.scriptEditor.value = this.editedScript;
706
+ if (typeof CodeMirror === 'undefined') {
707
+ return;
708
+ }
709
+ this.destroyCodeMirror();
710
+ this.codeEditor = CodeMirror.fromTextArea(this.$refs.scriptEditor, {
711
+ mode: 'javascript',
712
+ lineNumbers: true,
713
+ smartIndent: false
714
+ });
715
+ });
716
+ },
717
+ cancelEditing() {
718
+ this.isEditing = false;
719
+ this.destroyCodeMirror();
720
+ this.editedScript = this.script;
721
+ this.highlightCode();
722
+ },
723
+ finishEditing() {
724
+ this.isEditing = false;
725
+ this.destroyCodeMirror();
726
+ this.highlightCode();
727
+ },
728
+ destroyCodeMirror() {
729
+ if (this.codeEditor) {
730
+ this.codeEditor.toTextArea();
731
+ this.codeEditor = null;
732
+ }
733
+ },
734
+ handleScriptInput(event) {
735
+ this.editedScript = event?.target?.value || '';
736
+ },
737
+ highlightCode() {
738
+ this.$nextTick(() => {
739
+ if (this.$refs.code) {
740
+ Prism.highlightElement(this.$refs.code);
741
+ }
742
+ });
743
+ },
680
744
  handleBodyClick(event) {
681
745
  const dropdown = this.$refs.dropdown;
682
746
  if (dropdown && typeof dropdown.contains === 'function' && !dropdown.contains(event.target)) {
@@ -701,11 +765,22 @@ module.exports = app => app.component('chat-message-script', {
701
765
  this.$router.push('/dashboard/' + dashboard._id);
702
766
  },
703
767
  async copyOutput() {
704
- let output = this.message.executionResult.output;
768
+ const executionResult = this.message.executionResult || {};
769
+ let output = executionResult.output;
705
770
  if (output != null && typeof output === 'object') {
706
771
  output = JSON.stringify(output, null, 2);
707
772
  }
708
- await navigator.clipboard.writeText(output);
773
+
774
+ const logs = executionResult.logs;
775
+ const parts = [];
776
+ if (output != null) {
777
+ parts.push(output);
778
+ }
779
+ if (logs) {
780
+ parts.push(logs);
781
+ }
782
+
783
+ await navigator.clipboard.writeText(parts.join('\n\n'));
709
784
  vanillatoasts.create({
710
785
  title: 'Code output copied!',
711
786
  type: 'success',
@@ -721,18 +796,25 @@ module.exports = app => app.component('chat-message-script', {
721
796
  this.dashboardEditor.toTextArea();
722
797
  this.dashboardEditor = null;
723
798
  }
799
+ },
800
+ script(newScript) {
801
+ if (!this.isEditing) {
802
+ this.editedScript = newScript;
803
+ this.highlightCode();
804
+ }
724
805
  }
725
806
  },
726
807
  mounted() {
727
- Prism.highlightElement(this.$refs.code);
808
+ this.highlightCode();
728
809
  this.$nextTick(() => {
729
810
  document.body.addEventListener('click', this.handleBodyClick);
730
811
  });
731
- if (this.message.executionResult?.output) {
812
+ if (this.message.executionResult?.output || this.message.executionResult?.logs) {
732
813
  this.activeTab = 'output';
733
814
  }
734
815
  },
735
816
  unmounted() {
817
+ this.destroyCodeMirror();
736
818
  document.body.removeEventListener('click', this.handleBodyClick);
737
819
  }
738
820
  });
@@ -4233,7 +4315,7 @@ module.exports = ".models {\n position: relative;\n display: flex;\n flex-dir
4233
4315
  /***/ ((module) => {
4234
4316
 
4235
4317
  "use strict";
4236
- module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n </nav>\n </aside>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px] z-10\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"relative flex-grow m-0\">\n <input ref=\"searchInput\" class=\"w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none\" type=\"text\" placeholder=\"Filter\" v-model=\"searchText\" @click=\"initFilter\" @input=\"updateAutocomplete\" @keydown=\"handleKeyDown\" />\n <ul v-if=\"autocompleteSuggestions.length\" class=\"absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow\">\n <li v-for=\"(suggestion, index) in autocompleteSuggestions\" :key=\"suggestion\" class=\"px-2 py-1 cursor-pointer\" :class=\"{ 'bg-ultramarine-100': index === autocompleteIndex }\" @mousedown.prevent=\"applySuggestion(index)\">{{ suggestion }}</li>\n </ul>\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }\"\n class=\"rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\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 <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\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-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\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 rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\n </div>\n </div>\n <div class=\"documents-container relative\">\n <table v-if=\"outputType === 'table'\">\n <thead>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\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\">\n <td v-for=\"schemaPath in filteredPaths\" :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-if=\"outputType === 'json'\" class=\"flex flex-col space-y-6\">\n <div\n v-for=\"document in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentContainerClick(document, $event)\"\n :class=\"[\n 'group relative transition-colors',\n selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:bg-slate-100'\n ]\"\n >\n <button\n type=\"button\"\n class=\"absolute top-2 right-2 z-10 inline-flex items-center rounded bg-ultramarine-600 px-2 py-1 text-xs font-semibold text-white shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\n @click.stop=\"openDocument(document)\"\n >\n Open this Document\n </button>\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </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\">{{ index.name }}</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=\"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-gray-300 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-gray-700 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-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 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-gray-500 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-gray-500 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</div>\n";
4318
+ module.exports = "<div class=\"models flex\" style=\"height: calc(100vh - 55px); height: calc(100dvh - 55px)\">\n <div class=\"fixed top-[65px] cursor-pointer bg-gray-100 rounded-r-md z-10\" @click=\"hideSidebar = false\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"#5f6368\"><path d=\"M360-120v-720h80v720h-80Zm160-160v-400l200 200-200 200Z\"/></svg>\n </div>\n <aside class=\"bg-white border-r overflow-y-auto overflow-x-hidden h-full transition-all duration-300 ease-in-out z-20 w-0 lg:w-48 fixed lg:relative shrink-0\" :class=\"hideSidebar === true ? '!w-0' : hideSidebar === false ? '!w-48' : ''\">\n <div class=\"flex items-center border-b border-gray-100 w-48 overflow-x-hidden\">\n <div class=\"p-4 font-bold text-lg\">Models</div>\n <button\n @click=\"hideSidebar = true\"\n class=\"ml-auto mr-2 p-2 rounded hover:bg-gray-200 focus:outline-none\"\n aria-label=\"Close sidebar\"\n >\n <svg xmlns=\"http://www.w3.org/2000/svg\" style=\"h-5 w-5\" viewBox=\"0 -960 960 960\" class=\"w-5\" fill=\"currentColor\"><path d=\"M660-320v-320L500-480l160 160ZM200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm120-80v-560H200v560h120Zm80 0h360v-560H400v560Zm-80 0H200h120Z\"/></svg>\n </button>\n </div>\n <nav class=\"flex flex-1 flex-col\">\n <ul role=\"list\" class=\"flex flex-1 flex-col gap-y-7\">\n <li>\n <ul role=\"list\">\n <li v-for=\"model in models\">\n <router-link\n :to=\"'/model/' + model\"\n class=\"block truncate rounded-md py-2 pr-2 pl-2 text-sm font-semibold text-gray-700\"\n :class=\"model === currentModel ? 'bg-ultramarine-100 font-bold' : 'hover:bg-ultramarine-100'\">\n {{model}}\n </router-link>\n </li>\n </ul>\n </li>\n </ul>\n <div v-if=\"models.length === 0 && status === 'loaded'\" class=\"p-2 bg-red-100\">\n No models found\n </div>\n </nav>\n </aside>\n <div class=\"documents\" ref=\"documentsList\">\n <div class=\"relative h-[42px] z-10\">\n <div class=\"documents-menu\">\n <div class=\"flex flex-row items-center w-full gap-2\">\n <form @submit.prevent=\"search\" class=\"relative flex-grow m-0\">\n <input ref=\"searchInput\" class=\"w-full font-mono rounded-md p-1 border border-gray-300 outline-gray-300 text-lg focus:ring-1 focus:ring-ultramarine-200 focus:ring-offset-0 focus:outline-none\" type=\"text\" placeholder=\"Filter\" v-model=\"searchText\" @click=\"initFilter\" @input=\"updateAutocomplete\" @keydown=\"handleKeyDown\" />\n <ul v-if=\"autocompleteSuggestions.length\" class=\"absolute z-[9999] bg-white border border-gray-300 rounded mt-1 w-full max-h-40 overflow-y-auto shadow\">\n <li v-for=\"(suggestion, index) in autocompleteSuggestions\" :key=\"suggestion\" class=\"px-2 py-1 cursor-pointer\" :class=\"{ 'bg-ultramarine-100': index === autocompleteIndex }\" @mousedown.prevent=\"applySuggestion(index)\">{{ suggestion }}</li>\n </ul>\n </form>\n <div>\n <span v-if=\"numDocuments == null\">Loading ...</span>\n <span v-else-if=\"typeof numDocuments === 'number'\">{{numDocuments === 1 ? numDocuments+ ' document' : numDocuments + ' documents'}}</span>\n </div>\n <button\n @click=\"shouldShowExportModal = true\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Export\n </button>\n <button\n @click=\"stagingSelect\"\n type=\"button\"\n :class=\"{ 'bg-gray-500 ring-inset ring-2 ring-gray-300 hover:bg-gray-600': selectMultiple, 'bg-ultramarine-600 hover:bg-ultramarine-500' : !selectMultiple }\"\n class=\"rounded px-2 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\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 <button\n @click=\"openIndexModal\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Indexes\n </button>\n <button\n @click=\"shouldShowCreateModal = true;\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Create\n </button>\n <button\n @click=\"openFieldSelection\"\n type=\"button\"\n v-show=\"!selectMultiple\"\n class=\"rounded bg-ultramarine-600 px-2 py-2 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\">\n Fields\n </button>\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-gray-50 focus:z-10\"\n :class=\"outputType === 'table' ? 'bg-gray-200' : 'bg-white'\">\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 rounded-none rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10\"\n :class=\"outputType === 'json' ? 'bg-gray-200' : 'bg-white'\">\n <img class=\"h-5 w-5\" src=\"images/json.svg\">\n </button>\n </span>\n </div>\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>\n <th v-for=\"path in filteredPaths\" @click=\"clickFilter(path.path)\" class=\"cursor-pointer\">\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\">\n <td v-for=\"schemaPath in filteredPaths\" :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-6\">\n <div\n v-for=\"document in documents\"\n :key=\"document._id\"\n @click=\"handleDocumentContainerClick(document, $event)\"\n :class=\"[\n 'group relative transition-colors',\n selectedDocuments.some(x => x._id.toString() === document._id.toString()) ? 'bg-blue-200' : 'hover:bg-slate-100'\n ]\"\n >\n <button\n type=\"button\"\n class=\"absolute top-2 right-2 z-10 inline-flex items-center rounded bg-ultramarine-600 px-2 py-1 text-xs font-semibold text-white shadow-sm transition-opacity duration-150 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 hover:bg-ultramarine-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ultramarine-600\"\n @click.stop=\"openDocument(document)\"\n >\n Open this Document\n </button>\n <list-json :value=\"filterDocument(document)\">\n </list-json>\n </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\">{{ index.name }}</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=\"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-gray-300 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-gray-700 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-ultramarine-600 px-2.5 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-ultramarine-500 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-gray-500 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-gray-500 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</div>\n";
4237
4319
 
4238
4320
  /***/ }),
4239
4321
 
@@ -4322,7 +4404,8 @@ module.exports = app => app.component('models', {
4322
4404
  interval: null,
4323
4405
  outputType: 'table', // json, table
4324
4406
  hideSidebar: null,
4325
- lastSelectedIndex: null
4407
+ lastSelectedIndex: null,
4408
+ error: null
4326
4409
  }),
4327
4410
  created() {
4328
4411
  this.currentModel = this.model;
@@ -4338,10 +4421,18 @@ module.exports = app => app.component('models', {
4338
4421
  document.addEventListener('scroll', this.onScroll, true);
4339
4422
  this.onPopState = () => this.initSearchFromUrl();
4340
4423
  window.addEventListener('popstate', this.onPopState, true);
4341
- this.models = await api.Model.listModels().then(res => res.models);
4424
+ const { models, readyState } = await api.Model.listModels();
4425
+ this.models = models;
4342
4426
  if (this.currentModel == null && this.models.length > 0) {
4343
4427
  this.currentModel = this.models[0];
4344
4428
  }
4429
+ if (this.models.length === 0) {
4430
+ this.status = 'loaded';
4431
+ this.numDocuments = 0;
4432
+ if (readyState === 0) {
4433
+ this.error = 'No models found and Mongoose is not connected. Check our documentation for more information.';
4434
+ }
4435
+ }
4345
4436
 
4346
4437
  await this.initSearchFromUrl();
4347
4438
  },
@@ -16276,7 +16367,7 @@ module.exports = function stringToParts(str) {
16276
16367
  /***/ ((module) => {
16277
16368
 
16278
16369
  "use strict";
16279
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.139","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.1.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.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","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
16370
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.140","description":"A sleek, powerful MongoDB UI with built-in dashboarding and auth, seamlessly integrated with your Express, Vercel, or Netlify app.","homepage":"https://studio.mongoosejs.io/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.1.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vanillatoasts":"^1.6.0","vue":"3.x","webpack":"5.x"},"peerDependencies":{"bson":"^5.5.1 || 6.x","express":"4.x","mongoose":"7.x || 8.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","mongoose":"8.x"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js"}}');
16280
16371
 
16281
16372
  /***/ })
16282
16373
 
@@ -703,6 +703,10 @@ video {
703
703
  margin: 0px;
704
704
  }
705
705
 
706
+ .m-4 {
707
+ margin: 1rem;
708
+ }
709
+
706
710
  .-mx-4 {
707
711
  margin-left: -1rem;
708
712
  margin-right: -1rem;
@@ -817,6 +821,10 @@ video {
817
821
  margin-top: 0.5rem;
818
822
  }
819
823
 
824
+ .mt-3 {
825
+ margin-top: 0.75rem;
826
+ }
827
+
820
828
  .mt-4 {
821
829
  margin-top: 1rem;
822
830
  }
@@ -940,6 +948,10 @@ video {
940
948
  height: 42px;
941
949
  }
942
950
 
951
+ .h-\[45vh\] {
952
+ height: 45vh;
953
+ }
954
+
943
955
  .h-full {
944
956
  height: 100%;
945
957
  }
@@ -952,6 +964,10 @@ video {
952
964
  max-height: 10rem;
953
965
  }
954
966
 
967
+ .max-h-\[280px\] {
968
+ max-height: 280px;
969
+ }
970
+
955
971
  .max-h-\[50vh\] {
956
972
  max-height: 50vh;
957
973
  }
@@ -1455,6 +1471,11 @@ video {
1455
1471
  border-color: rgb(209 213 219 / var(--tw-border-opacity));
1456
1472
  }
1457
1473
 
1474
+ .border-red-400 {
1475
+ --tw-border-opacity: 1;
1476
+ border-color: rgb(248 113 113 / var(--tw-border-opacity));
1477
+ }
1478
+
1458
1479
  .border-red-500 {
1459
1480
  --tw-border-opacity: 1;
1460
1481
  border-color: rgb(239 68 68 / var(--tw-border-opacity));
@@ -1574,6 +1595,11 @@ video {
1574
1595
  background-color: rgb(219 39 119 / var(--tw-bg-opacity));
1575
1596
  }
1576
1597
 
1598
+ .bg-red-100 {
1599
+ --tw-bg-opacity: 1;
1600
+ background-color: rgb(254 226 226 / var(--tw-bg-opacity));
1601
+ }
1602
+
1577
1603
  .bg-red-300 {
1578
1604
  --tw-bg-opacity: 1;
1579
1605
  background-color: rgb(252 165 165 / var(--tw-bg-opacity));
@@ -1804,6 +1830,10 @@ video {
1804
1830
  padding-top: 0.25rem;
1805
1831
  }
1806
1832
 
1833
+ .pt-3 {
1834
+ padding-top: 0.75rem;
1835
+ }
1836
+
1807
1837
  .pt-4 {
1808
1838
  padding-top: 1rem;
1809
1839
  }
@@ -1883,6 +1913,10 @@ video {
1883
1913
  font-weight: 600;
1884
1914
  }
1885
1915
 
1916
+ .uppercase {
1917
+ text-transform: uppercase;
1918
+ }
1919
+
1886
1920
  .capitalize {
1887
1921
  text-transform: capitalize;
1888
1922
  }
@@ -1903,6 +1937,10 @@ video {
1903
1937
  line-height: 1.25;
1904
1938
  }
1905
1939
 
1940
+ .tracking-wide {
1941
+ letter-spacing: 0.025em;
1942
+ }
1943
+
1906
1944
  .text-amber-600 {
1907
1945
  --tw-text-opacity: 1;
1908
1946
  color: rgb(217 119 6 / var(--tw-text-opacity));
@@ -2288,6 +2326,11 @@ video {
2288
2326
  background-color: rgb(209 213 219 / var(--tw-bg-opacity));
2289
2327
  }
2290
2328
 
2329
+ .hover\:bg-gray-400:hover {
2330
+ --tw-bg-opacity: 1;
2331
+ background-color: rgb(156 163 175 / var(--tw-bg-opacity));
2332
+ }
2333
+
2291
2334
  .hover\:bg-gray-50:hover {
2292
2335
  --tw-bg-opacity: 1;
2293
2336
  background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@@ -29,9 +29,18 @@
29
29
  <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" />
30
30
  </svg>
31
31
  </button>
32
+ <button
33
+ v-if="activeTab === 'code' && !isEditing"
34
+ class="px-2 py-1 mr-1 text-xs bg-gray-500 text-white border-none rounded cursor-pointer hover:bg-gray-600 transition-colors flex items-center"
35
+ @click.stop="startEditing">
36
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
37
+ <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" />
38
+ </svg>
39
+ </button>
32
40
  <async-button
41
+ v-if="!isEditing"
33
42
  class="px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400"
34
- @click="executeScript(message, script)">
43
+ @click="executeScript">
35
44
  Execute
36
45
  </async-button>
37
46
  <div class="relative ml-1" ref="dropdown">
@@ -60,12 +69,36 @@
60
69
  </div>
61
70
  </div>
62
71
 
63
- <pre class="p-3 whitespace-pre-wrap max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto" v-show="activeTab === 'code'"><code v-text="script" ref="code" :class="'language-' + language"></code></pre>
72
+ <div class="p-3 max-h-[50vh] max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] overflow-y-auto" v-show="activeTab === 'code'">
73
+ <div v-if="isEditing" class="flex flex-col space-y-2">
74
+ <div class="border border-gray-200">
75
+ <textarea ref="scriptEditor" class="w-full h-[45vh]" @input="handleScriptInput"></textarea>
76
+ </div>
77
+ <div class="flex justify-end gap-2">
78
+ <button
79
+ class="px-2 py-1 text-xs bg-gray-300 text-gray-800 border-none rounded cursor-pointer hover:bg-gray-400 transition-colors"
80
+ @click.stop="cancelEditing">
81
+ Cancel
82
+ </button>
83
+ <async-button
84
+ class="px-2 py-1 text-xs bg-green-500 text-white border-none rounded cursor-pointer hover:bg-green-600 transition-colors disabled:bg-gray-400"
85
+ @click="executeScript">
86
+ Execute
87
+ </async-button>
88
+ </div>
89
+ </div>
90
+ <pre v-else class="whitespace-pre-wrap"><code v-text="script" ref="code" :class="'language-' + language"></code></pre>
91
+ </div>
64
92
 
65
93
  <div class="p-3 whitespace-pre-wrap max-h-[50vh] overflow-y-auto bg-white border-t max-w-[calc(100vw-4rem)] lg:max-w-[calc(100vw-20rem)] relative" v-show="activeTab === 'output'">
66
94
  <dashboard-chart v-if="message.executionResult?.output?.$chart" :value="message.executionResult?.output" />
67
95
  <dashboard-map v-else-if="message.executionResult?.output?.$featureCollection" :value="message.executionResult?.output" />
68
96
  <pre v-else>{{ message.executionResult?.output || 'No output' }}</pre>
97
+
98
+ <div v-if="message.executionResult?.logs?.length" class="mt-3 pt-3 border-t border-gray-200">
99
+ <div class="text-xs font-semibold text-gray-600 uppercase tracking-wide">Console</div>
100
+ <pre class="mt-1 bg-gray-100 text-gray-900 p-3 rounded whitespace-pre-wrap overflow-x-auto max-h-[280px]">{{ message.executionResult.logs }}</pre>
101
+ </div>
69
102
  </div>
70
103
 
71
104
  <modal ref="outputModal" v-if="showDetailModal" containerClass="!h-[90vh] !w-[90vw]">
@@ -1,3 +1,4 @@
1
+ /* global CodeMirror, Prism */
1
2
  'use strict';
2
3
 
3
4
  const api = require('../../api');
@@ -17,7 +18,10 @@ module.exports = app => app.component('chat-message-script', {
17
18
  newDashboardTitle: '',
18
19
  dashboardCode: '',
19
20
  createError: null,
20
- dashboardEditor: null
21
+ dashboardEditor: null,
22
+ isEditing: false,
23
+ codeEditor: null,
24
+ editedScript: null
21
25
  };
22
26
  },
23
27
  computed: {
@@ -26,13 +30,27 @@ module.exports = app => app.component('chat-message-script', {
26
30
  }
27
31
  },
28
32
  methods: {
29
- async executeScript(message, script) {
33
+ async executeScript() {
34
+ let scriptToRun = this.script;
35
+ if (this.isEditing) {
36
+ scriptToRun = this.codeEditor ? this.codeEditor.getValue() : this.editedScript;
37
+ }
38
+ this.editedScript = scriptToRun;
30
39
  const { chatMessage } = await api.ChatMessage.executeScript({
31
- chatMessageId: message._id,
32
- script
40
+ chatMessageId: this.message._id,
41
+ script: scriptToRun
33
42
  });
34
- message.executionResult = chatMessage.executionResult;
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();
51
+ }
35
52
  this.activeTab = 'output';
53
+ return chatMessage;
36
54
  },
37
55
  openDetailModal() {
38
56
  this.showDetailModal = true;
@@ -59,6 +77,52 @@ module.exports = app => app.component('chat-message-script', {
59
77
  toggleDropdown() {
60
78
  this.showDropdown = !this.showDropdown;
61
79
  },
80
+ startEditing() {
81
+ this.isEditing = true;
82
+ this.editedScript = this.script;
83
+ this.$nextTick(() => {
84
+ if (!this.$refs.scriptEditor) {
85
+ return;
86
+ }
87
+ this.$refs.scriptEditor.value = this.editedScript;
88
+ if (typeof CodeMirror === 'undefined') {
89
+ return;
90
+ }
91
+ this.destroyCodeMirror();
92
+ this.codeEditor = CodeMirror.fromTextArea(this.$refs.scriptEditor, {
93
+ mode: 'javascript',
94
+ lineNumbers: true,
95
+ smartIndent: false
96
+ });
97
+ });
98
+ },
99
+ cancelEditing() {
100
+ this.isEditing = false;
101
+ this.destroyCodeMirror();
102
+ this.editedScript = this.script;
103
+ this.highlightCode();
104
+ },
105
+ finishEditing() {
106
+ this.isEditing = false;
107
+ this.destroyCodeMirror();
108
+ this.highlightCode();
109
+ },
110
+ destroyCodeMirror() {
111
+ if (this.codeEditor) {
112
+ this.codeEditor.toTextArea();
113
+ this.codeEditor = null;
114
+ }
115
+ },
116
+ handleScriptInput(event) {
117
+ this.editedScript = event?.target?.value || '';
118
+ },
119
+ highlightCode() {
120
+ this.$nextTick(() => {
121
+ if (this.$refs.code) {
122
+ Prism.highlightElement(this.$refs.code);
123
+ }
124
+ });
125
+ },
62
126
  handleBodyClick(event) {
63
127
  const dropdown = this.$refs.dropdown;
64
128
  if (dropdown && typeof dropdown.contains === 'function' && !dropdown.contains(event.target)) {
@@ -83,11 +147,22 @@ module.exports = app => app.component('chat-message-script', {
83
147
  this.$router.push('/dashboard/' + dashboard._id);
84
148
  },
85
149
  async copyOutput() {
86
- let output = this.message.executionResult.output;
150
+ const executionResult = this.message.executionResult || {};
151
+ let output = executionResult.output;
87
152
  if (output != null && typeof output === 'object') {
88
153
  output = JSON.stringify(output, null, 2);
89
154
  }
90
- await navigator.clipboard.writeText(output);
155
+
156
+ const logs = executionResult.logs;
157
+ const parts = [];
158
+ if (output != null) {
159
+ parts.push(output);
160
+ }
161
+ if (logs) {
162
+ parts.push(logs);
163
+ }
164
+
165
+ await navigator.clipboard.writeText(parts.join('\n\n'));
91
166
  vanillatoasts.create({
92
167
  title: 'Code output copied!',
93
168
  type: 'success',
@@ -103,18 +178,25 @@ module.exports = app => app.component('chat-message-script', {
103
178
  this.dashboardEditor.toTextArea();
104
179
  this.dashboardEditor = null;
105
180
  }
181
+ },
182
+ script(newScript) {
183
+ if (!this.isEditing) {
184
+ this.editedScript = newScript;
185
+ this.highlightCode();
186
+ }
106
187
  }
107
188
  },
108
189
  mounted() {
109
- Prism.highlightElement(this.$refs.code);
190
+ this.highlightCode();
110
191
  this.$nextTick(() => {
111
192
  document.body.addEventListener('click', this.handleBodyClick);
112
193
  });
113
- if (this.message.executionResult?.output) {
194
+ if (this.message.executionResult?.output || this.message.executionResult?.logs) {
114
195
  this.activeTab = 'output';
115
196
  }
116
197
  },
117
198
  unmounted() {
199
+ this.destroyCodeMirror();
118
200
  document.body.removeEventListener('click', this.handleBodyClick);
119
201
  }
120
202
  });
@@ -28,6 +28,9 @@
28
28
  </ul>
29
29
  </li>
30
30
  </ul>
31
+ <div v-if="models.length === 0 && status === 'loaded'" class="p-2 bg-red-100">
32
+ No models found
33
+ </div>
31
34
  </nav>
32
35
  </aside>
33
36
  <div class="documents" ref="documentsList">
@@ -116,7 +119,13 @@
116
119
  </div>
117
120
  </div>
118
121
  <div class="documents-container relative">
119
- <table v-if="outputType === 'table'">
122
+ <div v-if="error">
123
+ <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 relative m-4 rounded-md" role="alert">
124
+ <span class="block font-bold">Error</span>
125
+ <span class="block">{{ error }}</span>
126
+ </div>
127
+ </div>
128
+ <table v-else-if="outputType === 'table'">
120
129
  <thead>
121
130
  <th v-for="path in filteredPaths" @click="clickFilter(path.path)" class="cursor-pointer">
122
131
  {{path.path}}
@@ -139,7 +148,7 @@
139
148
  </tr>
140
149
  </tbody>
141
150
  </table>
142
- <div v-if="outputType === 'json'" class="flex flex-col space-y-6">
151
+ <div v-else-if="outputType === 'json'" class="flex flex-col space-y-6">
143
152
  <div
144
153
  v-for="document in documents"
145
154
  :key="document._id"
@@ -76,7 +76,8 @@ module.exports = app => app.component('models', {
76
76
  interval: null,
77
77
  outputType: 'table', // json, table
78
78
  hideSidebar: null,
79
- lastSelectedIndex: null
79
+ lastSelectedIndex: null,
80
+ error: null
80
81
  }),
81
82
  created() {
82
83
  this.currentModel = this.model;
@@ -92,10 +93,18 @@ module.exports = app => app.component('models', {
92
93
  document.addEventListener('scroll', this.onScroll, true);
93
94
  this.onPopState = () => this.initSearchFromUrl();
94
95
  window.addEventListener('popstate', this.onPopState, true);
95
- this.models = await api.Model.listModels().then(res => res.models);
96
+ const { models, readyState } = await api.Model.listModels();
97
+ this.models = models;
96
98
  if (this.currentModel == null && this.models.length > 0) {
97
99
  this.currentModel = this.models[0];
98
100
  }
101
+ if (this.models.length === 0) {
102
+ this.status = 'loaded';
103
+ this.numDocuments = 0;
104
+ if (readyState === 0) {
105
+ this.error = 'No models found and Mongoose is not connected. Check our documentation for more information.';
106
+ }
107
+ }
99
108
 
100
109
  await this.initSearchFromUrl();
101
110
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mongoosejs/studio",
3
- "version": "0.0.139",
3
+ "version": "0.0.140",
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": {