@mongoosejs/studio 0.3.5 → 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.
Files changed (37) hide show
  1. package/backend/actions/ChatThread/createChatMessage.js +12 -2
  2. package/backend/actions/ChatThread/streamChatMessage.js +12 -2
  3. package/backend/actions/Model/createChatMessage.js +12 -2
  4. package/backend/actions/Model/streamChatMessage.js +13 -2
  5. package/backend/actions/Task/getTasksOverTime.js +66 -0
  6. package/backend/actions/Task/index.js +1 -0
  7. package/frontend/public/app.js +809 -110
  8. package/frontend/public/tw.css +19 -16
  9. package/frontend/src/api.js +6 -0
  10. package/frontend/src/chat/chat.html +19 -12
  11. package/frontend/src/chat/chat.js +9 -3
  12. package/frontend/src/create-document/create-document.js +3 -1
  13. package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.html +2 -2
  14. package/frontend/src/dashboard-result/dashboard-primitive/dashboard-primitive.js +13 -1
  15. package/frontend/src/dashboard-result/dashboard-result.html +1 -1
  16. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.html +21 -1
  17. package/frontend/src/dashboard-result/dashboard-table/dashboard-table.js +52 -0
  18. package/frontend/src/detail-date/detail-date.html +1 -0
  19. package/frontend/src/detail-date/detail-date.js +123 -0
  20. package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.html +26 -0
  21. package/frontend/src/document-details/date-view-mode-picker/date-view-mode-picker.js +41 -0
  22. package/frontend/src/document-details/document-property/document-property.html +13 -5
  23. package/frontend/src/document-details/document-property/document-property.js +14 -1
  24. package/frontend/src/getCurrentDateTimeContext.js +17 -0
  25. package/frontend/src/list-json/list-json.js +0 -7
  26. package/frontend/src/modal/modal.js +25 -1
  27. package/frontend/src/models/document-search/document-search.js +3 -0
  28. package/frontend/src/models/models.html +25 -5
  29. package/frontend/src/models/models.js +1 -1
  30. package/frontend/src/navbar/navbar.html +6 -1
  31. package/frontend/src/navbar/navbar.js +1 -6
  32. package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.html +38 -0
  33. package/frontend/src/pro-upgrade-modal/pro-upgrade-modal.js +23 -0
  34. package/frontend/src/tasks/tasks.html +34 -20
  35. package/frontend/src/tasks/tasks.js +158 -5
  36. package/local.js +2 -1
  37. package/package.json +2 -1
@@ -719,10 +719,6 @@ video {
719
719
  right: 0.5rem;
720
720
  }
721
721
 
722
- .right-4 {
723
- right: 1rem;
724
- }
725
-
726
722
  .top-0 {
727
723
  top: 0px;
728
724
  }
@@ -1224,6 +1220,10 @@ video {
1224
1220
  width: 1rem;
1225
1221
  }
1226
1222
 
1223
+ .w-40 {
1224
+ width: 10rem;
1225
+ }
1226
+
1227
1227
  .w-44 {
1228
1228
  width: 11rem;
1229
1229
  }
@@ -1240,6 +1240,10 @@ video {
1240
1240
  width: 13rem;
1241
1241
  }
1242
1242
 
1243
+ .w-56 {
1244
+ width: 14rem;
1245
+ }
1246
+
1243
1247
  .w-6 {
1244
1248
  width: 1.5rem;
1245
1249
  }
@@ -3064,14 +3068,6 @@ video {
3064
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));
3065
3069
  }
3066
3070
 
3067
- .hover\:rounded-lg:hover {
3068
- border-radius: 0.5rem;
3069
- }
3070
-
3071
- .hover\:border:hover {
3072
- border-width: 1px;
3073
- }
3074
-
3075
3071
  .hover\:border-edge-strong:hover {
3076
3072
  border-color: var(--color-edge-strong);
3077
3073
  }
@@ -3328,6 +3324,11 @@ video {
3328
3324
  color: var(--color-primary);
3329
3325
  }
3330
3326
 
3327
+ .hover\:text-red-900:hover {
3328
+ --tw-text-opacity: 1;
3329
+ color: rgb(127 29 29 / var(--tw-text-opacity));
3330
+ }
3331
+
3331
3332
  .hover\:text-slate-700:hover {
3332
3333
  --tw-text-opacity: 1;
3333
3334
  color: rgb(51 65 85 / var(--tw-text-opacity));
@@ -3616,10 +3617,6 @@ video {
3616
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));
3617
3618
  }
