@mongoosejs/studio 0.1.6 → 0.1.7

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.
@@ -148,6 +148,9 @@ var map = {
148
148
  "./modal/modal.css": "./frontend/src/modal/modal.css",
149
149
  "./modal/modal.html": "./frontend/src/modal/modal.html",
150
150
  "./modal/modal.js": "./frontend/src/modal/modal.js",
151
+ "./models/document-search/document-search": "./frontend/src/models/document-search/document-search.js",
152
+ "./models/document-search/document-search.html": "./frontend/src/models/document-search/document-search.html",
153
+ "./models/document-search/document-search.js": "./frontend/src/models/document-search/document-search.js",
151
154
  "./models/models": "./frontend/src/models/models.js",
152
155
  "./models/models.css": "./frontend/src/models/models.css",
153
156
  "./models/models.html": "./frontend/src/models/models.html",
@@ -4524,43 +4527,28 @@ module.exports = app => app.component('modal', {
4524
4527
 
4525
4528
  /***/ }),
4526
4529
 
4527
- /***/ "./frontend/src/models/models.css":
4528
- /*!****************************************!*\
4529
- !*** ./frontend/src/models/models.css ***!
4530
- \****************************************/
4531
- /***/ ((module) => {
4532
-
4533
- "use strict";
4534
- module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models button.gray {\n color: black;\n background-color: #eee;\n}\n\n.models .model-selector {\n background-color: #eee;\n flex-grow: 0;\n padding: 15px;\n padding-top: 0px;\n}\n\n.models h1 {\n margin-top: 0px;\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: scroll;\n max-height: calc(100vh - 56px);\n}\n\n.models .documents table {\n /* max-width: -moz-fit-content;\n max-width: fit-content; */\n width: 100%;\n table-layout: auto;\n font-size: small;\n padding: 0;\n margin-right: 1em;\n white-space: nowrap;\n z-index: -1;\n border-collapse: collapse;\n line-height: 1.5em;\n}\n\n.models .documents table th {\n position: sticky;\n top: 42px;\n background-color: white;\n z-index: 1;\n}\n\n.models .documents table th:after {\n content: \"\";\n position: absolute;\n left: 0;\n width: 100%;\n bottom: -1px;\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n}\n\n.models .documents table tr {\n color: black;\n border-spacing: 0px 0px;\n background-color: white;\n cursor: pointer;\n}\n\n.models .documents table tr:nth-child(even) {\n background-color: #f5f5f5;\n}\n\n.models .documents table tr:hover {\n background-color: #a7b9ff;\n}\n\n.models .documents table th,\ntd {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n padding: 0 16px;\n height: 48px;\n}\n\n.models textarea {\n font-size: 1.2em;\n}\n\n.models .path-type {\n color: rgba(0, 0, 0, 0.36);\n font-size: 0.8em;\n}\n\n.models .documents-menu {\n position: fixed;\n background-color: white;\n z-index: 1;\n padding: 4px;\n display: flex;\n width: 100vw;\n}\n\n@media (min-width: 1024px) {\n .models .documents-menu {\n width: calc(100vw - 12rem);\n }\n}\n\n.models .documents-menu .search-input {\n flex-grow: 1;\n align-items: center;\n}\n\n.models .search-input input {\n padding: 0.25em 0.5em;\n font-size: 1.1em;\n border: 1px solid #ddd;\n border-radius: 3px;\n width: calc(100% - 1em);\n}\n\n.models .sort-arrow {\n padding-left: 10px;\n padding-right: 10px;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .documents .buttons {\n display: inline-flex;\n justify-content: space-around;\n align-items: center;\n}\n\n";
4535
-
4536
- /***/ }),
4537
-
4538
- /***/ "./frontend/src/models/models.html":
4539
- /*!*****************************************!*\
4540
- !*** ./frontend/src/models/models.html ***!
4541
- \*****************************************/
4530
+ /***/ "./frontend/src/models/document-search/document-search.html":
4531
+ /*!******************************************************************!*\
4532
+ !*** ./frontend/src/models/document-search/document-search.html ***!
4533
+ \******************************************************************/
4542
4534
  /***/ ((module) => {
4543
4535
 
4544
4536
  "use strict";
4545
- 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)\" :references=\"referenceMap\">\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";
4537
+ module.exports = "<form @submit.prevent=\"emitSearch\" class=\"relative flex-grow m-0\">\n <input\n ref=\"searchInput\"\n 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\"\n type=\"text\"\n placeholder=\"Filter\"\n v-model=\"searchText\"\n @click=\"initFilter\"\n @input=\"updateAutocomplete\"\n @keydown=\"handleKeyDown\"\n />\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\n v-for=\"(suggestion, index) in autocompleteSuggestions\"\n :key=\"suggestion\"\n class=\"px-2 py-1 cursor-pointer\"\n :class=\"{ 'bg-ultramarine-100': index === autocompleteIndex }\"\n @mousedown.prevent=\"applySuggestion(index)\"\n >\n {{ suggestion }}\n </li>\n </ul>\n</form>\n";
4546
4538
 
4547
4539
  /***/ }),
4548
4540
 
4549
- /***/ "./frontend/src/models/models.js":
4550
- /*!***************************************!*\
4551
- !*** ./frontend/src/models/models.js ***!
4552
- \***************************************/
4541
+ /***/ "./frontend/src/models/document-search/document-search.js":
4542
+ /*!****************************************************************!*\
4543
+ !*** ./frontend/src/models/document-search/document-search.js ***!
4544
+ \****************************************************************/
4553
4545
  /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4554
4546
 
4555
4547
  "use strict";
4556
4548
 
4557
4549
 
4558
- const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
4559
- const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models/models.html");
4560
- const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
4561
-
4562
- const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
4563
- const { Trie } = __webpack_require__(/*! ./trie */ "./frontend/src/models/trie.js");
4550
+ const template = __webpack_require__(/*! ./document-search.html */ "./frontend/src/models/document-search/document-search.html");
4551
+ const { Trie } = __webpack_require__(/*! ../trie */ "./frontend/src/models/trie.js");
4564
4552
 
4565
4553
  const QUERY_SELECTORS = [
4566
4554
  '$eq',
@@ -4589,7 +4577,241 @@ const QUERY_SELECTORS = [
4589
4577
  '$mod'
4590
4578
  ];
4591
4579
 
4580
+ module.exports = app => app.component('document-search', {
4581
+ template,
4582
+ props: {
4583
+ value: {
4584
+ type: String,
4585
+ default: ''
4586
+ },
4587
+ schemaPaths: {
4588
+ type: Array,
4589
+ default: () => []
4590
+ }
4591
+ },
4592
+ data() {
4593
+ return {
4594
+ autocompleteSuggestions: [],
4595
+ autocompleteIndex: 0,
4596
+ autocompleteTrie: null,
4597
+ searchText: this.value || ''
4598
+ };
4599
+ },
4600
+ watch: {
4601
+ value(val) {
4602
+ this.searchText = val || '';
4603
+ },
4604
+ schemaPaths: {
4605
+ handler() {
4606
+ this.buildAutocompleteTrie();
4607
+ },
4608
+ deep: true
4609
+ }
4610
+ },
4611
+ created() {
4612
+ this.buildAutocompleteTrie();
4613
+ },
4614
+ methods: {
4615
+ emitSearch() {
4616
+ this.$emit('input', this.searchText);
4617
+ this.$emit('search', this.searchText);
4618
+ },
4619
+ buildAutocompleteTrie() {
4620
+ this.autocompleteTrie = new Trie();
4621
+ this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5, 'operator');
4622
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
4623
+ const paths = this.schemaPaths
4624
+ .map(path => path?.path)
4625
+ .filter(path => typeof path === 'string' && path.length > 0);
4626
+ for (const path of this.schemaPaths) {
4627
+ if (path.schema) {
4628
+ paths.push(...Object.keys(path.schema).map(subpath => `${path.path}.${subpath}`));
4629
+ }
4630
+ }
4631
+ this.autocompleteTrie.bulkInsert(paths, 10, 'fieldName');
4632
+ }
4633
+ },
4634
+ initFilter(ev) {
4635
+ if (!this.searchText) {
4636
+ this.searchText = '{}';
4637
+ this.$nextTick(() => {
4638
+ ev.target.setSelectionRange(1, 1);
4639
+ });
4640
+ }
4641
+ },
4642
+ updateAutocomplete() {
4643
+ const input = this.$refs.searchInput;
4644
+ const cursorPos = input ? input.selectionStart : 0;
4645
+ const before = this.searchText.slice(0, cursorPos);
4646
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
4647
+ if (match && match[1]) {
4648
+ const token = match[1];
4649
+ const leadingQuoteMatch = token.match(/^["']/);
4650
+ const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
4651
+ ? token[token.length - 1]
4652
+ : '';
4653
+ const term = token
4654
+ .replace(/^["']/, '')
4655
+ .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
4656
+ .trim();
4657
+ if (!term) {
4658
+ this.autocompleteSuggestions = [];
4659
+ return;
4660
+ }
4661
+
4662
+ const colonMatch = before.match(/:\s*([^,\}\]]*)$/);
4663
+ const role = colonMatch ? 'operator' : 'fieldName';
4664
+
4665
+ if (this.autocompleteTrie) {
4666
+ const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10, role);
4667
+ const suggestionsSet = new Set(primarySuggestions);
4668
+ if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
4669
+ for (const schemaPath of this.schemaPaths) {
4670
+ const path = schemaPath?.path;
4671
+ if (
4672
+ typeof path === 'string' &&
4673
+ path.startsWith(`${term}.`) &&
4674
+ !suggestionsSet.has(path)
4675
+ ) {
4676
+ suggestionsSet.add(path);
4677
+ if (suggestionsSet.size >= 10) {
4678
+ break;
4679
+ }
4680
+ }
4681
+ }
4682
+ }
4683
+ let suggestions = Array.from(suggestionsSet);
4684
+ if (leadingQuoteMatch) {
4685
+ const leadingQuote = leadingQuoteMatch[0];
4686
+ suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
4687
+ }
4688
+ if (trailingQuoteMatch) {
4689
+ suggestions = suggestions.map(suggestion =>
4690
+ suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
4691
+ );
4692
+ }
4693
+ this.autocompleteSuggestions = suggestions;
4694
+ this.autocompleteIndex = 0;
4695
+ return;
4696
+ }
4697
+ }
4698
+ this.autocompleteSuggestions = [];
4699
+ },
4700
+ handleKeyDown(ev) {
4701
+ if (this.autocompleteSuggestions.length === 0) {
4702
+ return;
4703
+ }
4704
+ if (ev.key === 'Tab' || ev.key === 'Enter') {
4705
+ ev.preventDefault();
4706
+ this.applySuggestion(this.autocompleteIndex);
4707
+ } else if (ev.key === 'ArrowDown') {
4708
+ ev.preventDefault();
4709
+ this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
4710
+ } else if (ev.key === 'ArrowUp') {
4711
+ ev.preventDefault();
4712
+ this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
4713
+ }
4714
+ },
4715
+ applySuggestion(index) {
4716
+ const suggestion = this.autocompleteSuggestions[index];
4717
+ if (!suggestion) {
4718
+ return;
4719
+ }
4720
+ const input = this.$refs.searchInput;
4721
+ const cursorPos = input.selectionStart;
4722
+ const before = this.searchText.slice(0, cursorPos);
4723
+ const after = this.searchText.slice(cursorPos);
4724
+ const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
4725
+ const colonNeeded = !/^\s*:/.test(after);
4726
+ if (!match) {
4727
+ return;
4728
+ }
4729
+ const token = match[1];
4730
+ const start = cursorPos - token.length;
4731
+ let replacement = suggestion;
4732
+ const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
4733
+ const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
4734
+ if (leadingQuote && !replacement.startsWith(leadingQuote)) {
4735
+ replacement = `${leadingQuote}${replacement}`;
4736
+ }
4737
+ if (trailingQuote && !replacement.endsWith(trailingQuote)) {
4738
+ replacement = `${replacement}${trailingQuote}`;
4739
+ }
4740
+ // Only insert : if we know the user isn't entering in a nested path
4741
+ if (colonNeeded && (!leadingQuote || trailingQuote)) {
4742
+ replacement = `${replacement}:`;
4743
+ }
4744
+ this.searchText = this.searchText.slice(0, start) + replacement + after;
4745
+ this.$nextTick(() => {
4746
+ const pos = start + replacement.length;
4747
+ input.setSelectionRange(pos, pos);
4748
+ });
4749
+ this.autocompleteSuggestions = [];
4750
+ },
4751
+ addPathFilter(path) {
4752
+ if (this.searchText) {
4753
+ if (this.searchText.endsWith('}')) {
4754
+ this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
4755
+ } else {
4756
+ this.searchText += `, ${path}: }`;
4757
+ }
4758
+
4759
+ } else {
4760
+ // If this.searchText is empty or undefined, initialize it with a new object
4761
+ this.searchText = `{ ${path}: }`;
4762
+ }
4763
+
4764
+
4765
+ this.$nextTick(() => {
4766
+ const input = this.$refs.searchInput;
4767
+ const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
4768
+
4769
+ input.focus();
4770
+ input.setSelectionRange(cursorIndex, cursorIndex);
4771
+ });
4772
+ }
4773
+ }
4774
+ });
4592
4775
 
