@mongoosejs/studio 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/actions/Task/getTasksOverTime.js +66 -0
- package/backend/actions/Task/index.js +1 -0
- package/frontend/public/app.js +541 -88
- package/frontend/public/tw.css +14 -12
- package/frontend/src/api.js +6 -0
- package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.html +2 -2
- package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.js +13 -1
- package/frontend/src/dashboard-result/dashboard-result.html +1 -1
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +21 -1
- package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +52 -0
- package/frontend/src/detail-date/detail-date.html +1 -0
- package/frontend/src/detail-date/detail-date.js +123 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +26 -0
- package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +41 -0
- package/frontend/src/document-details/document-property/document-property.html +13 -5
- package/frontend/src/document-details/document-property/document-property.js +14 -1
- package/frontend/src/tasks/tasks.html +34 -20
- package/frontend/src/tasks/tasks.js +158 -5
- package/local.js +2 -1
- package/package.json +1 -1
package/frontend/public/tw.css
CHANGED
|
@@ -1220,6 +1220,10 @@ video {
|
|
|
1220
1220
|
width: 1rem;
|
|
1221
1221
|
}
|
|
1222
1222
|
|
|
1223
|
+
.w-40 {
|
|
1224
|
+
width: 10rem;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1223
1227
|
.w-44 {
|
|
1224
1228
|
width: 11rem;
|
|
1225
1229
|
}
|
|
@@ -1236,6 +1240,10 @@ video {
|
|
|
1236
1240
|
width: 13rem;
|
|
1237
1241
|
}
|
|
1238
1242
|
|
|
1243
|
+
.w-56 {
|
|
1244
|
+
width: 14rem;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1239
1247
|
.w-6 {
|
|
1240
1248
|
width: 1.5rem;
|
|
1241
1249
|
}
|
|
@@ -3060,14 +3068,6 @@ video {
|
|
|
3060
3068
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
3061
3069
|
}
|
|
3062
3070
|
|
|
3063
|
-
.hover\:rounded-lg:hover {
|
|
3064
|
-
border-radius: 0.5rem;
|
|
3065
|
-
}
|
|
3066
|
-
|
|
3067
|
-
.hover\:border:hover {
|
|
3068
|
-
border-width: 1px;
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
3071
|
.hover\:border-edge-strong:hover {
|
|
3072
3072
|
border-color: var(--color-edge-strong);
|
|
3073
3073
|
}
|
|
@@ -3617,10 +3617,6 @@ video {
|
|
|
3617
3617
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
|
3618
3618
|
}
|
|
3619
3619
|
|
|
3620
|
-
.group:hover .group-hover\:text-content-secondary {
|
|
3621
|
-
color: var(--color-content-secondary);
|
|
3622
|
-
}
|
|
3623
|
-
|
|
3624
3620
|
.group:hover .group-hover\:text-primary {
|
|
3625
3621
|
color: var(--color-primary);
|
|
3626
3622
|
}
|
|
@@ -3796,6 +3792,12 @@ video {
|
|
|
3796
3792
|
}
|
|
3797
3793
|
}
|
|
3798
3794
|
|
|
3795
|
+
@media (min-width: 1280px) {
|
|
3796
|
+
.xl\:grid-cols-3 {
|
|
3797
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3799
3801
|
:is(:where(.dark) .dark\:bg-shark-800) {
|
|
3800
3802
|
--tw-bg-opacity: 1;
|
|
3801
3803
|
background-color: rgb(33 37 41 / var(--tw-bg-opacity));
|
package/frontend/src/api.js
CHANGED
|
@@ -191,6 +191,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
191
191
|
getTaskOverview: function getTaskOverview(params) {
|
|
192
192
|
return client.post('', { action: 'Task.getTaskOverview', ...params }).then(res => res.data);
|
|
193
193
|
},
|
|
194
|
+
getTasksOverTime: function getTasksOverTime(params) {
|
|
195
|
+
return client.post('', { action: 'Task.getTasksOverTime', ...params }).then(res => res.data);
|
|
196
|
+
},
|
|
194
197
|
rescheduleTask: function rescheduleTask(params) {
|
|
195
198
|
return client.post('', { action: 'Task.rescheduleTask', ...params }).then(res => res.data);
|
|
196
199
|
},
|
|
@@ -531,6 +534,9 @@ if (window.MONGOOSE_STUDIO_CONFIG.isLambda) {
|
|
|
531
534
|
getTaskOverview: function getTaskOverview(params) {
|
|
532
535
|
return client.post('/Task/getTaskOverview', params).then(res => res.data);
|
|
533
536
|
},
|
|
537
|
+
getTasksOverTime: function getTasksOverTime(params) {
|
|
538
|
+
return client.post('/Task/getTasksOverTime', params).then(res => res.data);
|
|
539
|
+
},
|
|
534
540
|
rescheduleTask: function rescheduleTask(params) {
|
|
535
541
|
return client.post('/Task/rescheduleTask', params).then(res => res.data);
|
|
536
542
|
},
|
|
@@ -9,16 +9,28 @@ module.exports = app => app.component('dashboard-primitive', {
|
|
|
9
9
|
props: ['value'],
|
|
10
10
|
computed: {
|
|
11
11
|
header() {
|
|
12
|
-
if (this.value != null && this.value.$primitive
|
|
12
|
+
if (this.value != null && this.value.$primitive?.header) {
|
|
13
13
|
return this.value.$primitive.header;
|
|
14
14
|
}
|
|
15
15
|
return null;
|
|
16
16
|
},
|
|
17
17
|
displayValue() {
|
|
18
18
|
if (this.value != null && this.value.$primitive) {
|
|
19
|
+
if (this.value.$primitive.value === null) {
|
|
20
|
+
return 'null';
|
|
21
|
+
}
|
|
19
22
|
return this.value.$primitive.value;
|
|
20
23
|
}
|
|
24
|
+
if (this.value === null) {
|
|
25
|
+
return 'null';
|
|
26
|
+
}
|
|
21
27
|
return this.value;
|
|
28
|
+
},
|
|
29
|
+
displayClass() {
|
|
30
|
+
if (this.value == null) {
|
|
31
|
+
return 'text-content-tertiary';
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
22
34
|
}
|
|
23
35
|
}
|
|
24
36
|
});
|
|
@@ -7,7 +7,27 @@
|
|
|
7
7
|
:key="'column-' + index"
|
|
8
8
|
class="bg-slate-50 p-3 text-left text-sm font-semibold text-content border-b border-edge"
|
|
9
9
|
>
|
|
10
|
-
|
|
10
|
+
<div v-if="index === columns.length - 1" class="relative flex items-center gap-2 w-full" ref="dropdown">
|
|
11
|
+
<span class="min-w-0 flex-1">{{ column }}</span>
|
|
12
|
+
<button
|
|
13
|
+
@click.stop="toggleDropdown"
|
|
14
|
+
class="ml-auto rounded p-1 text-content-secondary hover:bg-muted hover:text-content"
|
|
15
|
+
aria-label="Table actions">
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
17
|
+
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4zm0 6a2 2 0 110-4 2 2 0 010 4z" />
|
|
18
|
+
</svg>
|
|
19
|
+
</button>
|
|
20
|
+
<div
|
|
21
|
+
v-if="showDropdown"
|
|
22
|
+
class="absolute right-0 top-full z-10 mt-2 w-56 origin-top-right rounded-md bg-surface py-1 shadow-lg ring-1 ring-black/5">
|
|
23
|
+
<button
|
|
24
|
+
class="block w-full text-left px-4 py-2 text-xs text-content-secondary hover:bg-muted"
|
|
25
|
+
@click="downloadCsv(); showDropdown = false">
|
|
26
|
+
Download as CSV
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<template v-else>{{ column }}</template>
|
|
11
31
|
</th>
|
|
12
32
|
</tr>
|
|
13
33
|
</thead>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* global Blob, URL, document */
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
const template = require('./dashboard-table.html');
|
|
@@ -5,6 +6,11 @@ const template = require('./dashboard-table.html');
|
|
|
5
6
|
module.exports = app => app.component('dashboard-table', {
|
|
6
7
|
template,
|
|
7
8
|
props: ['value'],
|
|
9
|
+
data() {
|
|
10
|
+
return {
|
|
11
|
+
showDropdown: false
|
|
12
|
+
};
|
|
13
|
+
},
|
|
8
14
|
computed: {
|
|
9
15
|
columns() {
|
|
10
16
|
return Array.isArray(this.value?.$table?.columns) ? this.value.$table.columns : [];
|
|
@@ -20,6 +26,46 @@ module.exports = app => app.component('dashboard-table', {
|
|
|
20
26
|
}
|
|
21
27
|
},
|
|
22
28
|
methods: {
|
|
29
|
+
toggleDropdown() {
|
|
30
|
+
this.showDropdown = !this.showDropdown;
|
|
31
|
+
},
|
|
32
|
+
handleBodyClick(event) {
|
|
33
|
+
const dropdownRefs = this.$refs.dropdown;
|
|
34
|
+
const dropdowns = Array.isArray(dropdownRefs) ? dropdownRefs : [dropdownRefs];
|
|
35
|
+
const hasClickInsideDropdown = dropdowns
|
|
36
|
+
.filter(dropdown => dropdown && typeof dropdown.contains === 'function')
|
|
37
|
+
.some(dropdown => dropdown.contains(event.target));
|
|
38
|
+
|
|
39
|
+
if (!hasClickInsideDropdown) {
|
|
40
|
+
this.showDropdown = false;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
neutralizeCsvCell(cell) {
|
|
44
|
+
const value = this.displayValue(cell);
|
|
45
|
+
return /^\s*[=+\-@]/.test(value) ? `'${value}` : value;
|
|
46
|
+
},
|
|
47
|
+
escapeCsvCell(cell) {
|
|
48
|
+
const escapedCell = this.neutralizeCsvCell(cell).replaceAll('"', '""');
|
|
49
|
+
return `"${escapedCell}"`;
|
|
50
|
+
},
|
|
51
|
+
downloadCsv() {
|
|
52
|
+
const header = this.columns.map(this.escapeCsvCell).join(',');
|
|
53
|
+
const rows = this.rows
|
|
54
|
+
.map(row => row.map(this.escapeCsvCell).join(','))
|
|
55
|
+
.join('\n');
|
|
56
|
+
|
|
57
|
+
const csv = [header, rows].filter(v => v.length > 0).join('\n');
|
|
58
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
59
|
+
const url = URL.createObjectURL(blob);
|
|
60
|
+
const anchor = document.createElement('a');
|
|
61
|
+
anchor.href = url;
|
|
62
|
+
anchor.download = 'table.csv';
|
|
63
|
+
document.body.appendChild(anchor);
|
|
64
|
+
anchor.click();
|
|
65
|
+
document.body.removeChild(anchor);
|
|
66
|
+
URL.revokeObjectURL(url);
|
|
67
|
+
this.$toast.success('CSV downloaded!');
|
|
68
|
+
},
|
|
23
69
|
displayValue(cell) {
|
|
24
70
|
if (cell == null) {
|
|
25
71
|
return '';
|
|
@@ -33,5 +79,11 @@ module.exports = app => app.component('dashboard-table', {
|
|
|
33
79
|
}
|
|
34
80
|
return String(cell);
|
|
35
81
|
}
|
|
82
|
+
},
|
|
83
|
+
mounted() {
|
|
84
|
+
document.body.addEventListener('click', this.handleBodyClick);
|
|
85
|
+
},
|
|
86
|
+
unmounted() {
|
|
87
|
+
document.body.removeEventListener('click', this.handleBodyClick);
|
|
36
88
|
}
|
|
37
89
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<pre class="w-full whitespace-pre-wrap break-words font-mono text-sm text-content-secondary m-0">{{displayValue}}</pre>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const template = require('./detail-date.html');
|
|
4
|
+
|
|
5
|
+
module.exports = app => app.component('detail-date', {
|
|
6
|
+
template,
|
|
7
|
+
props: ['value', 'viewMode'],
|
|
8
|
+
emits: ['updated'],
|
|
9
|
+
watch: {
|
|
10
|
+
displayValue: {
|
|
11
|
+
immediate: true,
|
|
12
|
+
handler(val) {
|
|
13
|
+
this.$emit('updated', val);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
computed: {
|
|
18
|
+
format() {
|
|
19
|
+
if (this.viewMode != null && typeof this.viewMode === 'object') {
|
|
20
|
+
return this.viewMode.format;
|
|
21
|
+
}
|
|
22
|
+
return this.viewMode;
|
|
23
|
+
},
|
|
24
|
+
timezone() {
|
|
25
|
+
if (this.viewMode != null && typeof this.viewMode === 'object') {
|
|
26
|
+
return this.viewMode.timezone || '';
|
|
27
|
+
}
|
|
28
|
+
return '';
|
|
29
|
+
},
|
|
30
|
+
parsedDate() {
|
|
31
|
+
if (this.value == null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const date = new Date(this.value);
|
|
35
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
36
|
+
},
|
|
37
|
+
displayValue() {
|
|
38
|
+
if (this.value == null) {
|
|
39
|
+
return String(this.value);
|
|
40
|
+
}
|
|
41
|
+
if (!this.parsedDate) {
|
|
42
|
+
return 'Invalid Date';
|
|
43
|
+
}
|
|
44
|
+
return this.formatDateForDisplay(this.parsedDate);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
methods: {
|
|
48
|
+
formatDateForDisplay(date) {
|
|
49
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
|
|
50
|
+
return 'Invalid Date';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (this.format === 'utc_iso') {
|
|
54
|
+
return date.toISOString();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.format === 'local_browser') {
|
|
58
|
+
return date.toLocaleString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (this.format === 'unix_ms') {
|
|
62
|
+
return String(date.getTime());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (this.format === 'unix_seconds') {
|
|
66
|
+
return String(Math.floor(date.getTime() / 1000));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (this.format === 'duration_relative') {
|
|
70
|
+
return this.formatRelativeDuration(date);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.format === 'custom_tz') {
|
|
74
|
+
return this.formatCustomTimezone(date);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return date.toISOString();
|
|
78
|
+
},
|
|
79
|
+
formatRelativeDuration(date) {
|
|
80
|
+
const diffMs = date.getTime() - Date.now();
|
|
81
|
+
const absMs = Math.abs(diffMs);
|
|
82
|
+
const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: 'auto' });
|
|
83
|
+
const units = [
|
|
84
|
+
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
|
85
|
+
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
|
86
|
+
{ unit: 'day', ms: 24 * 60 * 60 * 1000 },
|
|
87
|
+
{ unit: 'hour', ms: 60 * 60 * 1000 },
|
|
88
|
+
{ unit: 'minute', ms: 60 * 1000 },
|
|
89
|
+
{ unit: 'second', ms: 1000 }
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
for (const { unit, ms } of units) {
|
|
93
|
+
if (absMs >= ms || unit === 'second') {
|
|
94
|
+
const value = Math.round(diffMs / ms);
|
|
95
|
+
return rtf.format(value, unit);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return 'now';
|
|
100
|
+
},
|
|
101
|
+
formatCustomTimezone(date) {
|
|
102
|
+
const tz = (this.timezone || '').trim();
|
|
103
|
+
if (!tz) {
|
|
104
|
+
return `${date.toISOString()} (enter an IANA timezone)`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
109
|
+
timeZone: tz,
|
|
110
|
+
year: 'numeric',
|
|
111
|
+
month: '2-digit',
|
|
112
|
+
day: '2-digit',
|
|
113
|
+
hour: '2-digit',
|
|
114
|
+
minute: '2-digit',
|
|
115
|
+
second: '2-digit',
|
|
116
|
+
timeZoneName: 'short'
|
|
117
|
+
}).format(date);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
return `Invalid timezone: ${tz}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div class="flex items-center gap-2" @click.stop>
|
|
2
|
+
<select
|
|
3
|
+
:value="format"
|
|
4
|
+
@input="onFormatChange($event.target.value)"
|
|
5
|
+
class="text-xs border border-edge rounded-md bg-surface px-2 py-1"
|
|
6
|
+
>
|
|
7
|
+
<option value="utc_iso">UTC (ISO)</option>
|
|
8
|
+
<option value="local_browser">Local (Browser)</option>
|
|
9
|
+
<option value="unix_ms">Unix (ms)</option>
|
|
10
|
+
<option value="unix_seconds">Unix (seconds)</option>
|
|
11
|
+
<option value="duration_relative">Duration relative to now</option>
|
|
12
|
+
<option value="custom_tz">Custom TZ...</option>
|
|
13
|
+
</select>
|
|
14
|
+
<input
|
|
15
|
+
v-if="format === 'custom_tz'"
|
|
16
|
+
:value="timezone"
|
|
17
|
+
@input="onTimezoneChange($event.target.value.trim())"
|
|
18
|
+
:list="timezoneDatalistId"
|
|
19
|
+
type="text"
|
|
20
|
+
placeholder="America/New_York"
|
|
21
|
+
class="text-xs border border-edge rounded-md bg-surface px-2 py-1 w-40"
|
|
22
|
+
>
|
|
23
|
+
<datalist v-if="format === 'custom_tz'" :id="timezoneDatalistId">
|
|
24
|
+
<option v-for="tz in timezones" :key="tz" :value="tz"></option>
|
|
25
|
+
</datalist>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const template = require('./date-view-mode-picker.html');
|
|
4
|
+
|
|
5
|
+
module.exports = app => app.component('date-view-mode-picker', {
|
|
6
|
+
template,
|
|
7
|
+
props: ['viewMode', 'path'],
|
|
8
|
+
emits: ['update:viewMode'],
|
|
9
|
+
computed: {
|
|
10
|
+
format() {
|
|
11
|
+
if (this.viewMode != null && typeof this.viewMode === 'object') {
|
|
12
|
+
return this.viewMode.format;
|
|
13
|
+
}
|
|
14
|
+
return this.viewMode;
|
|
15
|
+
},
|
|
16
|
+
timezone() {
|
|
17
|
+
if (this.viewMode != null && typeof this.viewMode === 'object') {
|
|
18
|
+
return this.viewMode.timezone || '';
|
|
19
|
+
}
|
|
20
|
+
return '';
|
|
21
|
+
},
|
|
22
|
+
timezoneDatalistId() {
|
|
23
|
+
return `timezone-options-${String(this.path?.path || '').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
|
|
24
|
+
},
|
|
25
|
+
timezones() {
|
|
26
|
+
return Intl.supportedValuesOf('timeZone');
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
methods: {
|
|
30
|
+
onFormatChange(newFormat) {
|
|
31
|
+
if (newFormat === 'custom_tz') {
|
|
32
|
+
this.$emit('update:viewMode', { format: 'custom_tz', timezone: this.timezone });
|
|
33
|
+
} else {
|
|
34
|
+
this.$emit('update:viewMode', newFormat);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
onTimezoneChange(newTimezone) {
|
|
38
|
+
this.$emit('update:viewMode', { format: 'custom_tz', timezone: newTimezone });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
@@ -70,6 +70,12 @@
|
|
|
70
70
|
</div>
|
|
71
71
|
</div>
|
|
72
72
|
<div class="flex items-center gap-2">
|
|
73
|
+
<date-view-mode-picker
|
|
74
|
+
v-if="isDatePath"
|
|
75
|
+
:viewMode="dateViewMode"
|
|
76
|
+
:path="path"
|
|
77
|
+
@update:viewMode="dateViewMode = $event"
|
|
78
|
+
></date-view-mode-picker>
|
|
73
79
|
<button
|
|
74
80
|
type="button"
|
|
75
81
|
class="flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-edge-strong bg-surface"
|
|
@@ -125,7 +131,7 @@
|
|
|
125
131
|
v-if="isGeoJsonGeometry"
|
|
126
132
|
:is="getComponentForPath(path)"
|
|
127
133
|
:value="getEditValueForPath(path)"
|
|
128
|
-
:
|
|
134
|
+
:viewMode="detailViewMode"
|
|
129
135
|
:on-change="handleInputChange"
|
|
130
136
|
>
|
|
131
137
|
</component>
|
|
@@ -195,8 +201,9 @@
|
|
|
195
201
|
<div v-else-if="needsTruncation && isValueExpanded" class="relative">
|
|
196
202
|
<component
|
|
197
203
|
:is="getComponentForPath(path)"
|
|
198
|
-
:value="
|
|
199
|
-
:
|
|
204
|
+
:value="rawValue"
|
|
205
|
+
:viewMode="isDatePath ? dateViewMode : detailViewMode"
|
|
206
|
+
@updated="renderedValue = $event"></component>
|
|
200
207
|
<button
|
|
201
208
|
@click="toggleValueExpansion"
|
|
202
209
|
class="mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5"
|
|
@@ -211,8 +218,9 @@
|
|
|
211
218
|
<div v-else>
|
|
212
219
|
<component
|
|
213
220
|
:is="getComponentForPath(path)"
|
|
214
|
-
:value="
|
|
215
|
-
:
|
|
221
|
+
:value="rawValue"
|
|
222
|
+
:viewMode="isDatePath ? dateViewMode : detailViewMode"
|
|
223
|
+
@updated="renderedValue = $event"></component>
|
|
216
224
|
</div>
|
|
217
225
|
</div>
|
|
218
226
|
</div>
|
|
@@ -10,11 +10,15 @@ const appendCSS = require('../../appendCSS');
|
|
|
10
10
|
|
|
11
11
|
appendCSS(require('./document-property.css'));
|
|
12
12
|
|
|
13
|
+
const UNSET = Symbol('unset');
|
|
14
|
+
|
|
13
15
|
module.exports = app => app.component('document-property', {
|
|
14
16
|
template,
|
|
15
17
|
data: function() {
|
|
16
18
|
return {
|
|
17
19
|
dateType: 'picker', // picker, iso
|
|
20
|
+
dateViewMode: 'utc_iso',
|
|
21
|
+
renderedValue: UNSET,
|
|
18
22
|
isCollapsed: false, // Start uncollapsed by default
|
|
19
23
|
isValueExpanded: false, // Track if the value is expanded
|
|
20
24
|
detailViewMode: 'text',
|
|
@@ -31,8 +35,14 @@ module.exports = app => app.component('document-property', {
|
|
|
31
35
|
},
|
|
32
36
|
props: ['path', 'document', 'schemaPaths', 'editting', 'changes', 'invalid', 'highlight'],
|
|
33
37
|
computed: {
|
|
38
|
+
isDatePath() {
|
|
39
|
+
return this.path?.instance === 'Date';
|
|
40
|
+
},
|
|
41
|
+
rawValue() {
|
|
42
|
+
return this.getValueForPath(this.path.path);
|
|
43
|
+
},
|
|
34
44
|
valueAsString() {
|
|
35
|
-
const value = this.
|
|
45
|
+
const value = this.renderedValue !== UNSET ? this.renderedValue : this.rawValue;
|
|
36
46
|
if (value == null) {
|
|
37
47
|
return String(value);
|
|
38
48
|
}
|
|
@@ -164,6 +174,9 @@ module.exports = app => app.component('document-property', {
|
|
|
164
174
|
if (schemaPath.instance === 'Array') {
|
|
165
175
|
return 'detail-array';
|
|
166
176
|
}
|
|
177
|
+
if (schemaPath.instance === 'Date') {
|
|
178
|
+
return 'detail-date';
|
|
179
|
+
}
|
|
167
180
|
return 'detail-default';
|
|
168
181
|
},
|
|
169
182
|
getEditComponentForPath(path) {
|
|
@@ -79,63 +79,77 @@
|
|
|
79
79
|
<div class="text-2xl font-bold">{{cancelledCount}}</div>
|
|
80
80
|
</button>
|
|
81
81
|
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Tasks Over Time Chart -->
|
|
84
|
+
<!--
|
|
85
|
+
Canvas is gated by showOverTimeChart (v-if) so Chart.js is destroyed
|
|
86
|
+
and the DOM node is removed before each refresh. In-place Chart.js
|
|
87
|
+
updates during Vue re-renders from filter changes could freeze the UI
|
|
88
|
+
(dropdowns stuck, chart not updating). See tasks.js getTasks().
|
|
89
|
+
-->
|
|
90
|
+
<div class="mt-6">
|
|
91
|
+
<h2 class="text-lg font-semibold text-content-secondary mb-3">Tasks Over Time</h2>
|
|
92
|
+
<div class="bg-page border border-edge rounded-lg p-4" style="height: 260px;">
|
|
93
|
+
<canvas v-if="showOverTimeChart && overTimeBuckets.length > 0" ref="overTimeChart" style="width:100%;height:100%;"></canvas>
|
|
94
|
+
<div v-else class="flex items-center justify-center h-full text-content-tertiary text-sm">
|
|
95
|
+
No task activity in the selected window
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
82
99
|
|
|
83
100
|
<!-- Grouped Task List -->
|
|
84
101
|
<div class="mt-6">
|
|
85
102
|
<h2 class="text-lg font-semibold text-content-secondary mb-4">Tasks by Name</h2>
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
<div class="flex items-
|
|
89
|
-
<div class="flex-1 cursor-pointer" @click="openTaskGroupDetails(group)">
|
|
90
|
-
<div class="flex items-center gap-
|
|
91
|
-
<div class="font-medium text-
|
|
92
|
-
<svg class="w-
|
|
103
|
+
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
104
|
+
<div v-for="group in tasksByName" :key="group.name" class="border border-edge rounded-lg p-4 group hover:shadow-xl transform hover:-translate-y-1 transition-all duration-200">
|
|
105
|
+
<div class="flex items-start justify-between mb-3">
|
|
106
|
+
<div class="flex-1 cursor-pointer min-w-0 mr-2" @click="openTaskGroupDetails(group)">
|
|
107
|
+
<div class="flex items-center gap-1">
|
|
108
|
+
<div class="font-medium text-sm group-hover:text-primary transition-colors truncate">{{ group.name }}</div>
|
|
109
|
+
<svg class="w-3 h-3 text-gray-400 group-hover:text-primary transition-colors flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
93
110
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
94
111
|
</svg>
|
|
95
112
|
</div>
|
|
96
|
-
<div class="text-
|
|
97
|
-
<div class="text-xs text-primary opacity-0 group-hover:opacity-100 transition-opacity mt-1">
|
|
98
|
-
Click to view details
|
|
99
|
-
</div>
|
|
113
|
+
<div class="text-xs text-content-tertiary">Total: {{ group.totalCount }} tasks</div>
|
|
100
114
|
</div>
|
|
101
|
-
<div class="text-
|
|
102
|
-
|
|
115
|
+
<div class="text-xs text-content-tertiary flex-shrink-0">
|
|
116
|
+
{{ group.lastRun ? new Date(group.lastRun).toLocaleString() : 'Never' }}
|
|
103
117
|
</div>
|
|
104
118
|
</div>
|
|
105
119
|
|
|
106
120
|
<!-- Status Counts -->
|
|
107
|
-
<div class="grid grid-cols-2
|
|
121
|
+
<div class="grid grid-cols-2 gap-1.5">
|
|
108
122
|
<button
|
|
109
123
|
@click.stop="openTaskGroupDetailsWithFilter(group, 'pending')"
|
|
110
124
|
class="bg-yellow-50 border border-yellow-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-yellow-300"
|
|
111
125
|
>
|
|
112
126
|
<div class="text-xs text-yellow-600 font-medium">Pending</div>
|
|
113
|
-
<div class="text-
|
|
127
|
+
<div class="text-base font-bold text-yellow-700">{{ group.statusCounts.pending || 0 }}</div>
|
|
114
128
|
</button>
|
|
115
129
|
<button
|
|
116
130
|
@click.stop="openTaskGroupDetailsWithFilter(group, 'succeeded')"
|
|
117
131
|
class="bg-green-50 border border-green-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-green-300"
|
|
118
132
|
>
|
|
119
133
|
<div class="text-xs text-green-600 font-medium">Succeeded</div>
|
|
120
|
-
<div class="text-
|
|
134
|
+
<div class="text-base font-bold text-green-700">{{ group.statusCounts.succeeded || 0 }}</div>
|
|
121
135
|
</button>
|
|
122
136
|
<button
|
|
123
137
|
@click.stop="openTaskGroupDetailsWithFilter(group, 'failed')"
|
|
124
138
|
class="bg-red-50 border border-red-200 rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-red-300"
|
|
125
139
|
>
|
|
126
140
|
<div class="text-xs text-red-600 font-medium">Failed</div>
|
|
127
|
-
<div class="text-
|
|
141
|
+
<div class="text-base font-bold text-red-700">{{ group.statusCounts.failed || 0 }}</div>
|
|
128
142
|
</button>
|
|
129
143
|
<button
|
|
130
144
|
@click.stop="openTaskGroupDetailsWithFilter(group, 'cancelled')"
|
|
131
145
|
class="bg-page border border-edge rounded-md p-2 text-center shadow-sm hover:shadow-md transform hover:-translate-y-1 transition-all duration-200 cursor-pointer hover:border-edge-strong"
|
|
132
146
|
>
|
|
133
147
|
<div class="text-xs text-gray-600 font-medium">Cancelled</div>
|
|
134
|
-
<div class="text-
|
|
148
|
+
<div class="text-base font-bold text-content-secondary">{{ group.statusCounts.cancelled || 0 }}</div>
|
|
135
149
|
</button>
|
|
136
150
|
</div>
|
|
137
|
-
</
|
|
138
|
-
</
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
139
153
|
</div>
|
|
140
154
|
</div>
|
|
141
155
|
</div>
|