3618
3619
 
3619
- .group:hover .group-hover\:text-content-secondary {
3620
- color: var(--color-content-secondary);
3621
- }
3622
-
3623
3620
  .group:hover .group-hover\:text-primary {
3624
3621
  color: var(--color-primary);
3625
3622
  }
@@ -3795,6 +3792,12 @@ video {
3795
3792
  }
3796
3793
  }
3797
3794
 
3795
+ @media (min-width: 1280px) {
3796
+ .xl\:grid-cols-3 {
3797
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3798
+ }
3799
+ }
3800
+
3798
3801
  :is(:where(.dark) .dark\:bg-shark-800) {
3799
3802
  --tw-bg-opacity: 1;
3800
3803
  background-color: rgb(33 37 41 / var(--tw-bg-opacity));
@@ -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
  },
@@ -10,17 +10,6 @@
10
10
  <path stroke-linecap="round" stroke-linejoin="round" d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5" />
11
11
  </svg>
12
12
  </button>
13
- <button
14
- class="fixed top-[65px] right-4 z-10 p-2 rounded-md shadow bg-surface"
15
- :class="hasWorkspace ? 'text-content-secondary hover:bg-muted' : 'text-gray-300 cursor-not-allowed bg-page'"
16
- @click="toggleShareThread"
17
- :disabled="!hasWorkspace || !chatThreadId || sharingThread"
18
- aria-label="Share thread with workspace"
19
- title="Share thread with workspace"
20
- >
21
- <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
22
- <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
23
- </button>
24
13
  <!-- Sidebar: Chat Threads -->
25
14
  <aside
26
15
  class="bg-page border-r overflow-hidden h-full transition-all duration-300 ease-in-out z-20 w-64 fixed lg:relative shrink-0 flex flex-col"
@@ -84,6 +73,18 @@
84
73
  <path stroke-linecap="round" stroke-linejoin="round" d="m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5" />
85
74
  </svg>
86
75
  </button>
76
+ <button
77
+ type="button"
78
+ @click="hasWorkspace ? toggleShareThread() : (showProUpgradeModal = true)"
79
+ class="rounded p-1.5"
80
+ :class="hasWorkspace && chatThreadId ? 'text-gray-400 hover:text-gray-600 hover:bg-muted' : !hasWorkspace ? 'text-gray-400 hover:text-gray-600 hover:bg-muted' : 'text-gray-300 cursor-not-allowed'"
81
+ :disabled="hasWorkspace && !chatThreadId"
82
+ aria-label="Share thread with workspace"
83
+ :title="'Share thread with workspace' + (!hasWorkspace ? ' (requires a pro workspace)' : !chatThreadId ? ': Open a thread first!' : '')"
84
+ >
85
+ <svg v-if="hasWorkspace" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7a2.48 2.48 0 0 0 0-1.39l7.02-4.11a2.5 2.5 0 1 0-.87-1.37L8.04 9.94a2.5 2.5 0 1 0 0 4.12l7.12 4.16a2.5 2.5 0 1 0 .84-1.34l-7.05-4.12c-.04-.02-.08-.05-.11-.07a2.48 2.48 0 0 0 0-1.39c.03-.02.07-.04.11-.07l7.11-4.16c.52.47 1.2.76 1.94.76a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0-1.94.94L7.97 8.43a2.5 2.5 0 1 0 0 7.14l9.09 5.3c.52-.47 1.2-.76 1.94-.76a2.5 2.5 0 1 0 0-5z"/></svg>
86
+ <svg v-else xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 1a5 5 0 00-5 5v3H6a2 2 0 00-2 2v9a2 2 0 002 2h12a2 2 0 002-2v-9a2 2 0 00-2-2h-1V6a5 5 0 00-5-5zm-3 8V6a3 3 0 016 0v3H9zm9 2v9H6v-9h12z"/></svg>
87
+ </button>
87
88
  </div>
88
89
  </aside>
89
90
 
@@ -118,7 +119,7 @@
118
119
  @input="adjustTextareaHeight"
119
120
  @keydown.enter.exact.prevent="handleEnter"
120
121
  ></textarea>
121
- <button class="bg-blue-600 text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
122
+ <button class="bg-primary hover:bg-primary-hover text-white px-4 h-[42px] rounded disabled:bg-gray-600" :disabled="sendingMessage">
122
123
  <svg v-if="sendingMessage" style="height: 1em" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
123
124
  <g>