4776
+
4777
+ /***/ }),
4778
+
4779
+ /***/ "./frontend/src/models/models.css":
4780
+ /*!****************************************!*\
4781
+ !*** ./frontend/src/models/models.css ***!
4782
+ \****************************************/
4783
+ /***/ ((module) => {
4784
+
4785
+ "use strict";
4786
+ module.exports = ".models {\n position: relative;\n display: flex;\n flex-direction: row;\n min-height: calc(100% - 56px);\n}\n\n.models button.gray {\n color: black;\n background-color: #eee;\n}\n\n.models .model-selector {\n background-color: #eee;\n flex-grow: 0;\n padding: 15px;\n padding-top: 0px;\n}\n\n.models h1 {\n margin-top: 0px;\n}\n\n.models .documents {\n flex-grow: 1;\n overflow: scroll;\n max-height: calc(100vh - 56px);\n}\n\n.models .documents table {\n /* max-width: -moz-fit-content;\n max-width: fit-content; */\n width: 100%;\n table-layout: auto;\n font-size: small;\n padding: 0;\n margin-right: 1em;\n white-space: nowrap;\n z-index: -1;\n border-collapse: collapse;\n line-height: 1.5em;\n}\n\n.models .documents table th {\n position: sticky;\n top: 42px;\n background-color: white;\n z-index: 1;\n}\n\n.models .documents table th:after {\n content: \"\";\n position: absolute;\n left: 0;\n width: 100%;\n bottom: -1px;\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n}\n\n.models .documents table tr {\n color: black;\n border-spacing: 0px 0px;\n background-color: white;\n cursor: pointer;\n}\n\n.models .documents table tr:nth-child(even) {\n background-color: #f5f5f5;\n}\n\n.models .documents table tr:hover {\n background-color: #a7b9ff;\n}\n\n.models .documents table th,\ntd {\n border-bottom: thin solid rgba(0, 0, 0, 0.12);\n text-align: left;\n padding: 0 16px;\n height: 48px;\n}\n\n.models textarea {\n font-size: 1.2em;\n}\n\n.models .path-type {\n color: rgba(0, 0, 0, 0.36);\n font-size: 0.8em;\n}\n\n.models .documents-menu {\n position: fixed;\n background-color: white;\n z-index: 1;\n padding: 4px;\n display: flex;\n width: 100vw;\n}\n\n@media (min-width: 1024px) {\n .models .documents-menu {\n width: calc(100vw - 12rem);\n }\n}\n\n.models .documents-menu .search-input {\n flex-grow: 1;\n align-items: center;\n}\n\n.models .search-input input {\n padding: 0.25em 0.5em;\n font-size: 1.1em;\n border: 1px solid #ddd;\n border-radius: 3px;\n width: calc(100% - 1em);\n}\n\n.models .sort-arrow {\n padding-left: 10px;\n padding-right: 10px;\n}\n\n.models .loader {\n width: 100%;\n text-align: center;\n}\n\n.models .loader img {\n height: 4em;\n}\n\n.models .documents .buttons {\n display: inline-flex;\n justify-content: space-around;\n align-items: center;\n}\n\n";
4787
+
4788
+ /***/ }),
4789
+
4790
+ /***/ "./frontend/src/models/models.html":
4791
+ /*!*****************************************!*\
4792
+ !*** ./frontend/src/models/models.html ***!
4793
+ \*****************************************/
4794
+ /***/ ((module) => {
4795
+
4796
+ "use strict";
4797
+ 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 <document-search\n ref=\"documentSearch\"\n :value=\"searchText\"\n :schema-paths=\"schemaPaths\"\n @search=\"search\"\n >\n </document-search>\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=\"addPathFilter(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)\" :references=\"referenceMap\">\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";
4798
+
4799
+ /***/ }),
4800
+
4801
+ /***/ "./frontend/src/models/models.js":
4802
+ /*!***************************************!*\
4803
+ !*** ./frontend/src/models/models.js ***!
4804
+ \***************************************/
4805
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
4806
+
4807
+ "use strict";
4808
+
4809
+
4810
+ const api = __webpack_require__(/*! ../api */ "./frontend/src/api.js");
4811
+ const template = __webpack_require__(/*! ./models.html */ "./frontend/src/models/models.html");
4812
+ const mpath = __webpack_require__(/*! mpath */ "./node_modules/mpath/index.js");
4813
+
4814
+ const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
4593
4815
  appendCSS(__webpack_require__(/*! ./models.css */ "./frontend/src/models/models.css"));
