@mongoosejs/studio 0.0.130 → 0.0.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend/public/app.js +96 -28
- package/frontend/public/tw.css +19 -0
- package/frontend/src/document/confirm-delete/confirm-delete.html +17 -17
- package/frontend/src/document-details/document-details.html +21 -8
- package/frontend/src/document-details/document-details.js +89 -22
- package/frontend/src/document-details/document-property/document-property.html +11 -10
- package/frontend/src/document-details/document-property/document-property.js +2 -2
- package/frontend/src/modal/modal.css +8 -0
- package/package.json +1 -1
package/frontend/public/app.js
CHANGED
|
@@ -1639,38 +1639,39 @@ module.exports = app => app.component('document-details', {
|
|
|
1639
1639
|
shouldUseDatePicker() {
|
|
1640
1640
|
return this.fieldData.type === 'Date';
|
|
1641
1641
|
},
|
|
1642
|
-
|
|
1642
|
+
typeFilteredSchemaPaths() {
|
|
1643
1643
|
let paths = this.schemaPaths || [];
|
|
1644
1644
|
|
|
1645
|
-
// Filter by search query
|
|
1646
|
-
if (this.searchQuery.trim()) {
|
|
1647
|
-
const query = this.searchQuery.toLowerCase();
|
|
1648
|
-
paths = paths.filter(path =>
|
|
1649
|
-
path.path.toLowerCase().includes(query)
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
// Filter by data type
|
|
1654
1645
|
if (this.selectedType) {
|
|
1655
|
-
paths = paths.filter(path =>
|
|
1656
|
-
path.instance === this.selectedType
|
|
1657
|
-
);
|
|
1646
|
+
paths = paths.filter(path => path.instance === this.selectedType);
|
|
1658
1647
|
}
|
|
1659
1648
|
|
|
1660
1649
|
return paths;
|
|
1661
1650
|
},
|
|
1662
|
-
|
|
1663
|
-
|
|
1651
|
+
filteredSchemaPaths() {
|
|
1652
|
+
const paths = this.typeFilteredSchemaPaths.slice();
|
|
1664
1653
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
const query = this.searchQuery.toLowerCase();
|
|
1668
|
-
virtuals = virtuals.filter(virtual =>
|
|
1669
|
-
virtual.name.toLowerCase().includes(query)
|
|
1670
|
-
);
|
|
1654
|
+
if (!this.searchQuery.trim()) {
|
|
1655
|
+
return paths;
|
|
1671
1656
|
}
|
|
1672
1657
|
|
|
1673
|
-
|
|
1658
|
+
const query = this.searchQuery.toLowerCase();
|
|
1659
|
+
const matches = [];
|
|
1660
|
+
const nonMatches = [];
|
|
1661
|
+
|
|
1662
|
+
paths.forEach(path => {
|
|
1663
|
+
if (path.path.toLowerCase().includes(query)) {
|
|
1664
|
+
matches.push(path);
|
|
1665
|
+
} else {
|
|
1666
|
+
nonMatches.push(path);
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
return matches.concat(nonMatches);
|
|
1671
|
+
},
|
|
1672
|
+
typeFilteredVirtuals() {
|
|
1673
|
+
let virtuals = this.virtuals;
|
|
1674
|
+
|
|
1674
1675
|
if (this.selectedType) {
|
|
1675
1676
|
virtuals = virtuals.filter(virtual => {
|
|
1676
1677
|
const virtualType = this.getVirtualFieldType(virtual);
|
|
@@ -1680,6 +1681,58 @@ module.exports = app => app.component('document-details', {
|
|
|
1680
1681
|
|
|
1681
1682
|
return virtuals;
|
|
1682
1683
|
},
|
|
1684
|
+
filteredVirtuals() {
|
|
1685
|
+
const virtuals = this.typeFilteredVirtuals.slice();
|
|
1686
|
+
|
|
1687
|
+
if (!this.searchQuery.trim()) {
|
|
1688
|
+
return virtuals;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
const query = this.searchQuery.toLowerCase();
|
|
1692
|
+
const matches = [];
|
|
1693
|
+
const nonMatches = [];
|
|
1694
|
+
|
|
1695
|
+
virtuals.forEach(virtual => {
|
|
1696
|
+
if (virtual.name.toLowerCase().includes(query)) {
|
|
1697
|
+
matches.push(virtual);
|
|
1698
|
+
} else {
|
|
1699
|
+
nonMatches.push(virtual);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
return matches.concat(nonMatches);
|
|
1704
|
+
},
|
|
1705
|
+
schemaSearchMatchSet() {
|
|
1706
|
+
if (!this.searchQuery.trim()) {
|
|
1707
|
+
return new Set();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const query = this.searchQuery.toLowerCase();
|
|
1711
|
+
return new Set(
|
|
1712
|
+
this.typeFilteredSchemaPaths
|
|
1713
|
+
.filter(path => path.path.toLowerCase().includes(query))
|
|
1714
|
+
.map(path => path.path)
|
|
1715
|
+
);
|
|
1716
|
+
},
|
|
1717
|
+
virtualSearchMatchSet() {
|
|
1718
|
+
if (!this.searchQuery.trim()) {
|
|
1719
|
+
return new Set();
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
const query = this.searchQuery.toLowerCase();
|
|
1723
|
+
return new Set(
|
|
1724
|
+
this.typeFilteredVirtuals
|
|
1725
|
+
.filter(virtual => virtual.name.toLowerCase().includes(query))
|
|
1726
|
+
.map(virtual => virtual.name)
|
|
1727
|
+
);
|
|
1728
|
+
},
|
|
1729
|
+
hasSearchMatches() {
|
|
1730
|
+
if (!this.searchQuery.trim()) {
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
return this.schemaSearchMatchSet.size > 0 || this.virtualSearchMatchSet.size > 0;
|
|
1735
|
+
},
|
|
1683
1736
|
formattedJson() {
|
|
1684
1737
|
if (!this.document) {
|
|
1685
1738
|
return '{}';
|
|
@@ -1695,6 +1748,20 @@ module.exports = app => app.component('document-details', {
|
|
|
1695
1748
|
this.collapsedVirtuals.add(fieldName);
|
|
1696
1749
|
}
|
|
1697
1750
|
},
|
|
1751
|
+
isSchemaPathMatched(path) {
|
|
1752
|
+
if (!path) {
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
return this.schemaSearchMatchSet.has(path.path);
|
|
1757
|
+
},
|
|
1758
|
+
isVirtualMatched(virtual) {
|
|
1759
|
+
if (!virtual) {
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
return this.virtualSearchMatchSet.has(virtual.name);
|
|
1764
|
+
},
|
|
1698
1765
|
isVirtualFieldCollapsed(fieldName) {
|
|
1699
1766
|
return this.collapsedVirtuals.has(fieldName);
|
|
1700
1767
|
},
|
|
@@ -1909,7 +1976,7 @@ module.exports = app => app.component('document-property', {
|
|
|
1909
1976
|
isValueExpanded: false // Track if the value is expanded
|
|
1910
1977
|
};
|
|
1911
1978
|
},
|
|
1912
|
-
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid'],
|
|
1979
|
+
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight'],
|
|
1913
1980
|
computed: {
|
|
1914
1981
|
valueAsString() {
|
|
1915
1982
|
const value = this.getValueForPath(this.path.path);
|
|
@@ -1989,6 +2056,7 @@ module.exports = app => app.component('document-property', {
|
|
|
1989
2056
|
}
|
|
1990
2057
|
});
|
|
1991
2058
|
|
|
2059
|
+
|
|
1992
2060
|
/***/ }),
|
|
1993
2061
|
|
|
1994
2062
|
/***/ "./frontend/src/document/confirm-changes/confirm-changes.js":
|
|
@@ -5376,7 +5444,7 @@ module.exports = ".document-details {\n width: 100%;\n}\n\n.document-details .v
|
|
|
5376
5444
|
/***/ ((module) => {
|
|
5377
5445
|
|
|
5378
5446
|
"use strict";
|
|
5379
|
-
module.exports = "<div class=\"document-details\">\n <!-- View Toggle and Search/Filter Bar -->\n <div class=\"mb-4 mt-4\">\n\n <!-- Search and Filter Bar (only show in fields view) -->\n <div v-if=\"viewMode === 'fields'\" class=\"flex md:gap-3\">\n <!-- Search Bar -->\n <div class=\"relative flex-1\">\n <input\n ref=\"searchInput\"\n v-model=\"searchQuery\"\n type=\"text\"\n placeholder=\"Search fields...\"\n class=\"w-full px-4 py-2 pl-10 pr-4 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n />\n <div class=\"absolute inset-y-0 left-0 flex items-center pl-3\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Data Type Filter -->\n <div class=\"relative\">\n <select\n v-model=\"selectedType\"\n class=\"hidden md:block px-4 py-2 pr-8 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white min-w-[140px] appearance-none\"\n >\n <option value=\"\">All Types</option>\n <option v-for=\"type in availableTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <div class=\"absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Add Field Button -->\n <button\n @click=\"openAddFieldModal\"\n class=\"hidden md:block px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 flex items-center gap-2\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\"></path>\n </svg>\n Add Field\n </button>\n </div>\n </div>\n\n <!-- Fields View -->\n <div v-if=\"viewMode === 'fields'\">\n <!-- Schema Paths -->\n <div v-for=\"path in filteredSchemaPaths\" :key=\"path\" class=\"value\">\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Virtual Fields -->\n <div v-for=\"path in filteredVirtuals\" class=\"border border-gray-200 rounded-lg mb-2\">\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-50 hover:bg-slate-100 cursor-pointer flex items-center justify-between border-b border-gray-200\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.name}}</span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- No Results Message -->\n <div v-if=\"searchQuery && filteredSchemaPaths.length === 0 && filteredVirtuals.length === 0\" class=\"text-center py-8 text-gray-500\">\n <svg class=\"w-12 h-12 mx-auto mb-4 text-gray-300\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 6.291A7.962 7.962 0 0012 5c-2.34 0-4.29 1.009-5.824 2.709\"></path>\n </svg>\n <p>No fields found matching \"{{searchQuery}}\"</p>\n </div>\n </div>\n\n <!-- JSON View -->\n <div v-if=\"viewMode === 'json'\" class=\"json-view\">\n <div class=\"border border-gray-300 rounded-lg bg-gray-50 p-4 overflow-auto\">\n <pre class=\"text-sm font-mono text-gray-800 whitespace-pre\">{{formattedJson}}</pre>\n </div>\n </div>\n\n <!-- Add Field Modal -->\n <modal v-if=\"showAddFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeAddFieldModal\">×</div>\n <div class=\"add-field-modal\">\n <div class=\"mb-6\">\n <h2 class=\"text-xl font-semibold text-gray-900 mb-2\">Add New Field</h2>\n <p class=\"text-sm text-gray-600\">Create a new field for this document</p>\n </div>\n\n <form @submit.prevent=\"handleAddFieldSubmit\" class=\"space-y-4\">\n <!-- Field Name -->\n <div>\n <label for=\"fieldName\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Field Name *\n </label>\n <input\n id=\"fieldName\"\n v-model=\"fieldData.name\"\n type=\"text\"\n required\n placeholder=\"Enter field name...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.name }\"\n />\n <p v-if=\"fieldErrors.name\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.name}}</p>\n <p v-if=\"fieldData.name && getTransformedFieldName() !== fieldData.name.trim()\" class=\"mt-1 text-sm text-blue-600\">\n Field name will be: <code class=\"bg-blue-50 px-1 py-0.5 rounded text-xs\">{{getTransformedFieldName()}}</code>\n </p>\n </div>\n\n <!-- Field Type -->\n <div>\n <label for=\"fieldType\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Field Type *\n </label>\n <select\n id=\"fieldType\"\n v-model=\"fieldData.type\"\n required\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.type }\"\n >\n <option value=\"\">Select field type...</option>\n <option v-for=\"type in allFieldTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <p v-if=\"fieldErrors.type\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.type}}</p>\n </div>\n\n <!-- Field Value -->\n <div>\n <label for=\"fieldValue\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Initial Value\n </label>\n\n <!-- Date picker for Date type -->\n <input\n v-if=\"shouldUseDatePicker\"\n v-model=\"fieldData.value\"\n type=\"date\"\n id=\"fieldValue\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- Simple input for basic types -->\n <input\n v-else-if=\"!shouldUseCodeMirror\"\n v-model=\"fieldData.value\"\n type=\"text\"\n id=\"fieldValue\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- CodeMirror textarea for complex types -->\n <textarea\n v-else\n ref=\"fieldValueEditor\"\n id=\"fieldValue\"\n rows=\"3\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n ></textarea>\n\n <p v-if=\"fieldErrors.value\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.value}}</p>\n <p class=\"mt-1 text-xs text-gray-500\">\n <span v-if=\"shouldUseDatePicker\">Select a date or leave empty for null/undefined values.</span>\n <span v-else-if=\"shouldUseCodeMirror\">Leave empty for null/undefined values. Use valid JSON format.</span>\n <span v-else>Leave empty for null/undefined values.</span>\n </p>\n </div>\n\n\n <!-- Action Buttons -->\n <div class=\"flex justify-end gap-3 pt-4 border-t border-gray-200\">\n <button\n type=\"button\"\n @click=\"closeAddFieldModal\"\n class=\"px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n :disabled=\"isSubmittingField\"\n class=\"px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <span v-if=\"isSubmittingField\">Adding...</span>\n <span v-else>Add Field</span>\n </button>\n </div>\n </form>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
5447
|
+
module.exports = "<div class=\"document-details\">\n <!-- View Toggle and Search/Filter Bar -->\n <div class=\"mb-4 mt-4\">\n\n <!-- Search and Filter Bar (only show in fields view) -->\n <div v-if=\"viewMode === 'fields'\" class=\"flex md:gap-3\">\n <!-- Search Bar -->\n <div class=\"relative flex-1\">\n <input\n ref=\"searchInput\"\n v-model=\"searchQuery\"\n type=\"text\"\n placeholder=\"Search fields...\"\n class=\"w-full px-4 py-2 pl-10 pr-4 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n />\n <div class=\"absolute inset-y-0 left-0 flex items-center pl-3\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Data Type Filter -->\n <div class=\"relative hidden md:block\">\n <select\n v-model=\"selectedType\"\n class=\"px-4 py-2 pr-8 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white min-w-[140px] appearance-none\"\n >\n <option value=\"\">All Types</option>\n <option v-for=\"type in availableTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <div class=\"absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none\">\n <svg class=\"w-4 h-4 text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n </div>\n </div>\n\n <!-- Add Field Button -->\n <button\n @click=\"openAddFieldModal\"\n class=\"hidden md:flex px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 items-center gap-2\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 4v16m8-8H4\"></path>\n </svg> Add Field\n </button>\n </div>\n </div>\n\n <!-- Fields View -->\n <div v-if=\"viewMode === 'fields'\">\n <!-- Schema Paths -->\n <div\n v-for=\"path in filteredSchemaPaths\"\n :key=\"path.path || path\"\n class=\"value\"\n >\n <document-property\n :path=\"path\"\n :document=\"document\"\n :schemaPaths=\"schemaPaths\"\n :editting=\"editting\"\n :changes=\"changes\"\n :highlight=\"isSchemaPathMatched(path)\"\n :invalid=\"invalid\"></document-property>\n </div>\n\n <!-- Virtual Fields -->\n <div\n v-for=\"path in filteredVirtuals\"\n :key=\"path.name\"\n class=\"border rounded-lg mb-2 transition-all duration-200 ease-in-out\"\n :class=\"[\n isVirtualMatched(path)\n ? 'border-amber-400 ring-2 ring-amber-200 ring-offset-1'\n : 'border-gray-200'\n ]\"\n >\n <!-- Virtual Field Header (Always Visible) -->\n <div\n @click=\"toggleVirtualField(path.name)\"\n class=\"p-3 bg-slate-50 hover:bg-slate-100 cursor-pointer flex items-center justify-between border-b border-gray-200\"\n >\n <div class=\"flex items-center\">\n <svg\n :class=\"isVirtualFieldCollapsed(path.name) ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.name}}</span>\n <span v-if=\"path.isVirtual\" class=\"ml-2 text-sm text-purple-600\">(virtual - {{getVirtualFieldType(path)}})</span>\n <span v-else class=\"ml-2 text-sm text-blue-600\">(user-added - {{getVirtualFieldType(path)}})</span>\n </div>\n </div>\n\n <!-- Virtual Field Content (Collapsible) -->\n <div v-if=\"!isVirtualFieldCollapsed(path.name)\" class=\"p-3\">\n <div v-if=\"path.value == null\" class=\"text-sky-800\">\n {{'' + path.value}}\n </div>\n <div v-else>\n {{path.value}}\n </div>\n </div>\n </div>\n\n <!-- No Results Message -->\n <div v-if=\"searchQuery.trim() && !hasSearchMatches\" class=\"text-center py-8 text-gray-500\">\n <svg class=\"w-12 h-12 mx-auto mb-4 text-gray-300\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 6.291A7.962 7.962 0 0012 5c-2.34 0-4.29 1.009-5.824 2.709\"></path>\n </svg>\n <p>No fields found matching \"{{searchQuery}}\"</p>\n </div>\n </div>\n\n <!-- JSON View -->\n <div v-if=\"viewMode === 'json'\" class=\"json-view\">\n <div class=\"border border-gray-300 rounded-lg bg-gray-50 p-4 overflow-auto\">\n <pre class=\"text-sm font-mono text-gray-800 whitespace-pre\">{{formattedJson}}</pre>\n </div>\n </div>\n\n <!-- Add Field Modal -->\n <modal v-if=\"showAddFieldModal\">\n <template v-slot:body>\n <div class=\"modal-exit\" @click=\"closeAddFieldModal\">×</div>\n <div class=\"add-field-modal\">\n <div class=\"mb-6\">\n <h2 class=\"text-xl font-semibold text-gray-900 mb-2\">Add New Field</h2>\n <p class=\"text-sm text-gray-600\">Create a new field for this document</p>\n </div>\n\n <form @submit.prevent=\"handleAddFieldSubmit\" class=\"space-y-4\">\n <!-- Field Name -->\n <div>\n <label for=\"fieldName\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Field Name *\n </label>\n <input\n id=\"fieldName\"\n v-model=\"fieldData.name\"\n type=\"text\"\n required\n placeholder=\"Enter field name...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.name }\"\n />\n <p v-if=\"fieldErrors.name\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.name}}</p>\n <p v-if=\"fieldData.name && getTransformedFieldName() !== fieldData.name.trim()\" class=\"mt-1 text-sm text-blue-600\">\n Field name will be: <code class=\"bg-blue-50 px-1 py-0.5 rounded text-xs\">{{getTransformedFieldName()}}</code>\n </p>\n </div>\n\n <!-- Field Type -->\n <div>\n <label for=\"fieldType\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Field Type *\n </label>\n <select\n id=\"fieldType\"\n v-model=\"fieldData.type\"\n required\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.type }\"\n >\n <option value=\"\">Select field type...</option>\n <option v-for=\"type in allFieldTypes\" :key=\"type\" :value=\"type\">\n {{type}}\n </option>\n </select>\n <p v-if=\"fieldErrors.type\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.type}}</p>\n </div>\n\n <!-- Field Value -->\n <div>\n <label for=\"fieldValue\" class=\"block text-sm font-medium text-gray-700 mb-1\">\n Initial Value\n </label>\n\n <!-- Date picker for Date type -->\n <input\n v-if=\"shouldUseDatePicker\"\n v-model=\"fieldData.value\"\n type=\"date\"\n id=\"fieldValue\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- Simple input for basic types -->\n <input\n v-else-if=\"!shouldUseCodeMirror\"\n v-model=\"fieldData.value\"\n type=\"text\"\n id=\"fieldValue\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n />\n\n <!-- CodeMirror textarea for complex types -->\n <textarea\n v-else\n ref=\"fieldValueEditor\"\n id=\"fieldValue\"\n rows=\"3\"\n placeholder=\"Enter initial value (optional)...\"\n class=\"w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent\"\n :class=\"{ 'border-red-500': fieldErrors.value }\"\n ></textarea>\n\n <p v-if=\"fieldErrors.value\" class=\"mt-1 text-sm text-red-600\">{{fieldErrors.value}}</p>\n <p class=\"mt-1 text-xs text-gray-500\">\n <span v-if=\"shouldUseDatePicker\">Select a date or leave empty for null/undefined values.</span>\n <span v-else-if=\"shouldUseCodeMirror\">Leave empty for null/undefined values. Use valid JSON format.</span>\n <span v-else>Leave empty for null/undefined values.</span>\n </p>\n </div>\n\n\n <!-- Action Buttons -->\n <div class=\"flex justify-end gap-3 pt-4 border-t border-gray-200\">\n <button\n type=\"button\"\n @click=\"closeAddFieldModal\"\n class=\"px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n :disabled=\"isSubmittingField\"\n class=\"px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <span v-if=\"isSubmittingField\">Adding...</span>\n <span v-else>Add Field</span>\n </button>\n </div>\n </form>\n </div>\n </template>\n </modal>\n</div>\n";
|
|
5380
5448
|
|
|
5381
5449
|
/***/ }),
|
|
5382
5450
|
|
|
@@ -5398,7 +5466,7 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
|
|
|
5398
5466
|
/***/ ((module) => {
|
|
5399
5467
|
|
|
5400
5468
|
"use strict";
|
|
5401
|
-
module.exports = "<div class=\"border border-gray-200 rounded-lg mb-2\">\n <!-- Collapsible Header -->\n <div
|
|
5469
|
+
module.exports = "<div class=\"border border-gray-200 rounded-lg mb-2\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-3 hover:bg-gray-100 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100': highlight, 'bg-gray-50': !highlight }\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-gray-500\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-3\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <component\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n @input=\"changes[path.path] = $event; delete invalid[path.path];\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <div v-if=\"needsTruncation && !isValueExpanded\" class=\"truncated-value-container\">\n <div class=\"text-gray-700 whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"expanded-value-container\">\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <div v-else>\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n </div>\n </div>\n </div>\n</div>\n";
|
|
5402
5470
|
|
|
5403
5471
|
/***/ }),
|
|
5404
5472
|
|
|
@@ -5420,7 +5488,7 @@ module.exports = "<div>\n <h2>\n Are you sure you want to save the fol
|
|
|
5420
5488
|
/***/ ((module) => {
|
|
5421
5489
|
|
|
5422
5490
|
"use strict";
|
|
5423
|
-
module.exports = "<div>\n
|
|
5491
|
+
module.exports = "<div>\n <h2>\n Are you sure you want to delete the following document?\n </h2>\n <pre class=\"max-h-[50vh] overflow-auto\"><code ref=\"code\" class=\"language-javascript\" v-text=\"displayValue\"></code></pre>\n <div class=\"flex gap-2 mt-2\">\n <async-button\n class=\"rounded-md bg-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\"\n @click=\"startDelete\">\n Confirm\n </async-button>\n <button\n class=\"rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-semibold text-black shadow-sm hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300\"\n @click=\"closeDelete\">\n Cancel\n </button>\n </div>\n</div>\n";
|
|
5424
5492
|
|
|
5425
5493
|
/***/ }),
|
|
5426
5494
|
|
|
@@ -5684,7 +5752,7 @@ module.exports = "<div class=\"list-subdocument tooltip\">\n <pre>\n <code r
|
|
|
5684
5752
|
/***/ ((module) => {
|
|
5685
5753
|
|
|
5686
5754
|
"use strict";
|
|
5687
|
-
module.exports = "/** Vue modal */\n\n.modal-mask {\n position: fixed;\n z-index: 9998;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.5);\n display: table;\n transition: opacity 0.3s ease;\n}\n\n.modal-wrapper {\n display: table-cell;\n vertical-align: middle;\n}\n\n.modal-container {\n width: 600px;\n margin: 0px auto;\n padding: 20px 30px;\n padding-bottom: 40px;\n background-color: #fff;\n border-radius: 2px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);\n transition: all 0.3s ease;\n font-family: Helvetica, Arial, sans-serif;\n position: relative;\n}\n\n.modal-header {\n margin-top: 0;\n font-size: 18px;\n font-weight: bold;\n}\n\n.modal-header-success {\n color: #42b983;\n}\n\n.modal-header-error {\n color: #ff0000;\n}\n\n.modal-body {\n margin: 20px 0;\n max-height: calc(100vh - 40px - 60px - 10px);\n overflow: auto;\n}\n\n.modal__button--default {\n float: right;\n}\n\n/*\n * The following styles are auto-applied to elements with\n * transition=\"modal\" when their visibility is toggled\n * by Vue.js.\n *\n * You can easily play with the modal transition by editing\n * these styles.\n */\n\n.modal-enter {\n opacity: 0;\n}\n\n.modal-leave-active {\n opacity: 0;\n}\n\n.modal-enter .modal-container,\n.modal-leave-active .modal-container {\n -webkit-transform: scale(1.1);\n transform: scale(1.1);\n}\n\n.modal-container .modal-exit {\n position: absolute;\n right: 0.25em;\n top: 0.25em;\n cursor: pointer;\n font-size: 1.25em;\n height: 1.25em;\n width: 1.25em;\n border-radius: 100%;\n border: 1px solid #ddd;\n display: flex;\n align-items: center;\n justify-content: center;\n padding-bottom: 0.25em;\n}\n\n.modal-container .modal-exit:hover {\n background-color: #f1f5ff;\n}\n";
|
|
5755
|
+
module.exports = "/** Vue modal */\n\n.modal-mask {\n position: fixed;\n z-index: 9998;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.5);\n display: table;\n transition: opacity 0.3s ease;\n}\n\n.modal-wrapper {\n display: table-cell;\n vertical-align: middle;\n}\n\n.modal-container {\n width: 600px;\n margin: 0px auto;\n padding: 20px 30px;\n padding-bottom: 40px;\n background-color: #fff;\n border-radius: 2px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);\n transition: all 0.3s ease;\n font-family: Helvetica, Arial, sans-serif;\n position: relative;\n}\n\n.modal-header {\n margin-top: 0;\n font-size: 18px;\n font-weight: bold;\n}\n\n.modal-header-success {\n color: #42b983;\n}\n\n.modal-header-error {\n color: #ff0000;\n}\n\n.modal-body {\n margin: 20px 0;\n max-height: calc(100vh - 40px - 60px - 10px);\n overflow: auto;\n}\n\n.modal__button--default {\n float: right;\n}\n\n/*\n * The following styles are auto-applied to elements with\n * transition=\"modal\" when their visibility is toggled\n * by Vue.js.\n *\n * You can easily play with the modal transition by editing\n * these styles.\n */\n\n.modal-enter {\n opacity: 0;\n}\n\n.modal-leave-active {\n opacity: 0;\n}\n\n.modal-enter .modal-container,\n.modal-leave-active .modal-container {\n -webkit-transform: scale(1.1);\n transform: scale(1.1);\n}\n\n.modal-container .modal-exit {\n position: absolute;\n right: 0.25em;\n top: 0.25em;\n cursor: pointer;\n font-size: 1.25em;\n height: 1.25em;\n width: 1.25em;\n border-radius: 100%;\n border: 1px solid #ddd;\n display: flex;\n align-items: center;\n justify-content: center;\n padding-bottom: 0.25em;\n}\n\n.modal-container .modal-exit:hover {\n background-color: #f1f5ff;\n}\n\n@media (max-width: 767px) {\n .modal-container {\n width: calc(100vw - 10px);\n margin: 0;\n margin-left: 5px;\n }\n}\n";
|
|
5688
5756
|
|
|
5689
5757
|
/***/ }),
|
|
5690
5758
|
|
|
@@ -15835,7 +15903,7 @@ var bson = /*#__PURE__*/Object.freeze({
|
|
|
15835
15903
|
/***/ ((module) => {
|
|
15836
15904
|
|
|
15837
15905
|
"use strict";
|
|
15838
|
-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.
|
|
15906
|
+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.0.131","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"},"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"}}');
|
|
15839
15907
|
|
|
15840
15908
|
/***/ })
|
|
15841
15909
|
|
package/frontend/public/tw.css
CHANGED
|
@@ -1399,6 +1399,11 @@ video {
|
|
|
1399
1399
|
border-style: none;
|
|
1400
1400
|
}
|
|
1401
1401
|
|
|
1402
|
+
.border-amber-400 {
|
|
1403
|
+
--tw-border-opacity: 1;
|
|
1404
|
+
border-color: rgb(251 191 36 / var(--tw-border-opacity));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1402
1407
|
.border-blue-300 {
|
|
1403
1408
|
--tw-border-opacity: 1;
|
|
1404
1409
|
border-color: rgb(147 197 253 / var(--tw-border-opacity));
|
|
@@ -1433,6 +1438,11 @@ video {
|
|
|
1433
1438
|
border-color: rgb(63 83 255 / var(--tw-border-opacity));
|
|
1434
1439
|
}
|
|
1435
1440
|
|
|
1441
|
+
.bg-amber-100 {
|
|
1442
|
+
--tw-bg-opacity: 1;
|
|
1443
|
+
background-color: rgb(254 243 199 / var(--tw-bg-opacity));
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1436
1446
|
.bg-black {
|
|
1437
1447
|
--tw-bg-opacity: 1;
|
|
1438
1448
|
background-color: rgb(0 0 0 / var(--tw-bg-opacity));
|
|
@@ -2044,6 +2054,11 @@ video {
|
|
|
2044
2054
|
--tw-ring-inset: inset;
|
|
2045
2055
|
}
|
|
2046
2056
|
|
|
2057
|
+
.ring-amber-200 {
|
|
2058
|
+
--tw-ring-opacity: 1;
|
|
2059
|
+
--tw-ring-color: rgb(253 230 138 / var(--tw-ring-opacity));
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2047
2062
|
.ring-black {
|
|
2048
2063
|
--tw-ring-opacity: 1;
|
|
2049
2064
|
--tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity));
|
|
@@ -2074,6 +2089,10 @@ video {
|
|
|
2074
2089
|
--tw-ring-opacity: 0.05;
|
|
2075
2090
|
}
|
|
2076
2091
|
|
|
2092
|
+
.ring-offset-1 {
|
|
2093
|
+
--tw-ring-offset-width: 1px;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2077
2096
|
.filter {
|
|
2078
2097
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
|
2079
2098
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
<div>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</div>
|
|
2
|
+
<h2>
|
|
3
|
+
Are you sure you want to delete the following document?
|
|
4
|
+
</h2>
|
|
5
|
+
<pre class="max-h-[50vh] overflow-auto"><code ref="code" class="language-javascript" v-text="displayValue"></code></pre>
|
|
6
|
+
<div class="flex gap-2 mt-2">
|
|
7
|
+
<async-button
|
|
8
|
+
class="rounded-md bg-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"
|
|
9
|
+
@click="startDelete">
|
|
10
|
+
Confirm
|
|
11
|
+
</async-button>
|
|
12
|
+
<button
|
|
13
|
+
class="rounded-md bg-gray-200 px-2.5 py-1.5 text-sm font-semibold text-black shadow-sm hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300"
|
|
14
|
+
@click="closeDelete">
|
|
15
|
+
Cancel
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
|
|
23
23
|
<!-- Data Type Filter -->
|
|
24
|
-
<div class="relative">
|
|
24
|
+
<div class="relative hidden md:block">
|
|
25
25
|
<select
|
|
26
26
|
v-model="selectedType"
|
|
27
|
-
class="
|
|
27
|
+
class="px-4 py-2 pr-8 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white min-w-[140px] appearance-none"
|
|
28
28
|
>
|
|
29
29
|
<option value="">All Types</option>
|
|
30
30
|
<option v-for="type in availableTypes" :key="type" :value="type">
|
|
@@ -41,12 +41,11 @@
|
|
|
41
41
|
<!-- Add Field Button -->
|
|
42
42
|
<button
|
|
43
43
|
@click="openAddFieldModal"
|
|
44
|
-
class="hidden md:
|
|
44
|
+
class="hidden md:flex px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 items-center gap-2"
|
|
45
45
|
>
|
|
46
46
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
47
47
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
48
|
-
</svg>
|
|
49
|
-
Add Field
|
|
48
|
+
</svg> Add Field
|
|
50
49
|
</button>
|
|
51
50
|
</div>
|
|
52
51
|
</div>
|
|
@@ -54,18 +53,32 @@
|
|
|
54
53
|
<!-- Fields View -->
|
|
55
54
|
<div v-if="viewMode === 'fields'">
|
|
56
55
|
<!-- Schema Paths -->
|
|
57
|
-
<div
|
|
56
|
+
<div
|
|
57
|
+
v-for="path in filteredSchemaPaths"
|
|
58
|
+
:key="path.path || path"
|
|
59
|
+
class="value"
|
|
60
|
+
>
|
|
58
61
|
<document-property
|
|
59
62
|
:path="path"
|
|
60
63
|
:document="document"
|
|
61
64
|
:schemaPaths="schemaPaths"
|
|
62
65
|
:editting="editting"
|
|
63
66
|
:changes="changes"
|
|
67
|
+
:highlight="isSchemaPathMatched(path)"
|
|
64
68
|
:invalid="invalid"></document-property>
|
|
65
69
|
</div>
|
|
66
70
|
|
|
67
71
|
<!-- Virtual Fields -->
|
|
68
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
v-for="path in filteredVirtuals"
|
|
74
|
+
:key="path.name"
|
|
75
|
+
class="border rounded-lg mb-2 transition-all duration-200 ease-in-out"
|
|
76
|
+
:class="[
|
|
77
|
+
isVirtualMatched(path)
|
|
78
|
+
? 'border-amber-400 ring-2 ring-amber-200 ring-offset-1'
|
|
79
|
+
: 'border-gray-200'
|
|
80
|
+
]"
|
|
81
|
+
>
|
|
69
82
|
<!-- Virtual Field Header (Always Visible) -->
|
|
70
83
|
<div
|
|
71
84
|
@click="toggleVirtualField(path.name)"
|
|
@@ -99,7 +112,7 @@
|
|
|
99
112
|
</div>
|
|
100
113
|
|
|
101
114
|
<!-- No Results Message -->
|
|
102
|
-
<div v-if="searchQuery
|
|
115
|
+
<div v-if="searchQuery.trim() && !hasSearchMatches" class="text-center py-8 text-gray-500">
|
|
103
116
|
<svg class="w-12 h-12 mx-auto mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
104
117
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 6.291A7.962 7.962 0 0012 5c-2.34 0-4.29 1.009-5.824 2.709"></path>
|
|
105
118
|
</svg>
|
|
@@ -115,38 +115,39 @@ module.exports = app => app.component('document-details', {
|
|
|
115
115
|
shouldUseDatePicker() {
|
|
116
116
|
return this.fieldData.type === 'Date';
|
|
117
117
|
},
|
|
118
|
-
|
|
118
|
+
typeFilteredSchemaPaths() {
|
|
119
119
|
let paths = this.schemaPaths || [];
|
|
120
120
|
|
|
121
|
-
// Filter by search query
|
|
122
|
-
if (this.searchQuery.trim()) {
|
|
123
|
-
const query = this.searchQuery.toLowerCase();
|
|
124
|
-
paths = paths.filter(path =>
|
|
125
|
-
path.path.toLowerCase().includes(query)
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Filter by data type
|
|
130
121
|
if (this.selectedType) {
|
|
131
|
-
paths = paths.filter(path =>
|
|
132
|
-
path.instance === this.selectedType
|
|
133
|
-
);
|
|
122
|
+
paths = paths.filter(path => path.instance === this.selectedType);
|
|
134
123
|
}
|
|
135
124
|
|
|
136
125
|
return paths;
|
|
137
126
|
},
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
filteredSchemaPaths() {
|
|
128
|
+
const paths = this.typeFilteredSchemaPaths.slice();
|
|
140
129
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const query = this.searchQuery.toLowerCase();
|
|
144
|
-
virtuals = virtuals.filter(virtual =>
|
|
145
|
-
virtual.name.toLowerCase().includes(query)
|
|
146
|
-
);
|
|
130
|
+
if (!this.searchQuery.trim()) {
|
|
131
|
+
return paths;
|
|
147
132
|
}
|
|
148
133
|
|
|
149
|
-
|
|
134
|
+
const query = this.searchQuery.toLowerCase();
|
|
135
|
+
const matches = [];
|
|
136
|
+
const nonMatches = [];
|
|
137
|
+
|
|
138
|
+
paths.forEach(path => {
|
|
139
|
+
if (path.path.toLowerCase().includes(query)) {
|
|
140
|
+
matches.push(path);
|
|
141
|
+
} else {
|
|
142
|
+
nonMatches.push(path);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return matches.concat(nonMatches);
|
|
147
|
+
},
|
|
148
|
+
typeFilteredVirtuals() {
|
|
149
|
+
let virtuals = this.virtuals;
|
|
150
|
+
|
|
150
151
|
if (this.selectedType) {
|
|
151
152
|
virtuals = virtuals.filter(virtual => {
|
|
152
153
|
const virtualType = this.getVirtualFieldType(virtual);
|
|
@@ -156,6 +157,58 @@ module.exports = app => app.component('document-details', {
|
|
|
156
157
|
|
|
157
158
|
return virtuals;
|
|
158
159
|
},
|
|
160
|
+
filteredVirtuals() {
|
|
161
|
+
const virtuals = this.typeFilteredVirtuals.slice();
|
|
162
|
+
|
|
163
|
+
if (!this.searchQuery.trim()) {
|
|
164
|
+
return virtuals;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const query = this.searchQuery.toLowerCase();
|
|
168
|
+
const matches = [];
|
|
169
|
+
const nonMatches = [];
|
|
170
|
+
|
|
171
|
+
virtuals.forEach(virtual => {
|
|
172
|
+
if (virtual.name.toLowerCase().includes(query)) {
|
|
173
|
+
matches.push(virtual);
|
|
174
|
+
} else {
|
|
175
|
+
nonMatches.push(virtual);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return matches.concat(nonMatches);
|
|
180
|
+
},
|
|
181
|
+
schemaSearchMatchSet() {
|
|
182
|
+
if (!this.searchQuery.trim()) {
|
|
183
|
+
return new Set();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const query = this.searchQuery.toLowerCase();
|
|
187
|
+
return new Set(
|
|
188
|
+
this.typeFilteredSchemaPaths
|
|
189
|
+
.filter(path => path.path.toLowerCase().includes(query))
|
|
190
|
+
.map(path => path.path)
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
virtualSearchMatchSet() {
|
|
194
|
+
if (!this.searchQuery.trim()) {
|
|
195
|
+
return new Set();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const query = this.searchQuery.toLowerCase();
|
|
199
|
+
return new Set(
|
|
200
|
+
this.typeFilteredVirtuals
|
|
201
|
+
.filter(virtual => virtual.name.toLowerCase().includes(query))
|
|
202
|
+
.map(virtual => virtual.name)
|
|
203
|
+
);
|
|
204
|
+
},
|
|
205
|
+
hasSearchMatches() {
|
|
206
|
+
if (!this.searchQuery.trim()) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.schemaSearchMatchSet.size > 0 || this.virtualSearchMatchSet.size > 0;
|
|
211
|
+
},
|
|
159
212
|
formattedJson() {
|
|
160
213
|
if (!this.document) {
|
|
161
214
|
return '{}';
|
|
@@ -171,6 +224,20 @@ module.exports = app => app.component('document-details', {
|
|
|
171
224
|
this.collapsedVirtuals.add(fieldName);
|
|
172
225
|
}
|
|
173
226
|
},
|
|
227
|
+
isSchemaPathMatched(path) {
|
|
228
|
+
if (!path) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return this.schemaSearchMatchSet.has(path.path);
|
|
233
|
+
},
|
|
234
|
+
isVirtualMatched(virtual) {
|
|
235
|
+
if (!virtual) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return this.virtualSearchMatchSet.has(virtual.name);
|
|
240
|
+
},
|
|
174
241
|
isVirtualFieldCollapsed(fieldName) {
|
|
175
242
|
return this.collapsedVirtuals.has(fieldName);
|
|
176
243
|
},
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
<div class="border border-gray-200 rounded-lg mb-2">
|
|
2
2
|
<!-- Collapsible Header -->
|
|
3
|
-
<div
|
|
3
|
+
<div
|
|
4
4
|
@click="toggleCollapse"
|
|
5
|
-
class="p-3
|
|
5
|
+
class="p-3 hover:bg-gray-100 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out"
|
|
6
|
+
:class="{ 'bg-amber-100': highlight, 'bg-gray-50': !highlight }"
|
|
6
7
|
>
|
|
7
|
-
<div class="flex items-center">
|
|
8
|
-
<svg
|
|
8
|
+
<div class="flex items-center" >
|
|
9
|
+
<svg
|
|
9
10
|
:class="isCollapsed ? 'rotate-0' : 'rotate-90'"
|
|
10
11
|
class="w-4 h-4 text-gray-500 mr-2 transition-transform duration-200"
|
|
11
|
-
fill="none"
|
|
12
|
-
stroke="currentColor"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
13
14
|
viewBox="0 0 24 24"
|
|
14
15
|
>
|
|
15
16
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
@@ -27,7 +28,7 @@
|
|
|
27
28
|
</router-link>
|
|
28
29
|
</div>
|
|
29
30
|
</div>
|
|
30
|
-
|
|
31
|
+
|
|
31
32
|
<!-- Collapsible Content -->
|
|
32
33
|
<div v-if="!isCollapsed" class="p-3">
|
|
33
34
|
<!-- Date Type Selector (when editing dates) -->
|
|
@@ -53,7 +54,7 @@
|
|
|
53
54
|
</div>
|
|
54
55
|
</div>
|
|
55
56
|
</div>
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
<!-- Field Content -->
|
|
58
59
|
<div v-if="editting && path.path !== '_id'">
|
|
59
60
|
<component
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
<!-- Show truncated or full value based on needsTruncation and isValueExpanded -->
|
|
70
71
|
<div v-if="needsTruncation && !isValueExpanded" class="truncated-value-container">
|
|
71
72
|
<div class="text-gray-700 whitespace-pre-wrap break-words font-mono text-sm">{{truncatedString}}</div>
|
|
72
|
-
<button
|
|
73
|
+
<button
|
|
73
74
|
@click="toggleValueExpansion"
|
|
74
75
|
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
|
|
75
76
|
>
|
|
@@ -81,7 +82,7 @@
|
|
|
81
82
|
</div>
|
|
82
83
|
<div v-else-if="needsTruncation && isValueExpanded" class="expanded-value-container">
|
|
83
84
|
<component :is="getComponentForPath(path)" :value="getValueForPath(path.path)"></component>
|
|
84
|
-
<button
|
|
85
|
+
<button
|
|
85
86
|
@click="toggleValueExpansion"
|
|
86
87
|
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1"
|
|
87
88
|
>
|
|
@@ -16,7 +16,7 @@ module.exports = app => app.component('document-property', {
|
|
|
16
16
|
isValueExpanded: false // Track if the value is expanded
|
|
17
17
|
};
|
|
18
18
|
},
|
|
19
|
-
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid'],
|
|
19
|
+
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight'],
|
|
20
20
|
computed: {
|
|
21
21
|
valueAsString() {
|
|
22
22
|
const value = this.getValueForPath(this.path.path);
|
|
@@ -94,4 +94,4 @@ module.exports = app => app.component('document-property', {
|
|
|
94
94
|
this.isValueExpanded = !this.isValueExpanded;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
});
|
|
97
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.131",
|
|
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": {
|