124
125
  <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2" opacity="0.3" />
@@ -132,4 +133,10 @@
132
133
  </form>
133
134
  </div>
134
135
  </main>
136
+
137
+ <pro-upgrade-modal
138
+ :show="showProUpgradeModal"
139
+ feature-description="Sharing threads lets you collaborate with your team by sharing chat threads within your workspace."
140
+ @close="showProUpgradeModal = false"
141
+ ></pro-upgrade-modal>
135
142
  </div>
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const api = require('../api');
4
+ const getCurrentDateTimeContext = require('../getCurrentDateTimeContext');
4
5
  const template = require('./chat.html');
5
6
 
6
7
  module.exports = {
@@ -15,7 +16,8 @@ module.exports = {
15
16
  chatMessages: [],
16
17
  hideSidebar: null,
17
18
  sharingThread: false,
18
- threadSearch: ''
19
+ threadSearch: '',
20
+ showProUpgradeModal: false
19
21
  }),
20
22
  methods: {
21
23
  async sendMessage() {
@@ -44,7 +46,11 @@ module.exports = {
44
46
  }
45
47
  });
46
48
 
47
- const params = { chatThreadId: this.chatThreadId, content };
49
+ const params = {
50
+ chatThreadId: this.chatThreadId,
51
+ content,
52
+ currentDateTime: getCurrentDateTimeContext()
53
+ };
48
54
  let userChatMessage = null;
49
55
  let assistantChatMessage = null;
50
56
  for await (const event of api.ChatThread.streamChatMessage(params)) {
@@ -151,7 +157,7 @@ module.exports = {
151
157
  },
152
158
  async toggleShareThread() {
153
159
  if (!this.chatThreadId || !this.hasWorkspace) {
154
- return;
160
+ throw new Error('Cannot share thread: chatThreadId or hasWorkspace is missing');
155
161
  }
156
162
  this.sharingThread = true;
157
163
  try {
@@ -11,6 +11,7 @@ const ObjectId = new Proxy(BSON.ObjectId, {
11
11
  });
12
12
 
13
13
  const appendCSS = require('../appendCSS');
14
+ const getCurrentDateTimeContext = require('../getCurrentDateTimeContext');
14
15
 
15
16
  appendCSS(require('./create-document.css'));
16
17
 
@@ -49,7 +50,8 @@ module.exports = app => app.component('create-document', {
49
50
  for await (const event of api.Model.streamChatMessage({
50
51
  model: this.currentModel,
51
52
  content: prompt,
52
- documentData: this.aiOriginalDocument
53
+ documentData: this.aiOriginalDocument,
54
+ currentDateTime: getCurrentDateTimeContext()
53
55
  })) {
54
56
  if (event?.textPart) {
55
57
  this.aiSuggestion += event.textPart;
@@ -2,7 +2,7 @@
2
2
  <div v-if="header" class="border-b border-gray-100 px-2 pb-2 text-xl font-bold">
3
3
  {{header}}
4
4
  </div>
5
- <div class="text-xl p-2">
5
+ <div class="text-xl p-2" :class="displayClass">
6
6
  {{displayValue}}
7
7
  </div>
8
- </div>
8
+ </div>
@@ -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.header) {
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
  });
@@ -1,6 +1,6 @@
1
1
  <div>
2
2
  <div v-if="Array.isArray(result)">
3
- <div v-for="el in result" :key="el._id || el.finishedEvaluatingAt">
3
+ <div v-for="(el, index) in result">
4
4
  <component
5
5
  class="bg-surface shadow-sm ring-1 ring-gray-900/5 sm:rounded-xl"
6
6
  :is="getComponentForValue(el)"
@@ -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
- {{ column }}
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
- :view-mode="detailViewMode"
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="getValueForPath(path.path)"
199
- :view-mode="detailViewMode"></component>
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="getValueForPath(path.path)"
215
- :view-mode="detailViewMode"></component>
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.getValueForPath(this.path.path);
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) {
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const time = require('time-commando');
4
+
5
+ module.exports = function getCurrentDateTimeContext() {
6
+ const date = time.now();
7
+ const components = [
8
+ date.getFullYear(),
9
+ date.getMonth() + 1,
10
+ date.getDate(),
11
+ date.getHours(),
12
+ date.getMinutes(),
13
+ date.getSeconds()
14
+ ].map(num => num.toString().padStart(2, '0'));
15
+ const [yyyy, mm, dd, hh, mi, ss] = components;
16
+ return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}`;
17
+ };