4594
4816
 
4595
4817
  const limit = 20;
@@ -4615,9 +4837,6 @@ module.exports = app => app.component('models', {
4615
4837
  selectMultiple: false,
4616
4838
  selectedDocuments: [],
4617
4839
  searchText: '',
4618
- autocompleteSuggestions: [],
4619
- autocompleteIndex: 0,
4620
- autocompleteTrie: null,
4621
4840
  shouldShowExportModal: false,
4622
4841
  shouldShowCreateModal: false,
4623
4842
  shouldShowFieldModal: false,
@@ -4636,7 +4855,6 @@ module.exports = app => app.component('models', {
4636
4855
  }),
4637
4856
  created() {
4638
4857
  this.currentModel = this.model;
4639
- this.buildAutocompleteTrie();
4640
4858
  this.loadOutputPreference();
4641
4859
  },
4642
4860
  beforeDestroy() {
@@ -4675,16 +4893,6 @@ module.exports = app => app.component('models', {
4675
4893
  }
4676
4894
  },
4677
4895
  methods: {
4678
- buildAutocompleteTrie() {
4679
- this.autocompleteTrie = new Trie();
4680
- this.autocompleteTrie.bulkInsert(QUERY_SELECTORS, 5);
4681
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
4682
- const paths = this.schemaPaths
4683
- .map(path => path?.path)
4684
- .filter(path => typeof path === 'string' && path.length > 0);
4685
- this.autocompleteTrie.bulkInsert(paths, 10);
4686
- }
4687
- },
4688
4896
  loadOutputPreference() {
4689
4897
  if (typeof window === 'undefined' || !window.localStorage) {
4690
4898
  return;
@@ -4760,141 +4968,6 @@ module.exports = app => app.component('models', {
4760
4968
  const { mongoDBIndexes } = await api.Model.dropIndex({ model: this.currentModel, name });
4761
4969
  this.mongoDBIndexes = mongoDBIndexes;
4762
4970
  },
4763
- initFilter(ev) {
4764
- if (!this.searchText) {
4765
- this.searchText = '{}';
4766
- this.$nextTick(() => {
4767
- ev.target.setSelectionRange(1, 1);
4768
- });
4769
- }
4770
- },
4771
- updateAutocomplete() {
4772
- const input = this.$refs.searchInput;
4773
- const cursorPos = input ? input.selectionStart : 0;
4774
- const before = this.searchText.slice(0, cursorPos);
4775
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
4776
- if (match && match[1]) {
4777
- const token = match[1];
4778
- const leadingQuoteMatch = token.match(/^["']/);
4779
- const trailingQuoteMatch = token.length > 1 && /["']$/.test(token)
4780
- ? token[token.length - 1]
4781
- : '';
4782
- const term = token
4783
- .replace(/^["']/, '')
4784
- .replace(trailingQuoteMatch ? new RegExp(`[${trailingQuoteMatch}]$`) : '', '')
4785
- .trim();
4786
- if (!term) {
4787
- this.autocompleteSuggestions = [];
4788
- return;
4789
- }
4790
- if (this.autocompleteTrie) {
4791
- const primarySuggestions = this.autocompleteTrie.getSuggestions(term, 10);
4792
- const suggestionsSet = new Set(primarySuggestions);
4793
- if (Array.isArray(this.schemaPaths) && this.schemaPaths.length > 0) {
4794
- for (const schemaPath of this.schemaPaths) {
4795
- const path = schemaPath?.path;
4796
- if (
4797
- typeof path === 'string' &&
4798
- path.startsWith(`${term}.`) &&
4799
- !suggestionsSet.has(path)
4800
- ) {
4801
- suggestionsSet.add(path);
4802
- if (suggestionsSet.size >= 10) {
4803
- break;
4804
- }
4805
- }
4806
- }
4807
- }
4808
- let suggestions = Array.from(suggestionsSet);
4809
- if (leadingQuoteMatch) {
4810
- const leadingQuote = leadingQuoteMatch[0];
4811
- suggestions = suggestions.map(suggestion => `${leadingQuote}${suggestion}`);
4812
- }
4813
- if (trailingQuoteMatch) {
4814
- suggestions = suggestions.map(suggestion =>
4815
- suggestion.endsWith(trailingQuoteMatch) ? suggestion : `${suggestion}${trailingQuoteMatch}`
4816
- );
4817
- }
4818
- this.autocompleteSuggestions = suggestions;
4819
- this.autocompleteIndex = 0;
4820
- return;
4821
- }
4822
- }
4823
- this.autocompleteSuggestions = [];
4824
- },
4825
- handleKeyDown(ev) {
4826
- if (this.autocompleteSuggestions.length === 0) {
4827
- return;
4828
- }
4829
- if (ev.key === 'Tab' || ev.key === 'Enter') {
4830
- ev.preventDefault();
4831
- this.applySuggestion(this.autocompleteIndex);
4832
- } else if (ev.key === 'ArrowDown') {
4833
- ev.preventDefault();
4834
- this.autocompleteIndex = (this.autocompleteIndex + 1) % this.autocompleteSuggestions.length;
4835
- } else if (ev.key === 'ArrowUp') {
4836
- ev.preventDefault();
4837
- this.autocompleteIndex = (this.autocompleteIndex + this.autocompleteSuggestions.length - 1) % this.autocompleteSuggestions.length;
4838
- }
4839
- },
4840
- applySuggestion(index) {
4841
- const suggestion = this.autocompleteSuggestions[index];
4842
- if (!suggestion) {
4843
- return;
4844
- }
4845
- const input = this.$refs.searchInput;
4846
- const cursorPos = input.selectionStart;
4847
- const before = this.searchText.slice(0, cursorPos);
4848
- const after = this.searchText.slice(cursorPos);
4849
- const match = before.match(/(?:\{|,)\s*([^:\s]*)$/);
4850
- const colonNeeded = !/^\s*:/.test(after);
4851
- if (!match) {
4852
- return;
4853
- }
4854
- const token = match[1];
4855
- const start = cursorPos - token.length;
4856
- let replacement = suggestion;
4857
- const leadingQuote = token.startsWith('"') || token.startsWith('\'') ? token[0] : '';
4858
- const trailingQuote = token.length > 1 && (token.endsWith('"') || token.endsWith('\'')) ? token[token.length - 1] : '';
4859
- if (leadingQuote && !replacement.startsWith(leadingQuote)) {
4860
- replacement = `${leadingQuote}${replacement}`;
4861
- }
4862
- if (trailingQuote && !replacement.endsWith(trailingQuote)) {
4863
- replacement = `${replacement}${trailingQuote}`;
4864
- }
4865
- // Only insert : if we know the user isn't entering in a nested path
4866
- if (colonNeeded && (!leadingQuote || trailingQuote)) {
4867
- replacement = `${replacement}:`;
4868
- }
4869
- this.searchText = this.searchText.slice(0, start) + replacement + after;
4870
- this.$nextTick(() => {
4871
- const pos = start + replacement.length;
4872
- input.setSelectionRange(pos, pos);
4873
- });
4874
- this.autocompleteSuggestions = [];
4875
- },
4876
- clickFilter(path) {
4877
- if (this.searchText) {
4878
- if (this.searchText.endsWith('}')) {
4879
- this.searchText = this.searchText.slice(0, -1) + `, ${path}: }`;
4880
- } else {
4881
- this.searchText += `, ${path}: }`;
4882
- }
4883
-
4884
- } else {
4885
- // If this.searchText is empty or undefined, initialize it with a new object
4886
- this.searchText = `{ ${path}: }`;
4887
- }
4888
-
4889
-
4890
- this.$nextTick(() => {
4891
- const input = this.$refs.searchInput;
4892
- const cursorIndex = this.searchText.lastIndexOf(':') + 2; // Move cursor after ": "
4893
-
4894
- input.focus();
4895
- input.setSelectionRange(cursorIndex, cursorIndex);
4896
- });
4897
- },
4898
4971
  async closeCreationModal() {
4899
4972
  this.shouldShowCreateModal = false;
4900
4973
  await this.getDocuments();
@@ -4944,7 +5017,8 @@ module.exports = app => app.component('models', {
4944
5017
  }
4945
5018
  await this.loadMoreDocuments();
4946
5019
  },
4947
- async search() {
5020
+ async search(searchText) {
5021
+ this.searchText = searchText;
4948
5022
  const hasSearch = typeof this.searchText === 'string' && this.searchText.trim().length > 0;
4949
5023
  if (hasSearch) {
4950
5024
  this.query.search = this.searchText;
@@ -4959,6 +5033,11 @@ module.exports = app => app.component('models', {
4959
5033
  await this.loadMoreDocuments();
4960
5034
  this.status = 'loaded';
4961
5035
  },
5036
+ addPathFilter(path) {
5037
+ if (this.$refs.documentSearch?.addPathFilter) {
5038
+ this.$refs.documentSearch.addPathFilter(path);
5039
+ }
5040
+ },
4962
5041
  async openIndexModal() {
4963
5042
  this.shouldShowIndexModal = true;
4964
5043
  const { mongoDBIndexes, schemaIndexes } = await api.Model.getIndexes({ model: this.currentModel });
@@ -4978,7 +5057,6 @@ module.exports = app => app.component('models', {
4978
5057
  // Clear previous data
4979
5058
  this.documents = [];
4980
5059
  this.schemaPaths = [];
4981
- this.buildAutocompleteTrie();
4982
5060
  this.numDocuments = null;
4983
5061
  this.loadedAllDocs = false;
4984
5062
  this.lastSelectedIndex = null;
@@ -5006,7 +5084,6 @@ module.exports = app => app.component('models', {
5006
5084
  }
5007
5085
  this.filteredPaths = [...this.schemaPaths];
5008
5086
  this.selectedPaths = [...this.schemaPaths];
5009
- this.buildAutocompleteTrie();
5010
5087
  schemaPathsReceived = true;
5011
5088
  }
5012
5089
  if (event.numDocs !== undefined) {
@@ -5236,6 +5313,7 @@ class TrieNode {
5236
5313
  this.children = Object.create(null);
5237
5314
  this.isEnd = false;
5238
5315
  this.freq = 0;
5316
+ this.roles = new Set(); // semantic roles like 'fieldName', 'operator'
5239
5317
  }
5240
5318
  }
5241
5319
 
@@ -5244,7 +5322,7 @@ class Trie {
5244
5322
  this.root = new TrieNode();
5245
5323
  }
5246
5324
 
5247
- insert(word, freq = 1) {
5325
+ insert(word, freq = 1, role = null) {
5248
5326
  if (!word) {
5249
5327
  return;
5250
5328
  }
@@ -5257,27 +5335,25 @@ class Trie {
5257
5335
  }
5258
5336
  node.isEnd = true;
5259
5337
  node.freq += freq;
5338
+ if (role) {
5339
+ node.roles.add(role);
5340
+ }
5260
5341
  }
5261
5342
 
5262
- bulkInsert(words, freq = 1) {
5263
- if (!Array.isArray(words)) {
5264
- return;
5265
- }
5266
- for (const word of words) {
5267
- this.insert(word, freq);
5268
- }
5343
+ bulkInsert(words, freq = 1, role = null) {
5344
+ for (const word of words) this.insert(word, freq, role);
5269
5345
  }
5270
5346
 
5271
- collect(node, prefix, out) {
5272
- if (node.isEnd) {
5347
+ collect(node, prefix, out, role) {
5348
+ if (node.isEnd && (role == null || node.roles.has(role))) {
5273
5349
  out.push([prefix, node.freq]);
5274
5350
  }
5275
5351
  for (const [ch, child] of Object.entries(node.children)) {
5276
- this.collect(child, prefix + ch, out);
5352
+ this.collect(child, prefix + ch, out, role);
5277
5353
  }
5278
5354
  }
5279
5355
 
5280
- suggest(prefix, limit = 10) {
5356
+ suggest(prefix, limit = 10, role = null) {
5281
5357
  let node = this.root;
5282
5358
  for (const ch of prefix) {
5283
5359
  if (!node.children[ch]) {
@@ -5286,19 +5362,19 @@ class Trie {
5286
5362
  node = node.children[ch];
5287
5363
  }
5288
5364
  const results = [];
5289
- this.collect(node, prefix, results);
5365
+ this.collect(node, prefix, results, role);
5290
5366
  results.sort((a, b) => b[1] - a[1]);
5291
5367
  return results.slice(0, limit).map(([word]) => word);
5292
5368
  }
5293
5369
 
5294
- fuzzySuggest(prefix, limit = 10) {
5370
+ fuzzySuggest(prefix, limit = 10, role = null) {
5295
5371
  const results = new Set();
5296
5372
 
5297
5373
  const dfs = (node, path, edits) => {
5298
5374
  if (edits > 1) {
5299
5375
  return;
5300
5376
  }
5301
- if (node.isEnd && Math.abs(path.length - prefix.length) <= 1) {
5377
+ if (node.isEnd && Math.abs(path.length - prefix.length) <= 1 && (role == null || node.roles.has(role))) {
5302
5378
  const dist = levenshtein(prefix, path);
5303
5379
  if (dist <= 1) {
5304
5380
  results.add(path);
@@ -5314,17 +5390,44 @@ class Trie {
5314
5390
  return Array.from(results).slice(0, limit);
5315
5391
  }
5316
5392
 
5317
- getSuggestions(prefix, limit = 10) {
5393
+ getSuggestions(prefix, limit = 10, role = null) {
5318
5394
  if (!prefix) {
5319
5395
  return [];
5320
5396
  }
5321
- const exact = this.suggest(prefix, limit);
5397
+ const exact = this.suggest(prefix, limit, role);
5322
5398
  if (exact.length >= limit) {
5323
5399
  return exact;
5324
5400
  }
5325
- const fuzzy = this.fuzzySuggest(prefix, limit - exact.length);
5401
+ const fuzzy = this.fuzzySuggest(prefix, limit - exact.length, role);
5326
5402
  return [...exact, ...fuzzy];
5327
5403
  }
5404
+
5405
+ toString() {
5406
+ const lines = [];
5407
+ function dfs(node, prefix, depth) {
5408
+ let line = ' '.repeat(depth);
5409
+ if (prefix.length > 0) {
5410
+ line += prefix[prefix.length - 1];
5411
+ } else {
5412
+ line += '(root)';
5413
+ }
5414
+ if (node.isEnd) {
5415
+ line += ' *';
5416
+ }
5417
+ if (node.roles.size > 0) {
5418
+ line += ' [' + Array.from(node.roles).join(',') + ']';
5419
+ }
5420
+ if (node.freq > 0) {
5421
+ line += ` {freq:${node.freq}}`;
5422
+ }
5423
+ lines.push(line);
5424
+ for (const ch of Object.keys(node.children).sort()) {
5425
+ dfs(node.children[ch], prefix + ch, depth + 1);
5426
+ }
5427
+ }
5428
+ dfs(this.root, '', 0);
5429
+ return lines.join('\n');
5430
+ }
5328
5431
  }
5329
5432
 
5330
5433
  function levenshtein(a, b) {
@@ -16606,7 +16709,7 @@ module.exports = function stringToParts(str) {
16606
16709
  /***/ ((module) => {
16607
16710
 
16608
16711
  "use strict";
16609
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.6","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.2.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 || ^9.0.0-0"},"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":"9.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"}}');
16712
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.1.7","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.2.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 || ^9.0.0-0"},"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":"9.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"}}');
16610
16713
 
16611
16714
  /***/ })
16612
16715