@conduction/nextcloud-vue 2.96.0 → 2.97.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/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -7
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +301 -18
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +182 -3
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/l10n/en.json.js +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +301 -177
- package/dist/esm/utils/queryPresets.js +13 -2
- package/dist/esm/utils/queryPresets.js.map +1 -1
- package/dist/esm/utils/scopeListLayout.js +37 -1
- package/dist/esm/utils/scopeListLayout.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +3009 -1907
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +301 -177
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -1
- package/package.json +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
- package/src/components/CnConversationThread/CnConversationThread.md +12 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
- package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
- package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +324 -24
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
- package/src/components/CnIndexPage/useNamedSource.js +1 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
- package/src/composables/indexSources.js +182 -3
- package/src/schemas/app-manifest-v2.schema.json +11 -4
- package/src/utils/queryPresets.js +13 -2
- package/src/utils/scopeListLayout.js +37 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json
CHANGED
|
@@ -1145,6 +1145,8 @@
|
|
|
1145
1145
|
"Due tomorrow": "Due tomorrow",
|
|
1146
1146
|
"Due in {days} days": "Due in {days} days",
|
|
1147
1147
|
"Available": "Available",
|
|
1148
|
+
"{value} of {limit}": "{value} of {limit}",
|
|
1149
|
+
"{value} of {limit}, over the limit": "{value} of {limit}, over the limit",
|
|
1148
1150
|
"Ready to claim": "Ready to claim",
|
|
1149
1151
|
"In progress": "In progress",
|
|
1150
1152
|
"Completed": "Completed",
|
|
@@ -1584,7 +1586,11 @@
|
|
|
1584
1586
|
"{s} s": "{s} s",
|
|
1585
1587
|
"About this {app} installation.": "About this {app} installation.",
|
|
1586
1588
|
"{version}, up to date": "{version}, up to date",
|
|
1587
|
-
"for an SLA": "for an SLA"
|
|
1589
|
+
"for an SLA": "for an SLA",
|
|
1590
|
+
"About {name}": "About {name}",
|
|
1591
|
+
"Sent message": "Sent message",
|
|
1592
|
+
"Received message": "Received message",
|
|
1593
|
+
"Loading messages…": "Loading messages…"
|
|
1588
1594
|
},
|
|
1589
1595
|
"plurals": {
|
|
1590
1596
|
"{count} selected": [
|
package/l10n/nl.json
CHANGED
|
@@ -1171,6 +1171,8 @@
|
|
|
1171
1171
|
"Due tomorrow": "Morgen",
|
|
1172
1172
|
"Due in {days} days": "Over {days} dagen",
|
|
1173
1173
|
"Available": "Beschikbaar",
|
|
1174
|
+
"{value} of {limit}": "{value} van {limit}",
|
|
1175
|
+
"{value} of {limit}, over the limit": "{value} van {limit}, boven de limiet",
|
|
1174
1176
|
"Ready to claim": "Klaar om op te pakken",
|
|
1175
1177
|
"In progress": "In behandeling",
|
|
1176
1178
|
"Completed": "Afgerond",
|
|
@@ -1770,7 +1772,11 @@
|
|
|
1770
1772
|
"{s} s": "{s} s",
|
|
1771
1773
|
"About this {app} installation.": "Over deze {app}-installatie.",
|
|
1772
1774
|
"{version}, up to date": "{version}, bijgewerkt",
|
|
1773
|
-
"for an SLA": "voor een SLA"
|
|
1775
|
+
"for an SLA": "voor een SLA",
|
|
1776
|
+
"About {name}": "Over {name}",
|
|
1777
|
+
"Sent message": "Verstuurd bericht",
|
|
1778
|
+
"Received message": "Ontvangen bericht",
|
|
1779
|
+
"Loading messages…": "Berichten laden…"
|
|
1774
1780
|
},
|
|
1775
1781
|
"plurals": {
|
|
1776
1782
|
"{count} selected": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.97.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -24,6 +24,24 @@
|
|
|
24
24
|
</span>
|
|
25
25
|
</template>
|
|
26
26
|
|
|
27
|
+
<!-- Built-in "actions" widget: row actions as inline buttons ("Aan zaak
|
|
28
|
+
koppelen", "Nieuwe zaak"). The page resolves the buttons
|
|
29
|
+
(widgetProps.items) and runs a click (widgetProps.run). -->
|
|
30
|
+
<template v-else-if="widget === 'actions'">
|
|
31
|
+
<span class="cn-cell-renderer__actions" data-testid="cn-cell-actions">
|
|
32
|
+
<NcButton
|
|
33
|
+
v-for="item in actionItems"
|
|
34
|
+
:key="item.key"
|
|
35
|
+
:variant="item.variant"
|
|
36
|
+
size="small"
|
|
37
|
+
:disabled="item.disabled"
|
|
38
|
+
:data-testid="`cn-cell-action-${item.key}`"
|
|
39
|
+
@click.stop="runAction(item)">
|
|
40
|
+
{{ item.label }}
|
|
41
|
+
</NcButton>
|
|
42
|
+
</span>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
27
45
|
<!-- Built-in "badge" widget: renders the value as a status pill. An enum
|
|
28
46
|
value shows its label (x-enum-labels, translated) unless a formatter
|
|
29
47
|
shapes it; the colour stays keyed on the raw value. -->
|
|
@@ -350,7 +368,7 @@
|
|
|
350
368
|
|
|
351
369
|
<script>
|
|
352
370
|
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
353
|
-
import { NcAvatar, NcCheckboxRadioSwitch, NcDateTime } from '@nextcloud/vue'
|
|
371
|
+
import { NcAvatar, NcButton, NcCheckboxRadioSwitch, NcDateTime } from '@nextcloud/vue'
|
|
354
372
|
import CheckBold from 'vue-material-design-icons/CheckBold.vue'
|
|
355
373
|
import CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'
|
|
356
374
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
@@ -398,6 +416,7 @@ export default {
|
|
|
398
416
|
CnGroupNameCell,
|
|
399
417
|
CheckBold,
|
|
400
418
|
NcAvatar,
|
|
419
|
+
NcButton,
|
|
401
420
|
NcDateTime,
|
|
402
421
|
},
|
|
403
422
|
|
|
@@ -586,6 +605,24 @@ export default {
|
|
|
586
605
|
},
|
|
587
606
|
|
|
588
607
|
computed: {
|
|
608
|
+
/**
|
|
609
|
+
* The `actions` widget's buttons for this row: the page's resolved
|
|
610
|
+
* `widgetProps.items` (a list, or a function of the row returning
|
|
611
|
+
* one), each `{ key, label, variant?, disabled? }`.
|
|
612
|
+
*
|
|
613
|
+
* @return {Array<{key: string, label: string, variant: string, disabled: boolean}>}
|
|
614
|
+
* @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
|
|
615
|
+
*/
|
|
616
|
+
actionItems() {
|
|
617
|
+
const raw = typeof this.widgetProps?.items === 'function' ? this.widgetProps.items(this.row) : this.widgetProps?.items
|
|
618
|
+
const items = Array.isArray(raw) ? raw : []
|
|
619
|
+
return items.map((item) => ({
|
|
620
|
+
...item,
|
|
621
|
+
variant: item.variant || 'secondary',
|
|
622
|
+
disabled: item.disabled === true,
|
|
623
|
+
}))
|
|
624
|
+
},
|
|
625
|
+
|
|
589
626
|
/**
|
|
590
627
|
* Whether the "switch" widget is on: the pending state while a toggle
|
|
591
628
|
* is in flight, else `widgetProps.checked(row)`, else the value.
|
|
@@ -1419,6 +1456,19 @@ export default {
|
|
|
1419
1456
|
},
|
|
1420
1457
|
|
|
1421
1458
|
methods: {
|
|
1459
|
+
/**
|
|
1460
|
+
* Run an `actions` widget button for this row through the page.
|
|
1461
|
+
*
|
|
1462
|
+
* @param {{key: string}} item The button.
|
|
1463
|
+
* @return {void}
|
|
1464
|
+
* @spec openspec/changes/table-actions-column/specs/index-page/spec.md#requirement-a-column-can-show-row-actions-as-buttons
|
|
1465
|
+
*/
|
|
1466
|
+
runAction(item) {
|
|
1467
|
+
if (typeof this.widgetProps?.run === 'function') {
|
|
1468
|
+
this.widgetProps.run(item.key, this.row)
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
|
|
1422
1472
|
/**
|
|
1423
1473
|
* The "switch" widget was flipped: show the new state and hand it to
|
|
1424
1474
|
* `widgetProps.onToggle(row, on)`. A rejection puts the switch back.
|
|
@@ -1555,6 +1605,12 @@ export default {
|
|
|
1555
1605
|
</script>
|
|
1556
1606
|
|
|
1557
1607
|
<style scoped>
|
|
1608
|
+
.cn-cell-renderer__actions {
|
|
1609
|
+
display: inline-flex;
|
|
1610
|
+
flex-wrap: wrap;
|
|
1611
|
+
gap: 8px;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1558
1614
|
.cn-cell-renderer__icon-label {
|
|
1559
1615
|
display: inline-flex;
|
|
1560
1616
|
align-items: center;
|
|
@@ -49,3 +49,15 @@ Posting to an endpoint. The reply goes out as `{ message, ...payload }`. `title-
|
|
|
49
49
|
</template>
|
|
50
50
|
</CnConversationThread>
|
|
51
51
|
```
|
|
52
|
+
|
|
53
|
+
While the messages load, `loading` makes the thread say so (in a status region) instead of showing the empty text. A
|
|
54
|
+
message's `channel` is shown after its time:
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<CnConversationThread
|
|
58
|
+
title="Contact"
|
|
59
|
+
:loading="false"
|
|
60
|
+
:messages="[
|
|
61
|
+
{ id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them', channel: 'Message box', text: 'Thank you.' },
|
|
62
|
+
]" />
|
|
63
|
+
```
|
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
{{ title }}
|
|
18
18
|
</component>
|
|
19
19
|
|
|
20
|
-
<p v-if="rows.length === 0"
|
|
20
|
+
<p v-if="loading && rows.length === 0"
|
|
21
|
+
class="cn-conversation-thread__empty"
|
|
22
|
+
role="status"
|
|
23
|
+
data-testid="cn-conversation-loading">
|
|
24
|
+
{{ loadingText }}
|
|
25
|
+
</p>
|
|
26
|
+
<p v-else-if="rows.length === 0" class="cn-conversation-thread__empty" data-testid="cn-conversation-empty">
|
|
21
27
|
{{ emptyText }}
|
|
22
28
|
</p>
|
|
23
29
|
|
|
@@ -32,8 +38,10 @@
|
|
|
32
38
|
<span class="cn-conversation-thread__avatar" aria-hidden="true">{{ row.initials }}</span>
|
|
33
39
|
<div class="cn-conversation-thread__bubble">
|
|
34
40
|
<span class="cn-conversation-thread__meta">
|
|
41
|
+
<span class="hidden-visually">{{ row.directionLabel }}</span>
|
|
35
42
|
<span class="cn-conversation-thread__author">{{ row.author }}</span>
|
|
36
43
|
<time v-if="row.timeLabel" :datetime="row.datetime || null">{{ row.timeLabel }}</time>
|
|
44
|
+
<span v-if="row.channel" class="cn-conversation-thread__channel" data-testid="cn-conversation-channel">{{ row.channel }}</span>
|
|
37
45
|
</span>
|
|
38
46
|
<span class="cn-conversation-thread__text">{{ row.text }}</span>
|
|
39
47
|
</div>
|
|
@@ -123,9 +131,10 @@ export default {
|
|
|
123
131
|
|
|
124
132
|
props: {
|
|
125
133
|
/**
|
|
126
|
-
* The messages, oldest first: `{ id?, author?, time?, text, side? }`.
|
|
134
|
+
* The messages, oldest first: `{ id?, author?, time?, text, side?, channel? }`.
|
|
127
135
|
* `side` is `us` or `them` (default `them`). `time` is an ISO
|
|
128
|
-
* date, or any text to show as is.
|
|
136
|
+
* date, or any text to show as is. `channel` is shown after the time
|
|
137
|
+
* ("E-mail", "Phone note").
|
|
129
138
|
*/
|
|
130
139
|
messages: {
|
|
131
140
|
type: Array,
|
|
@@ -189,6 +198,12 @@ export default {
|
|
|
189
198
|
default: () => ({}),
|
|
190
199
|
},
|
|
191
200
|
|
|
201
|
+
/** Whether the messages are still loading; with none yet the thread says so. */
|
|
202
|
+
loading: {
|
|
203
|
+
type: Boolean,
|
|
204
|
+
default: false,
|
|
205
|
+
},
|
|
206
|
+
|
|
192
207
|
/** Shown when there are no messages yet. */
|
|
193
208
|
emptyText: {
|
|
194
209
|
type: String,
|
|
@@ -221,7 +236,7 @@ export default {
|
|
|
221
236
|
/**
|
|
222
237
|
* The messages, with author, side, time and initials resolved.
|
|
223
238
|
*
|
|
224
|
-
* @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.
|
|
239
|
+
* @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string, channel: string, directionLabel: string}>} The rows.
|
|
225
240
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
226
241
|
*/
|
|
227
242
|
rows() {
|
|
@@ -241,6 +256,8 @@ export default {
|
|
|
241
256
|
text: message.text === undefined || message.text === null ? '' : String(message.text),
|
|
242
257
|
timeLabel: time.label,
|
|
243
258
|
datetime: time.datetime,
|
|
259
|
+
channel: typeof message.channel === 'string' ? message.channel : '',
|
|
260
|
+
directionLabel: side === 'us' ? t('nextcloud-vue', 'Sent message') : t('nextcloud-vue', 'Received message'),
|
|
244
261
|
}
|
|
245
262
|
})
|
|
246
263
|
},
|
|
@@ -264,6 +281,11 @@ export default {
|
|
|
264
281
|
fallbackLabel() {
|
|
265
282
|
return t('nextcloud-vue', 'Conversation')
|
|
266
283
|
},
|
|
284
|
+
|
|
285
|
+
/** @return {string} What the thread says while its messages load. */
|
|
286
|
+
loadingText() {
|
|
287
|
+
return t('nextcloud-vue', 'Loading messages…')
|
|
288
|
+
},
|
|
267
289
|
},
|
|
268
290
|
|
|
269
291
|
methods: {
|
|
@@ -455,4 +477,8 @@ export default {
|
|
|
455
477
|
gap: calc(2.5 * var(--default-grid-baseline));
|
|
456
478
|
justify-content: flex-end;
|
|
457
479
|
}
|
|
480
|
+
|
|
481
|
+
.cn-conversation-thread__channel {
|
|
482
|
+
color: var(--color-text-maxcontrast);
|
|
483
|
+
}
|
|
458
484
|
</style>
|
|
@@ -7,29 +7,44 @@
|
|
|
7
7
|
<div class="cn-case-widget-card">
|
|
8
8
|
<CnConversationThread
|
|
9
9
|
:messages="messages"
|
|
10
|
-
:title="content.title || ''"
|
|
10
|
+
:title="tr(content.title || '')"
|
|
11
11
|
:allowReply="content.allowReply !== false"
|
|
12
|
-
:replyLabel="content.replyLabel
|
|
13
|
-
:placeholder="content.placeholder || ''"
|
|
14
|
-
:sendLabel="content.sendLabel
|
|
15
|
-
:endpoint="endpoint"
|
|
16
|
-
:
|
|
12
|
+
:replyLabel="content.replyLabel ? tr(content.replyLabel) : undefined"
|
|
13
|
+
:placeholder="tr(content.placeholder || '')"
|
|
14
|
+
:sendLabel="content.sendLabel ? tr(content.sendLabel) : undefined"
|
|
15
|
+
:endpoint="replyAction ? '' : endpoint"
|
|
16
|
+
:loading="loading"
|
|
17
|
+
:emptyText="content.emptyText ? tr(content.emptyText) : undefined"
|
|
18
|
+
@send="onSend"
|
|
17
19
|
@sent="onSent" />
|
|
18
20
|
</div>
|
|
19
21
|
</template>
|
|
20
22
|
|
|
21
23
|
<script>
|
|
22
24
|
import CnConversationThread from './CnConversationThread.vue'
|
|
25
|
+
import { dispatchAction } from '../../utils/actionsDispatcher.js'
|
|
26
|
+
import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'
|
|
23
27
|
import { readPath } from '../../utils/readPath.js'
|
|
28
|
+
import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* CnConversationWidget: the `conversation` detail widget type.
|
|
27
32
|
*
|
|
28
|
-
* Renders CnConversationThread from
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
+
* Renders CnConversationThread from the messages of a record, oldest first.
|
|
34
|
+
* The messages are a list on the bound record (`field`), or the objects of a
|
|
35
|
+
* register list (`source: { register, schema, filter?, limit? }`, the filter
|
|
36
|
+
* token-resolved like a record-list's: `{ "case": "@objectId" }`).
|
|
37
|
+
*
|
|
38
|
+
* Mapping keys name where each message keeps its parts: `textField` (alias
|
|
39
|
+
* `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias
|
|
40
|
+
* `senderField`), `sideField` (alias `directionField`) with `usValue` (alias
|
|
41
|
+
* `outValue`) for the messages the organisation sent, and `channelField` with
|
|
42
|
+
* an optional `channelMap` from stored value to label ("email" to "E-mail").
|
|
43
|
+
*
|
|
44
|
+
* The reply box posts to `endpoint` (`@objectId` filled in), or runs
|
|
45
|
+
* `replyAction`, an action descriptor the app decides the meaning of: it is
|
|
46
|
+
* dispatched with `args: [{ text, objectId }]` and `props.text`. After a send
|
|
47
|
+
* the source is read again.
|
|
33
48
|
*/
|
|
34
49
|
export default {
|
|
35
50
|
name: 'CnConversationWidget',
|
|
@@ -38,8 +53,19 @@ export default {
|
|
|
38
53
|
CnConversationThread,
|
|
39
54
|
},
|
|
40
55
|
|
|
56
|
+
inject: {
|
|
57
|
+
cnTranslate: { default: () => (key) => key },
|
|
58
|
+
/** Pre-bound action dispatcher of the page, for `replyAction`. */
|
|
59
|
+
cnDispatchAction: { default: null },
|
|
60
|
+
},
|
|
61
|
+
|
|
41
62
|
props: {
|
|
42
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* The widget's content: `field` (the dot-path to the messages on the
|
|
65
|
+
* record) or `source`, the mapping keys, `endpoint` or `replyAction`,
|
|
66
|
+
* and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,
|
|
67
|
+
* `emptyText`, translated).
|
|
68
|
+
*/
|
|
43
69
|
content: {
|
|
44
70
|
type: Object,
|
|
45
71
|
default: () => ({}),
|
|
@@ -62,29 +88,63 @@ export default {
|
|
|
62
88
|
return {
|
|
63
89
|
/** Replies sent from this widget that the record does not hold yet. */
|
|
64
90
|
sentHere: [],
|
|
91
|
+
/** The messages read from `source`. */
|
|
92
|
+
listed: [],
|
|
93
|
+
/** Whether `source` is being read. */
|
|
94
|
+
loading: false,
|
|
65
95
|
}
|
|
66
96
|
},
|
|
67
97
|
|
|
68
98
|
computed: {
|
|
99
|
+
/** @return {object|null} The register list the messages come from, or null. */
|
|
100
|
+
source() {
|
|
101
|
+
const source = this.content?.source
|
|
102
|
+
return source && typeof source === 'object' && source.register && source.schema ? source : null
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
/** @return {object|null} The reply action, or null. */
|
|
106
|
+
replyAction() {
|
|
107
|
+
const action = this.content?.replyAction
|
|
108
|
+
return action && typeof action === 'object' ? action : null
|
|
109
|
+
},
|
|
110
|
+
|
|
69
111
|
/**
|
|
70
|
-
* The
|
|
112
|
+
* The messages in the thread's shape, oldest first, plus the replies
|
|
113
|
+
* sent here.
|
|
71
114
|
*
|
|
72
115
|
* @return {Array<object>} The messages.
|
|
73
116
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
117
|
+
* @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts
|
|
74
118
|
*/
|
|
75
119
|
messages() {
|
|
76
|
-
const c = this.content
|
|
77
|
-
const
|
|
78
|
-
const usValue = String(c.
|
|
79
|
-
const
|
|
120
|
+
const c = this.content || {}
|
|
121
|
+
const raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')
|
|
122
|
+
const usValue = String(c.outValue ?? c.usValue ?? 'us')
|
|
123
|
+
const channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}
|
|
124
|
+
const stored = (Array.isArray(raw) ? raw : [])
|
|
80
125
|
.filter((message) => message && typeof message === 'object')
|
|
81
|
-
.map((message) =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
.map((message, index) => {
|
|
127
|
+
const channel = c.channelField ? readPath(message, c.channelField) : undefined
|
|
128
|
+
const channelKey = channel === undefined || channel === null ? '' : String(channel)
|
|
129
|
+
return {
|
|
130
|
+
id: message.id ?? `message-${index}`,
|
|
131
|
+
author: readPath(message, c.senderField || c.authorField || 'author'),
|
|
132
|
+
time: readPath(message, c.timestampField || c.timeField || 'time'),
|
|
133
|
+
text: readPath(message, c.bodyField || c.textField || 'text'),
|
|
134
|
+
side: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',
|
|
135
|
+
channel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),
|
|
136
|
+
order: index,
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
const at = (message) => {
|
|
140
|
+
const ms = Date.parse(message.time)
|
|
141
|
+
return Number.isNaN(ms) ? null : ms
|
|
142
|
+
}
|
|
143
|
+
stored.sort((a, b) => {
|
|
144
|
+
const ta = at(a)
|
|
145
|
+
const tb = at(b)
|
|
146
|
+
return (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb
|
|
147
|
+
})
|
|
88
148
|
return [...stored, ...this.sentHere]
|
|
89
149
|
},
|
|
90
150
|
|
|
@@ -101,6 +161,11 @@ export default {
|
|
|
101
161
|
}
|
|
102
162
|
return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
|
|
103
163
|
},
|
|
164
|
+
|
|
165
|
+
/** @return {object} The token context for `source.filter` and `replyAction`. */
|
|
166
|
+
tokenCtx() {
|
|
167
|
+
return { objectId: this.objectId, object: this.objectData || {} }
|
|
168
|
+
},
|
|
104
169
|
},
|
|
105
170
|
|
|
106
171
|
watch: {
|
|
@@ -108,9 +173,87 @@ export default {
|
|
|
108
173
|
objectData() {
|
|
109
174
|
this.sentHere = []
|
|
110
175
|
},
|
|
176
|
+
|
|
177
|
+
objectId: {
|
|
178
|
+
immediate: true,
|
|
179
|
+
handler() {
|
|
180
|
+
this.loadSource()
|
|
181
|
+
},
|
|
182
|
+
},
|
|
111
183
|
},
|
|
112
184
|
|
|
113
185
|
methods: {
|
|
186
|
+
/**
|
|
187
|
+
* Translate through the app's translate.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} key The source string.
|
|
190
|
+
* @return {string}
|
|
191
|
+
*/
|
|
192
|
+
tr(key) {
|
|
193
|
+
return key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read the messages from `source`. A filter whose token does not
|
|
198
|
+
* resolve sends no request; a failed read leaves the thread empty.
|
|
199
|
+
*
|
|
200
|
+
* @return {Promise<void>}
|
|
201
|
+
* @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts
|
|
202
|
+
*/
|
|
203
|
+
async loadSource() {
|
|
204
|
+
if (!this.source) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const raw = this.source.filter || {}
|
|
208
|
+
const filter = resolveDeepTokens(raw, this.tokenCtx)
|
|
209
|
+
const emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))
|
|
210
|
+
if (emptied || hasUnresolvedDeepTokens(filter)) {
|
|
211
|
+
this.listed = []
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
this.loading = true
|
|
215
|
+
try {
|
|
216
|
+
this.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })
|
|
217
|
+
this.sentHere = []
|
|
218
|
+
} catch {
|
|
219
|
+
this.listed = []
|
|
220
|
+
} finally {
|
|
221
|
+
this.loading = false
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Run `replyAction` with the reply, when there is one; the endpoint
|
|
227
|
+
* path is the thread's own.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} text The reply.
|
|
230
|
+
* @return {Promise<void>}
|
|
231
|
+
* @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action
|
|
232
|
+
*/
|
|
233
|
+
async onSend(text) {
|
|
234
|
+
if (!this.replyAction) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
const resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)
|
|
238
|
+
const action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }
|
|
239
|
+
const result = typeof this.cnDispatchAction === 'function'
|
|
240
|
+
? this.cnDispatchAction(action)
|
|
241
|
+
: dispatchAction(action, { router: this.$router || null })
|
|
242
|
+
try {
|
|
243
|
+
const outcome = await result
|
|
244
|
+
if (outcome === false) {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (this.source) {
|
|
251
|
+
await this.loadSource()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
this.onSent({ text })
|
|
255
|
+
},
|
|
256
|
+
|
|
114
257
|
/**
|
|
115
258
|
* Show an accepted reply at once, without waiting for the record.
|
|
116
259
|
*
|