@mongoosejs/studio 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/actions/Model/updateDocument.js +7 -9
- package/eslint.config.js +1 -0
- package/frontend/public/app.js +292 -76
- package/frontend/public/tw.css +4 -0
- package/frontend/src/_util/baseComponent.js +19 -0
- package/frontend/src/_util/deepEqual.js +53 -0
- package/frontend/src/_util/document-search-autocomplete.js +4 -4
- package/frontend/src/array-utils.js +1 -0
- package/frontend/src/dashboard/dashboard.js +22 -17
- package/frontend/src/document/confirm-changes/confirm-changes.js +11 -1
- package/frontend/src/document/document.html +1 -1
- package/frontend/src/document/document.js +16 -1
- package/frontend/src/document-details/document-property/document-property.html +1 -1
- package/frontend/src/document-details/document-property/document-property.js +15 -1
- package/frontend/src/edit-boolean/edit-boolean.js +1 -9
- package/frontend/src/index.js +9 -2
- package/frontend/src/mothership.js +11 -2
- package/package.json +5 -3
package/frontend/public/tw.css
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
destroyed() {
|
|
5
|
+
this.$parent.$options.$children = this.$parent.$options.$children.filter(
|
|
6
|
+
(el) => el !== this
|
|
7
|
+
);
|
|
8
|
+
},
|
|
9
|
+
created() {
|
|
10
|
+
this.$parent.$options.$children = this.$parent.$options.$children || [];
|
|
11
|
+
this.$parent.$options.$children.push(this);
|
|
12
|
+
},
|
|
13
|
+
mounted() {
|
|
14
|
+
this.isMounted = true;
|
|
15
|
+
},
|
|
16
|
+
unmounted() {
|
|
17
|
+
this.isMounted = false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deep equality check for values (handles primitives, arrays, objects, dates, etc.)
|
|
5
|
+
* @param {*} a - First value to compare
|
|
6
|
+
* @param {*} b - Second value to compare
|
|
7
|
+
* @returns {boolean} - True if values are deeply equal
|
|
8
|
+
*/
|
|
9
|
+
function deepEqual(a, b) {
|
|
10
|
+
// Handle primitives and same reference
|
|
11
|
+
if (a === b) return true;
|
|
12
|
+
|
|
13
|
+
// Handle null and undefined
|
|
14
|
+
if (a == null || b == null) return a === b;
|
|
15
|
+
|
|
16
|
+
// Handle different types
|
|
17
|
+
if (typeof a !== typeof b) return false;
|
|
18
|
+
|
|
19
|
+
// Handle dates
|
|
20
|
+
if (a instanceof Date && b instanceof Date) {
|
|
21
|
+
return a.getTime() === b.getTime();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle arrays - must both be arrays
|
|
25
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
26
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle objects (non-arrays)
|
|
35
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
36
|
+
const keysA = Object.keys(a);
|
|
37
|
+
const keysB = Object.keys(b);
|
|
38
|
+
|
|
39
|
+
if (keysA.length !== keysB.length) return false;
|
|
40
|
+
|
|
41
|
+
for (const key of keysA) {
|
|
42
|
+
if (!keysB.includes(key)) return false;
|
|
43
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback for primitives (strings, numbers, booleans)
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = deepEqual;
|
|
@@ -82,9 +82,9 @@ function getAutocompleteContext(searchText, cursorPos) {
|
|
|
82
82
|
|
|
83
83
|
// Check if we're in a value context (after a colon)
|
|
84
84
|
// Match the last colon followed by optional whitespace and capture everything after
|
|
85
|
-
const valueMatch = before.match(/:\s*([^\s,\}\]:]*)$/);
|
|
85
|
+
const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
|
|
86
86
|
if (valueMatch) {
|
|
87
|
-
const token = valueMatch[
|
|
87
|
+
const token = valueMatch[2];
|
|
88
88
|
return {
|
|
89
89
|
token,
|
|
90
90
|
role: 'value',
|
|
@@ -159,9 +159,9 @@ function applySuggestion(searchText, cursorPos, suggestion) {
|
|
|
159
159
|
const after = searchText.slice(cursorPos);
|
|
160
160
|
|
|
161
161
|
// Check if we're in a value context
|
|
162
|
-
const valueMatch = before.match(/:\s*([^\s,\}\]:]*)$/);
|
|
162
|
+
const valueMatch = before.match(/:\s*(\{?\s*)([^\s,\}\]:]*)$/);
|
|
163
163
|
if (valueMatch) {
|
|
164
|
-
const token = valueMatch[
|
|
164
|
+
const token = valueMatch[2];
|
|
165
165
|
const start = cursorPos - token.length;
|
|
166
166
|
let replacement = suggestion;
|
|
167
167
|
let cursorOffset = replacement.length;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const api = require('../api');
|
|
4
|
+
const baseComponent = require('../_util/baseComponent');
|
|
4
5
|
const template = require('./dashboard.html');
|
|
5
6
|
|
|
6
|
-
module.exports =
|
|
7
|
+
module.exports = {
|
|
7
8
|
template: template,
|
|
9
|
+
extends: baseComponent,
|
|
8
10
|
props: ['dashboardId'],
|
|
9
11
|
data: function() {
|
|
10
12
|
return {
|
|
@@ -48,7 +50,7 @@ module.exports = app => app.component('dashboard', {
|
|
|
48
50
|
}
|
|
49
51
|
},
|
|
50
52
|
shouldEvaluateDashboard() {
|
|
51
|
-
if (this.dashboardResults.length === 0) {
|
|
53
|
+
if (!this.dashboardResults || this.dashboardResults.length === 0) {
|
|
52
54
|
return true;
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -115,6 +117,22 @@ module.exports = app => app.component('dashboard', {
|
|
|
115
117
|
this.startingChat = false;
|
|
116
118
|
this.showActionsMenu = false;
|
|
117
119
|
}
|
|
120
|
+
},
|
|
121
|
+
async loadInitial() {
|
|
122
|
+
const { dashboard, dashboardResults, error } = await api.Dashboard.getDashboard({ dashboardId: this.dashboardId, evaluate: false });
|
|
123
|
+
if (!dashboard) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.dashboard = dashboard;
|
|
127
|
+
this.code = this.dashboard.code;
|
|
128
|
+
this.title = this.dashboard.title;
|
|
129
|
+
this.description = this.dashboard.description ?? '';
|
|
130
|
+
this.dashboardResults = dashboardResults;
|
|
131
|
+
if (this.shouldEvaluateDashboard()) {
|
|
132
|
+
await this.evaluateDashboard();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.status = 'loaded';
|
|
118
136
|
}
|
|
119
137
|
},
|
|
120
138
|
computed: {
|
|
@@ -125,22 +143,9 @@ module.exports = app => app.component('dashboard', {
|
|
|
125
143
|
mounted: async function() {
|
|
126
144
|
document.addEventListener('click', this.handleDocumentClick);
|
|
127
145
|
this.showEditor = this.$route.query.edit;
|
|
128
|
-
|
|
129
|
-
if (!dashboard) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
this.dashboard = dashboard;
|
|
133
|
-
this.code = this.dashboard.code;
|
|
134
|
-
this.title = this.dashboard.title;
|
|
135
|
-
this.description = this.dashboard.description ?? '';
|
|
136
|
-
this.dashboardResults = dashboardResults;
|
|
137
|
-
if (this.shouldEvaluateDashboard()) {
|
|
138
|
-
await this.evaluateDashboard();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
this.status = 'loaded';
|
|
146
|
+
await this.loadInitial();
|
|
142
147
|
},
|
|
143
148
|
beforeDestroy() {
|
|
144
149
|
document.removeEventListener('click', this.handleDocumentClick);
|
|
145
150
|
}
|
|
146
|
-
}
|
|
151
|
+
};
|
|
@@ -7,6 +7,16 @@ module.exports = app => app.component('confirm-changes', {
|
|
|
7
7
|
props: ['value'],
|
|
8
8
|
computed: {
|
|
9
9
|
displayValue() {
|
|
10
|
+
const hasUnsetFields = Object.keys(this.value).some(key => this.value[key] === undefined);
|
|
11
|
+
if (hasUnsetFields) {
|
|
12
|
+
const unsetFields = Object.keys(this.value)
|
|
13
|
+
.filter(key => this.value[key] === undefined)
|
|
14
|
+
.reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
|
|
15
|
+
const setFields = Object.keys(this.value)
|
|
16
|
+
.filter(key => this.value[key] !== undefined)
|
|
17
|
+
.reduce((obj, key) => Object.assign(obj, { [key]: this.value[key] }), {});
|
|
18
|
+
return JSON.stringify({ $set: setFields, $unset: unsetFields }, null, ' ').trim();
|
|
19
|
+
}
|
|
10
20
|
return JSON.stringify(this.value, null, ' ').trim();
|
|
11
21
|
}
|
|
12
22
|
},
|
|
@@ -21,4 +31,4 @@ module.exports = app => app.component('confirm-changes', {
|
|
|
21
31
|
mounted() {
|
|
22
32
|
Prism.highlightElement(this.$refs.code);
|
|
23
33
|
}
|
|
24
|
-
});
|
|
34
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<div class="document px-1 pt-4 md:px-0 bg-slate-50 w-full">
|
|
1
|
+
<div class="document px-1 pt-4 pb-16 md:px-0 bg-slate-50 w-full">
|
|
2
2
|
<div class="max-w-7xl mx-auto">
|
|
3
3
|
<div class="flex gap-4 items-center sticky top-0 z-50 bg-white p-4 border-b border-gray-200 shadow-sm">
|
|
4
4
|
<div class="font-bold overflow-hidden text-ellipsis">{{model}}: {{documentId}}</div>
|
|
@@ -74,10 +74,25 @@ module.exports = app => app.component('document', {
|
|
|
74
74
|
if (Object.keys(this.invalid).length > 0) {
|
|
75
75
|
throw new Error('Invalid paths: ' + Object.keys(this.invalid).join(', '));
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
let update = this.changes;
|
|
79
|
+
let unset = {};
|
|
80
|
+
const hasUnsetFields = Object.keys(this.changes)
|
|
81
|
+
.some(key => this.changes[key] === undefined);
|
|
82
|
+
if (hasUnsetFields) {
|
|
83
|
+
unset = Object.keys(this.changes)
|
|
84
|
+
.filter(key => this.changes[key] === undefined)
|
|
85
|
+
.reduce((obj, key) => Object.assign(obj, { [key]: 1 }), {});
|
|
86
|
+
update = Object.keys(this.changes)
|
|
87
|
+
.filter(key => this.changes[key] !== undefined)
|
|
88
|
+
.reduce((obj, key) => Object.assign(obj, { [key]: this.changes[key] }), {});
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
const { doc } = await api.Model.updateDocument({
|
|
78
92
|
model: this.model,
|
|
79
93
|
_id: this.document._id,
|
|
80
|
-
update
|
|
94
|
+
update,
|
|
95
|
+
unset
|
|
81
96
|
});
|
|
82
97
|
this.document = doc;
|
|
83
98
|
this.changes = {};
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
:value="getEditValueForPath(path)"
|
|
75
75
|
:format="dateType"
|
|
76
76
|
v-bind="getEditComponentProps(path)"
|
|
77
|
-
@input="
|
|
77
|
+
@input="handleInputChange($event)"
|
|
78
78
|
@error="invalid[path.path] = $event;"
|
|
79
79
|
>
|
|
80
80
|
</component>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
'use strict';
|
|
4
4
|
|
|
5
5
|
const mpath = require('mpath');
|
|
6
|
-
const
|
|
6
|
+
const deepEqual = require('../../_util/deepEqual');
|
|
7
7
|
const template = require('./document-property.html');
|
|
8
8
|
|
|
9
9
|
const appendCSS = require('../../appendCSS');
|
|
@@ -95,6 +95,20 @@ module.exports = app => app.component('document-property', {
|
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
97
|
methods: {
|
|
98
|
+
handleInputChange(newValue) {
|
|
99
|
+
const currentValue = this.getValueForPath(this.path.path);
|
|
100
|
+
|
|
101
|
+
// Only record as a change if the value is actually different
|
|
102
|
+
if (!deepEqual(currentValue, newValue)) {
|
|
103
|
+
this.changes[this.path.path] = newValue;
|
|
104
|
+
} else {
|
|
105
|
+
// If the value is the same as the original, remove it from changes
|
|
106
|
+
delete this.changes[this.path.path];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Always clear invalid state on input
|
|
110
|
+
delete this.invalid[this.path.path];
|
|
111
|
+
},
|
|
98
112
|
getComponentForPath(schemaPath) {
|
|
99
113
|
if (schemaPath.instance === 'Array') {
|
|
100
114
|
return 'detail-array';
|
|
@@ -19,20 +19,12 @@ module.exports = app => app.component('edit-boolean', {
|
|
|
19
19
|
this.selectedValue = newValue;
|
|
20
20
|
},
|
|
21
21
|
selectedValue(newValue) {
|
|
22
|
-
|
|
23
|
-
const emitValue = this.convertValueToString(newValue);
|
|
24
|
-
this.$emit('input', emitValue);
|
|
22
|
+
this.$emit('input', newValue);
|
|
25
23
|
}
|
|
26
24
|
},
|
|
27
25
|
methods: {
|
|
28
26
|
selectValue(value) {
|
|
29
27
|
this.selectedValue = value;
|
|
30
|
-
},
|
|
31
|
-
convertValueToString(value) {
|
|
32
|
-
// Convert null/undefined to strings for proper backend serialization
|
|
33
|
-
if (value === null) return 'null';
|
|
34
|
-
if (typeof value === 'undefined') return 'undefined';
|
|
35
|
-
return value;
|
|
36
28
|
}
|
|
37
29
|
}
|
|
38
30
|
});
|
package/frontend/src/index.js
CHANGED
|
@@ -44,7 +44,11 @@ requireComponents.keys().forEach((filePath) => {
|
|
|
44
44
|
// Check if the file name matches the directory name
|
|
45
45
|
if (directoryName === fileName) {
|
|
46
46
|
components[directoryName] = requireComponents(filePath);
|
|
47
|
-
components[directoryName]
|
|
47
|
+
if (typeof components[directoryName] === 'function') {
|
|
48
|
+
components[directoryName](app);
|
|
49
|
+
} else {
|
|
50
|
+
app.component(directoryName, components[directoryName]);
|
|
51
|
+
}
|
|
48
52
|
}
|
|
49
53
|
});
|
|
50
54
|
|
|
@@ -102,18 +106,21 @@ app.component('app-component', {
|
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
window.localStorage.setItem('_mongooseStudioAccessToken', accessToken._id);
|
|
110
|
+
|
|
105
111
|
try {
|
|
106
112
|
const { nodeEnv } = await api.status();
|
|
107
113
|
this.nodeEnv = nodeEnv;
|
|
108
114
|
} catch (err) {
|
|
109
115
|
this.authError = 'Error connecting to Mongoose Studio API: ' + err.response?.data?.message ?? err.message;
|
|
110
116
|
this.status = 'loaded';
|
|
117
|
+
window.localStorage.setItem('_mongooseStudioAccessToken', '');
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
this.user = user;
|
|
115
122
|
this.roles = roles;
|
|
116
|
-
|
|
123
|
+
|
|
117
124
|
setTimeout(() => {
|
|
118
125
|
this.$router.replace(this.$router.currentRoute.value.path);
|
|
119
126
|
}, 0);
|
|
@@ -18,12 +18,21 @@ client.interceptors.request.use(req => {
|
|
|
18
18
|
return req;
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
function sanitizedReturnUrl() {
|
|
22
|
+
const url = new URL(window.location.href);
|
|
23
|
+
if (url.hash && url.hash.includes('?')) {
|
|
24
|
+
const [hashPath, hashSearch] = url.hash.split('?', 2);
|
|
25
|
+
url.hash = hashPath;
|
|
26
|
+
}
|
|
27
|
+
return url.toString();
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
exports.githubLogin = function githubLogin() {
|
|
22
|
-
return client.post('/githubLogin', { state:
|
|
31
|
+
return client.post('/githubLogin', { state: sanitizedReturnUrl() }).then(res => res.data);
|
|
23
32
|
};
|
|
24
33
|
|
|
25
34
|
exports.googleLogin = function googleLogin() {
|
|
26
|
-
return client.post('/googleLogin', { state:
|
|
35
|
+
return client.post('/googleLogin', { state: sanitizedReturnUrl() }).then(res => res.data);
|
|
27
36
|
};
|
|
28
37
|
|
|
29
38
|
exports.getWorkspaceTeam = function getWorkspaceTeam() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mongoosejs/studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.",
|
|
5
5
|
"homepage": "https://mongoosestudio.app/",
|
|
6
6
|
"repository": {
|
|
@@ -34,12 +34,14 @@
|
|
|
34
34
|
"eslint": "9.30.0",
|
|
35
35
|
"express": "4.x",
|
|
36
36
|
"mocha": "10.2.0",
|
|
37
|
-
"mongoose": "9.x"
|
|
37
|
+
"mongoose": "9.x",
|
|
38
|
+
"sinon": "^21.0.1"
|
|
38
39
|
},
|
|
39
40
|
"scripts": {
|
|
40
41
|
"lint": "eslint .",
|
|
41
42
|
"tailwind": "tailwindcss -o ./frontend/public/tw.css",
|
|
42
43
|
"tailwind:watch": "tailwindcss -o ./frontend/public/tw.css --watch",
|
|
43
|
-
"test": "mocha test/*.test.js"
|
|
44
|
+
"test": "mocha test/*.test.js",
|
|
45
|
+
"test:frontend": "mocha test/frontend/*.test.js"
|
|
44
46
|
}
|
|
45
47
|
}
|