@mongoosejs/studio 0.2.13 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/actions/ChatMessage/executeScript.js +5 -1
- package/backend/actions/ChatThread/createChatMessage.js +2 -1
- package/backend/actions/ChatThread/streamChatMessage.js +2 -2
- package/eslint.config.js +4 -1
- package/frontend/public/app.js +24642 -543
- package/frontend/public/dark-theme.css +365 -0
- package/frontend/public/images/mongoose-studio.svg +4 -0
- package/frontend/public/index.html +21 -1
- package/frontend/public/style.css +5 -7
- package/frontend/public/theme-variables.css +294 -0
- package/frontend/public/tw.css +305 -252
- package/frontend/src/ace-editor/ace-editor.html +4 -0
- package/frontend/src/ace-editor/ace-editor.js +89 -0
- package/frontend/src/aceEditor.js +69 -0
- package/frontend/src/chat/chat-message/chat-message.html +1 -1
- package/frontend/src/chat/chat-message/chat-message.js +1 -1
- package/frontend/src/chat/chat-message-script/chat-message-script.html +51 -34
- package/frontend/src/chat/chat-message-script/chat-message-script.js +12 -55
- package/frontend/src/chat/chat.html +68 -39
- package/frontend/src/chat/chat.js +26 -2
- package/frontend/src/clone-document/clone-document.html +7 -2
- package/frontend/src/clone-document/clone-document.js +1 -8
- package/frontend/src/create-dashboard/create-dashboard.html +11 -6
- package/frontend/src/create-dashboard/create-dashboard.js +0 -7
- package/frontend/src/create-document/create-document.html +15 -9
- package/frontend/src/create-document/create-document.js +5 -12
- package/frontend/src/dashboard/dashboard.html +14 -12
- package/frontend/src/dashboard/dashboard.js +12 -4
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.html +13 -7
- package/frontend/src/dashboard/edit-dashboard/edit-dashboard.js +13 -21
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.html +19 -17
- package/frontend/src/dashboard-result/dashboard-chart/dashboard-chart.js +97 -2
- package/frontend/src/dashboard-result/dashboard-map/dashboard-map.js +27 -3
- package/frontend/src/dashboard-result/dashboard-result.html +3 -3
- package/frontend/src/dashboards/dashboards.html +101 -109
- package/frontend/src/dashboards/dashboards.js +25 -1
- package/frontend/src/detail-default/detail-default.html +2 -2
- package/frontend/src/detail-default/detail-default.js +24 -3
- package/frontend/src/document/confirm-changes/confirm-changes.html +1 -1
- package/frontend/src/document/confirm-delete/confirm-delete.html +1 -1
- package/frontend/src/document/document.css +1 -1
- package/frontend/src/document/document.html +28 -28
- package/frontend/src/document/execute-script/execute-script.html +20 -21
- package/frontend/src/document/execute-script/execute-script.js +1 -43
- package/frontend/src/document-details/document-details.css +4 -9
- package/frontend/src/document-details/document-details.html +34 -33
- package/frontend/src/document-details/document-details.js +2 -53
- package/frontend/src/document-details/document-property/document-property.html +12 -12
- package/frontend/src/edit-array/edit-array.html +7 -6
- package/frontend/src/edit-array/edit-array.js +10 -50
- package/frontend/src/edit-boolean/edit-boolean.html +12 -12
- package/frontend/src/edit-date/edit-date.html +2 -2
- package/frontend/src/edit-default/edit-default.html +1 -1
- package/frontend/src/edit-string/edit-string.html +3 -3
- package/frontend/src/edit-subdocument/edit-subdocument.html +5 -3
- package/frontend/src/edit-subdocument/edit-subdocument.js +1 -15
- package/frontend/src/export-query-results/export-query-results.html +3 -3
- package/frontend/src/json-node/json-node.html +3 -3
- package/frontend/src/list-json/json-node.html +1 -1
- package/frontend/src/models/document-search/document-search.html +3 -3
- package/frontend/src/models/model-switcher/model-switcher.html +53 -0
- package/frontend/src/models/model-switcher/model-switcher.js +123 -0
- package/frontend/src/models/models.css +3 -10
- package/frontend/src/models/models.html +146 -80
- package/frontend/src/models/models.js +108 -4
- package/frontend/src/navbar/navbar.html +157 -97
- package/frontend/src/navbar/navbar.js +31 -12
- package/frontend/src/routes.js +1 -1
- package/frontend/src/splash/splash.html +5 -5
- package/frontend/src/task-single/task-single.html +29 -29
- package/frontend/src/task-single/task-single.js +10 -10
- package/frontend/src/tasks/task-details/task-details.html +38 -38
- package/frontend/src/tasks/task-details/task-details.js +7 -2
- package/frontend/src/tasks/tasks.html +36 -35
- package/frontend/src/tasks/tasks.js +2 -25
- package/frontend/src/team/new-invitation/new-invitation.html +8 -8
- package/frontend/src/team/team.html +27 -27
- package/frontend/src/update-document/update-document.html +7 -2
- package/frontend/src/update-document/update-document.js +2 -11
- package/package.json +2 -1
- package/tailwind.config.js +75 -11
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
<div :class="responsive ? 'h-full' : ''">
|
|
2
2
|
<div v-if="header && !fullscreen" class="border-b border-gray-100 px-2 pb-2 flex items-center gap-1">
|
|
3
3
|
<div class="text-xl font-bold">{{header}}</div>
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
<template v-if="showControls">
|
|
5
|
+
<button
|
|
6
|
+
class="ml-auto px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors"
|
|
7
|
+
@click="exportPNG"
|
|
8
|
+
title="Export PNG">
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em;" viewBox="0 -960 960 960" fill="currentColor"><path d="M280-280h400v-80H280v80Zm200-120 160-160-56-56-64 62v-166h-80v166l-64-62-56 56 160 160Zm0 320q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
|
10
|
+
</button>
|
|
11
|
+
<button
|
|
12
|
+
class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
|
|
13
|
+
@click="$emit('fullscreen')"
|
|
14
|
+
aria-label="Expand dashboard result">
|
|
15
|
+
<svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 1v4m0 0h-4m4 0l-5-5" />
|
|
17
|
+
</svg>
|
|
18
|
+
</button>
|
|
19
|
+
</template>
|
|
18
20
|
</div>
|
|
19
|
-
<div v-else-if="!fullscreen" class="pt-1 border-b border-gray-100 px-2 pb-2 text-right flex items-center justify-end gap-1 w-full">
|
|
21
|
+
<div v-else-if="!fullscreen && showControls" class="pt-1 border-b border-gray-100 px-2 pb-2 text-right flex items-center justify-end gap-1 w-full">
|
|
20
22
|
<button
|
|
21
|
-
class="px-2 py-1 text-xs bg-
|
|
23
|
+
class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors"
|
|
22
24
|
@click="exportPNG"
|
|
23
25
|
title="Export PNG">
|
|
24
26
|
<svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em;" viewBox="0 -960 960 960" fill="currentColor"><path d="M280-280h400v-80H280v80Zm200-120 160-160-56-56-64 62v-166h-80v166l-64-62-56 56 160 160Zm0 320q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
|
25
27
|
</button>
|
|
26
28
|
<button
|
|
27
|
-
class="px-2 py-1 text-xs bg-
|
|
29
|
+
class="px-2 py-1 text-xs bg-primary text-primary-text border-none rounded cursor-pointer hover:bg-primary-hover transition-colors flex items-center"
|
|
28
30
|
@click="$emit('fullscreen')"
|
|
29
31
|
aria-label="Expand dashboard result">
|
|
30
32
|
<svg xmlns="http://www.w3.org/2000/svg" style="height: 1.5em" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
@@ -2,6 +2,86 @@
|
|
|
2
2
|
|
|
3
3
|
const template = require('./dashboard-chart.html');
|
|
4
4
|
|
|
5
|
+
function getChartThemeColor(cssVar, fallback) {
|
|
6
|
+
if (typeof document === 'undefined') return fallback;
|
|
7
|
+
const val = getComputedStyle(document.documentElement).getPropertyValue(cssVar)?.trim();
|
|
8
|
+
return val || fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getThemeColors() {
|
|
12
|
+
return {
|
|
13
|
+
tickColor: getChartThemeColor('--studio-text-primary', '#111827'),
|
|
14
|
+
gridColor: getChartThemeColor('--studio-border', '#e5e7eb')
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SCALELESS_TYPES = ['pie', 'doughnut', 'polarArea', 'radar'];
|
|
19
|
+
|
|
20
|
+
function applyThemeChartOptions(config) {
|
|
21
|
+
const { tickColor, gridColor } = getThemeColors();
|
|
22
|
+
const type = config.type || '';
|
|
23
|
+
const options = config.options || {};
|
|
24
|
+
const plugins = { ...options.plugins };
|
|
25
|
+
const legend = plugins.legend || {};
|
|
26
|
+
plugins.legend = {
|
|
27
|
+
...legend,
|
|
28
|
+
labels: { ...legend.labels, color: tickColor }
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Pie/doughnut/polarArea/radar don't use cartesian scales
|
|
32
|
+
if (SCALELESS_TYPES.includes(type)) {
|
|
33
|
+
return {
|
|
34
|
+
...config,
|
|
35
|
+
options: { ...options, plugins }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const scales = { ...options.scales };
|
|
40
|
+
const applyScaleTheme = (id) => {
|
|
41
|
+
const scale = scales[id] || {};
|
|
42
|
+
scales[id] = {
|
|
43
|
+
...scale,
|
|
44
|
+
ticks: { ...scale.ticks, color: tickColor },
|
|
45
|
+
grid: { ...scale.grid, color: gridColor }
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
['x', 'y'].forEach(id => {
|
|
49
|
+
if (!scales[id]) scales[id] = {};
|
|
50
|
+
applyScaleTheme(id);
|
|
51
|
+
});
|
|
52
|
+
Object.keys(scales).forEach(id => {
|
|
53
|
+
if (id !== 'x' && id !== 'y') applyScaleTheme(id);
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
...config,
|
|
57
|
+
options: {
|
|
58
|
+
...options,
|
|
59
|
+
scales,
|
|
60
|
+
plugins
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function recreateChartWithTheme(component) {
|
|
66
|
+
if (!component.$refs.chart || !component.value?.$chart) return;
|
|
67
|
+
const canvas = component.$refs.chart;
|
|
68
|
+
const Chart = typeof window !== 'undefined' && window.Chart;
|
|
69
|
+
if (!Chart) return;
|
|
70
|
+
const existing = Chart.getChart(canvas);
|
|
71
|
+
if (existing) {
|
|
72
|
+
try {
|
|
73
|
+
existing.destroy();
|
|
74
|
+
} catch (_) { /* ignore teardown errors */ }
|
|
75
|
+
}
|
|
76
|
+
component.chart = null;
|
|
77
|
+
try {
|
|
78
|
+
const config = applyThemeChartOptions(component.value.$chart);
|
|
79
|
+
component.chart = new Chart(canvas, config);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn('Dashboard chart recreate failed:', err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
5
85
|
module.exports = app => app.component('dashboard-chart', {
|
|
6
86
|
template: template,
|
|
7
87
|
props: ['value', 'fullscreen'],
|
|
@@ -11,8 +91,20 @@ module.exports = app => app.component('dashboard-chart', {
|
|
|
11
91
|
showDetailModal: false
|
|
12
92
|
}),
|
|
13
93
|
mounted() {
|
|
14
|
-
const
|
|
15
|
-
this.chart = new Chart(
|
|
94
|
+
const config = applyThemeChartOptions(this.value.$chart);
|
|
95
|
+
this.chart = new Chart(this.$refs.chart, config);
|
|
96
|
+
this._onStudioThemeChanged = () => recreateChartWithTheme(this);
|
|
97
|
+
document.documentElement.addEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
98
|
+
},
|
|
99
|
+
beforeUnmount() {
|
|
100
|
+
document.documentElement.removeEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
101
|
+
const existing = typeof window !== 'undefined' && window.Chart && window.Chart.getChart(this.$refs.chart);
|
|
102
|
+
if (existing) {
|
|
103
|
+
try {
|
|
104
|
+
existing.destroy();
|
|
105
|
+
} catch (_) { /* ignore */ }
|
|
106
|
+
}
|
|
107
|
+
this.chart = null;
|
|
16
108
|
},
|
|
17
109
|
methods: {
|
|
18
110
|
exportPNG() {
|
|
@@ -34,6 +126,9 @@ module.exports = app => app.component('dashboard-chart', {
|
|
|
34
126
|
return this.value.$chart.header;
|
|
35
127
|
}
|
|
36
128
|
return null;
|
|
129
|
+
},
|
|
130
|
+
showControls() {
|
|
131
|
+
return this.fullscreen !== undefined;
|
|
37
132
|
}
|
|
38
133
|
}
|
|
39
134
|
});
|
|
@@ -3,15 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
const template = require('./dashboard-map.html');
|
|
5
5
|
|
|
6
|
+
function getMapTileLayerOptions() {
|
|
7
|
+
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
8
|
+
return isDark
|
|
9
|
+
? { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20 }
|
|
10
|
+
: { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors' };
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
module.exports = app => app.component('dashboard-map', {
|
|
7
14
|
template: template,
|
|
8
15
|
props: ['value', 'height'],
|
|
16
|
+
data() {
|
|
17
|
+
return { _map: null, _tileLayer: null };
|
|
18
|
+
},
|
|
9
19
|
mounted() {
|
|
10
20
|
const fc = this.value.$featureCollection.featureCollection || this.value.$featureCollection;
|
|
11
21
|
const map = L.map(this.$refs.map).setView([0, 0], 1);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
this._map = map;
|
|
23
|
+
const opts = getMapTileLayerOptions();
|
|
24
|
+
this._tileLayer = L.tileLayer(opts.url, opts).addTo(map);
|
|
15
25
|
const layer = L.geoJSON(fc).addTo(map);
|
|
16
26
|
|
|
17
27
|
this.$nextTick(() => {
|
|
@@ -21,6 +31,20 @@ module.exports = app => app.component('dashboard-map', {
|
|
|
21
31
|
map.fitBounds(bounds);
|
|
22
32
|
}
|
|
23
33
|
});
|
|
34
|
+
this._onStudioThemeChanged = () => this._updateMapTileLayer();
|
|
35
|
+
document.documentElement.addEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
36
|
+
},
|
|
37
|
+
beforeDestroy() {
|
|
38
|
+
document.documentElement.removeEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
39
|
+
},
|
|
40
|
+
methods: {
|
|
41
|
+
_updateMapTileLayer() {
|
|
42
|
+
if (!this._map || !this._tileLayer || typeof L === 'undefined') return;
|
|
43
|
+
this._tileLayer.remove();
|
|
44
|
+
this._tileLayer = null;
|
|
45
|
+
const opts = getMapTileLayerOptions();
|
|
46
|
+
this._tileLayer = L.tileLayer(opts.url, opts).addTo(this._map);
|
|
47
|
+
}
|
|
24
48
|
},
|
|
25
49
|
computed: {
|
|
26
50
|
mapStyle() {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div v-if="Array.isArray(result)">
|
|
3
3
|
<div v-for="el in result" :key="el._id || el.finishedEvaluatingAt">
|
|
4
4
|
<component
|
|
5
|
-
class="bg-
|
|
5
|
+
class="bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
|
|
6
6
|
:is="getComponentForValue(el)"
|
|
7
7
|
:value="el">
|
|
8
8
|
</component>
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
</div>
|
|
11
11
|
<div v-else>
|
|
12
12
|
<component
|
|
13
|
-
class="bg-
|
|
13
|
+
class="bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
|
|
14
14
|
:is="getComponentForValue(result)"
|
|
15
15
|
:value="result"
|
|
16
16
|
:fullscreen="fullscreen"
|
|
17
17
|
@fullscreen="$emit('fullscreen')">
|
|
18
18
|
</component>
|
|
19
19
|
</div>
|
|
20
|
-
<div class="text-right text-sm text-
|
|
20
|
+
<div class="text-right text-sm text-content-secondary mt-1" v-if="finishedEvaluatingAt && !fullscreen">
|
|
21
21
|
Last Evaluated: {{ format.isoToLongDateTime(finishedEvaluatingAt) }}
|
|
22
22
|
</div>
|
|
23
23
|
</div>
|
|
@@ -1,119 +1,111 @@
|
|
|
1
|
-
<div class="dashboards
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
class="
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
cx="12"
|
|
12
|
-
cy="12"
|
|
13
|
-
r="10"
|
|
14
|
-
stroke="currentColor"
|
|
15
|
-
stroke-width="4"
|
|
16
|
-
></circle>
|
|
17
|
-
<path
|
|
18
|
-
class="opacity-75"
|
|
19
|
-
fill="currentColor"
|
|
20
|
-
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
|
|
21
|
-
></path>
|
|
22
|
-
</svg>
|
|
23
|
-
</div>
|
|
24
|
-
<div v-if="status === 'loaded' && dashboards.length === 0">
|
|
25
|
-
<div class="text-center">
|
|
26
|
-
<h3 class="mt-2 text-sm font-semibold text-gray-900">No dashboards yet</h3>
|
|
27
|
-
<p class="mt-1 text-sm text-gray-500">Get started by creating a new dashboard.</p>
|
|
28
|
-
<div class="mt-6">
|
|
29
|
-
<button type="button" class="inline-flex items-center rounded-md bg-ultramarine-600 px-3 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">
|
|
30
|
-
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
31
|
-
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
|
32
|
-
</svg>
|
|
33
|
-
New Dashboard
|
|
34
|
-
</button>
|
|
35
|
-
</div>
|
|
36
|
-
</div>
|
|
1
|
+
<div class="dashboards bg-page min-h-[calc(100vh-55px)]">
|
|
2
|
+
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
3
|
+
<div class="flex items-center justify-between mb-6">
|
|
4
|
+
<h1 class="text-lg font-semibold text-content">Dashboards</h1>
|
|
5
|
+
<button
|
|
6
|
+
type="button"
|
|
7
|
+
@click="showCreateDashboardModal = true"
|
|
8
|
+
class="rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary">
|
|
9
|
+
New Dashboard
|
|
10
|
+
</button>
|
|
37
11
|
</div>
|
|
38
12
|
|
|
39
|
-
<div class="
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
13
|
+
<div v-if="status === 'loading'" class="text-center py-12">
|
|
14
|
+
<svg
|
|
15
|
+
class="inline w-8 h-8 animate-spin text-gray-400"
|
|
16
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
17
|
+
fill="none"
|
|
18
|
+
viewBox="0 0 24 24"
|
|
19
|
+
>
|
|
20
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
21
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
|
|
22
|
+
</svg>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div v-else-if="dashboards.length === 0" class="text-center py-12">
|
|
26
|
+
<h3 class="text-sm font-semibold text-content">No dashboards yet</h3>
|
|
27
|
+
<p class="mt-1 text-sm text-content-tertiary">Get started by creating a new dashboard.</p>
|
|
28
|
+
<div class="mt-4">
|
|
29
|
+
<button
|
|
30
|
+
type="button"
|
|
31
|
+
@click="showCreateDashboardModal = true"
|
|
32
|
+
class="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
|
|
33
|
+
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
34
|
+
<path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
|
|
35
|
+
</svg>
|
|
36
|
+
New Dashboard
|
|
37
|
+
</button>
|
|
50
38
|
</div>
|
|
51
|
-
|
|
52
|
-
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
|
53
|
-
<div class="inline-block min-w-full py-2 align-middle">
|
|
54
|
-
<table class="min-w-full divide-y divide-gray-300">
|
|
55
|
-
<thead>
|
|
56
|
-
<tr>
|
|
57
|
-
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6 lg:pl-8">Title</th>
|
|
58
|
-
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 w-[50%]">Description</th>
|
|
59
|
-
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
|
|
60
|
-
</th>
|
|
61
|
-
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6 lg:pr-8">
|
|
62
|
-
</th>
|
|
63
|
-
</tr>
|
|
64
|
-
</thead>
|
|
65
|
-
<tbody class="divide-y divide-gray-200 bg-white">
|
|
66
|
-
<tr v-for="dashboard in dashboards">
|
|
67
|
-
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6 lg:pl-8">{{dashboard.title}}</td>
|
|
68
|
-
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 truncate w-[50%]">{{dashboard.description}}</td>
|
|
69
|
-
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
|
|
70
|
-
<router-link
|
|
71
|
-
:to="'/dashboard/' + dashboard._id + '?edit=true'"
|
|
72
|
-
class="text-ultramarine-600 hover:text-ultramarine-900">
|
|
73
|
-
Edit
|
|
74
|
-
</router-link>
|
|
75
|
-
</td>
|
|
76
|
-
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
|
|
77
|
-
<router-link
|
|
78
|
-
:to="'/dashboard/' + dashboard._id"
|
|
79
|
-
class="text-ultramarine-600 hover:text-ultramarine-900">
|
|
80
|
-
View
|
|
81
|
-
</router-link>
|
|
82
|
-
</td>
|
|
83
|
-
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6 lg:pr-8">
|
|
84
|
-
<button
|
|
85
|
-
@click="showDeleteDashboardModal=dashboard"
|
|
86
|
-
class="text-ultramarine-600 hover:text-ultramarine-900">
|
|
87
|
-
Delete
|
|
88
|
-
</button>
|
|
89
|
-
</td>
|
|
90
|
-
</tr>
|
|
39
|
+
</div>
|
|
91
40
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
41
|
+
<div v-else class="space-y-3">
|
|
42
|
+
<div
|
|
43
|
+
v-for="dashboard in dashboards"
|
|
44
|
+
:key="dashboard._id"
|
|
45
|
+
class="bg-surface rounded-lg border border-edge px-5 py-4 flex items-center justify-between hover:border-edge-strong transition-colors"
|
|
46
|
+
>
|
|
47
|
+
<div class="min-w-0 flex-1 mr-4">
|
|
48
|
+
<router-link
|
|
49
|
+
:to="'/dashboard/' + dashboard._id"
|
|
50
|
+
class="text-sm font-semibold text-content hover:text-primary block truncate">
|
|
51
|
+
{{ dashboard.title }}
|
|
52
|
+
</router-link>
|
|
53
|
+
<p v-if="dashboard.description" class="mt-0.5 text-sm text-content-tertiary truncate">{{ dashboard.description }}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="flex items-center gap-2 shrink-0">
|
|
56
|
+
<router-link
|
|
57
|
+
:to="'/dashboard/' + dashboard._id"
|
|
58
|
+
class="rounded-md bg-primary px-3 py-1.5 text-sm font-semibold text-primary-text shadow-sm hover:bg-primary-hover">
|
|
59
|
+
View
|
|
60
|
+
</router-link>
|
|
61
|
+
<div class="relative" v-click-outside="() => closeMenu(dashboard._id)">
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
@click="toggleMenu(dashboard._id)"
|
|
65
|
+
class="rounded-md bg-surface px-2 py-1.5 text-content-tertiary ring-1 ring-inset ring-gray-300 hover:bg-page">
|
|
66
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
67
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Zm6 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
|
68
|
+
</svg>
|
|
69
|
+
</button>
|
|
70
|
+
<div
|
|
71
|
+
v-if="openMenuId === dashboard._id"
|
|
72
|
+
class="absolute right-0 mt-1 w-36 origin-top-right rounded-md bg-surface shadow-lg ring-1 ring-black/5 z-10"
|
|
73
|
+
>
|
|
74
|
+
<div class="py-1">
|
|
75
|
+
<router-link
|
|
76
|
+
:to="'/dashboard/' + dashboard._id + '?edit=true'"
|
|
77
|
+
class="block px-4 py-2 text-sm text-content-secondary hover:bg-muted">
|
|
78
|
+
Edit
|
|
79
|
+
</router-link>
|
|
80
|
+
<button
|
|
81
|
+
@click="showDeleteDashboardModal = dashboard; openMenuId = null"
|
|
82
|
+
type="button"
|
|
83
|
+
class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-muted">
|
|
84
|
+
Delete
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
95
88
|
</div>
|
|
96
89
|
</div>
|
|
97
90
|
</div>
|
|
98
91
|
</div>
|
|
92
|
+
</div>
|
|
99
93
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</modal>
|
|
94
|
+
<modal v-if="showCreateDashboardModal">
|
|
95
|
+
<template v-slot:body>
|
|
96
|
+
<div class="modal-exit" @click="showCreateDashboardModal = false;">×</div>
|
|
97
|
+
<create-dashboard @close="insertNewDashboard"></create-dashboard>
|
|
98
|
+
</template>
|
|
99
|
+
</modal>
|
|
107
100
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
</div>
|
|
101
|
+
<modal v-if="showDeleteDashboardModal">
|
|
102
|
+
<template v-slot:body>
|
|
103
|
+
<div class="modal-exit" @click="showDeleteDashboardModal = null;">×</div>
|
|
104
|
+
<h2>Are you sure you want to delete "{{showDeleteDashboardModal.title}}"?</h2>
|
|
105
|
+
<div class="flex space-x-2 mt-4">
|
|
106
|
+
<button class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-500 text-sm font-semibold" @click="deleteDashboard(showDeleteDashboardModal)">Yes, delete</button>
|
|
107
|
+
<button class="px-4 py-2 bg-muted text-content-secondary rounded-md hover:bg-muted text-sm font-semibold" @click="showDeleteDashboardModal=null;">Cancel</button>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</modal>
|
|
111
|
+
</div>
|
|
@@ -10,9 +10,18 @@ module.exports = app => app.component('dashboards', {
|
|
|
10
10
|
status: 'loading',
|
|
11
11
|
dashboards: [],
|
|
12
12
|
showCreateDashboardModal: false,
|
|
13
|
-
showDeleteDashboardModal: null
|
|
13
|
+
showDeleteDashboardModal: null,
|
|
14
|
+
openMenuId: null
|
|
14
15
|
}),
|
|
15
16
|
methods: {
|
|
17
|
+
toggleMenu(id) {
|
|
18
|
+
this.openMenuId = this.openMenuId === id ? null : id;
|
|
19
|
+
},
|
|
20
|
+
closeMenu(id) {
|
|
21
|
+
if (this.openMenuId === id) {
|
|
22
|
+
this.openMenuId = null;
|
|
23
|
+
}
|
|
24
|
+
},
|
|
16
25
|
async deleteDashboard(dashboard) {
|
|
17
26
|
if (!dashboard) {
|
|
18
27
|
return;
|
|
@@ -28,6 +37,21 @@ module.exports = app => app.component('dashboards', {
|
|
|
28
37
|
this.showCreateDashboardModal = false;
|
|
29
38
|
}
|
|
30
39
|
},
|
|
40
|
+
directives: {
|
|
41
|
+
clickOutside: {
|
|
42
|
+
beforeMount(el, binding) {
|
|
43
|
+
el._clickOutside = (event) => {
|
|
44
|
+
if (!(event.target === el || el.contains(event.target))) {
|
|
45
|
+
binding.value();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
document.body.addEventListener('click', el._clickOutside);
|
|
49
|
+
},
|
|
50
|
+
unmounted(el) {
|
|
51
|
+
document.body.removeEventListener('click', el._clickOutside);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
31
55
|
async mounted() {
|
|
32
56
|
const { dashboards } = await api.Dashboard.getDashboards();
|
|
33
57
|
this.dashboards = dashboards;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div class="w-full">
|
|
2
|
-
<pre v-if="!isGeoJsonGeometry || !mapVisible" class="w-full whitespace-pre-wrap break-words font-mono text-sm text-
|
|
3
|
-
<div v-show="isGeoJsonGeometry && mapVisible" class="mt-2 border border-
|
|
2
|
+
<pre v-if="!isGeoJsonGeometry || !mapVisible" class="w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0">{{displayValue}}</pre>
|
|
3
|
+
<div v-show="isGeoJsonGeometry && mapVisible" class="mt-2 border border-edge rounded relative">
|
|
4
4
|
<div ref="map" class="h-64 w-full" style="min-height: 256px; height: 256px;"></div>
|
|
5
5
|
<!-- Undo button below map -->
|
|
6
6
|
<div v-if="isEditable && canUndo" class="mt-2 flex justify-end">
|
|
@@ -47,6 +47,7 @@ module.exports = app => app.component('detail-default', {
|
|
|
47
47
|
mapVisible: false,
|
|
48
48
|
mapInstance: null,
|
|
49
49
|
mapLayer: null,
|
|
50
|
+
mapTileLayer: null,
|
|
50
51
|
draggableMarker: null,
|
|
51
52
|
draggableMarkers: [], // For polygon vertices
|
|
52
53
|
hasUnsavedChanges: false,
|
|
@@ -138,6 +139,7 @@ module.exports = app => app.component('detail-default', {
|
|
|
138
139
|
},
|
|
139
140
|
beforeDestroy() {
|
|
140
141
|
this.hideContextMenu();
|
|
142
|
+
document.documentElement.removeEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
141
143
|
if (this.draggableMarker) {
|
|
142
144
|
this.draggableMarker.remove();
|
|
143
145
|
this.draggableMarker = null;
|
|
@@ -150,6 +152,10 @@ module.exports = app => app.component('detail-default', {
|
|
|
150
152
|
});
|
|
151
153
|
this.draggableMarkers = [];
|
|
152
154
|
if (this.mapInstance) {
|
|
155
|
+
if (this.mapTileLayer) {
|
|
156
|
+
this.mapTileLayer.remove();
|
|
157
|
+
this.mapTileLayer = null;
|
|
158
|
+
}
|
|
153
159
|
this.mapInstance.remove();
|
|
154
160
|
this.mapInstance = null;
|
|
155
161
|
this.mapLayer = null;
|
|
@@ -190,9 +196,9 @@ module.exports = app => app.component('detail-default', {
|
|
|
190
196
|
mapContainer.style.position = 'relative';
|
|
191
197
|
}
|
|
192
198
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
this.updateMapTileLayer();
|
|
200
|
+
this._onStudioThemeChanged = () => this.updateMapTileLayer();
|
|
201
|
+
document.documentElement.addEventListener('studio-theme-changed', this._onStudioThemeChanged);
|
|
196
202
|
|
|
197
203
|
// Explicitly invalidate size after creation to ensure proper rendering
|
|
198
204
|
this.$nextTick(() => {
|
|
@@ -212,6 +218,21 @@ module.exports = app => app.component('detail-default', {
|
|
|
212
218
|
}
|
|
213
219
|
});
|
|
214
220
|
},
|
|
221
|
+
getMapTileLayerOptions() {
|
|
222
|
+
const isDark = typeof document !== 'undefined' && document.documentElement.classList.contains('dark');
|
|
223
|
+
return isDark
|
|
224
|
+
? { url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20 }
|
|
225
|
+
: { url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: '© OpenStreetMap contributors' };
|
|
226
|
+
},
|
|
227
|
+
updateMapTileLayer() {
|
|
228
|
+
if (!this.mapInstance || typeof L === 'undefined') return;
|
|
229
|
+
if (this.mapTileLayer) {
|
|
230
|
+
this.mapTileLayer.remove();
|
|
231
|
+
this.mapTileLayer = null;
|
|
232
|
+
}
|
|
233
|
+
const opts = this.getMapTileLayerOptions();
|
|
234
|
+
this.mapTileLayer = L.tileLayer(opts.url, opts).addTo(this.mapInstance);
|
|
235
|
+
},
|
|
215
236
|
updateMapLayer() {
|
|
216
237
|
if (!this.mapInstance || !this.isGeoJsonGeometry) {
|
|
217
238
|
return;
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
Confirm
|
|
11
11
|
</async-button>
|
|
12
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-
|
|
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-edge-strong"
|
|
14
14
|
@click="closeConfirm">
|
|
15
15
|
Cancel
|
|
16
16
|
</button>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
Confirm
|
|
11
11
|
</async-button>
|
|
12
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-
|
|
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-edge-strong"
|
|
14
14
|
@click="closeDelete">
|
|
15
15
|
Cancel
|
|
16
16
|
</button>
|