@conduction/nextcloud-vue 2.96.0 → 2.98.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/CnAppRoot/CnAppRoot.vue2.js +17 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- 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/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
- 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/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.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/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.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 +344 -21
- 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/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +420 -5
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useOrganisationStore.js +115 -0
- package/dist/esm/composables/useOrganisationStore.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +21 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +36 -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 +725 -383
- package/dist/esm/utils/builtInFormatters.js +34 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/matrixCells.js +158 -0
- package/dist/esm/utils/matrixCells.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +121 -0
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
- 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 +14443 -10519
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +725 -383
- package/l10n/en.json +21 -1
- package/l10n/nl.json +36 -1
- package/package.json +2 -2
- package/scripts/merge-l10n.js +196 -0
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
- package/src/components/CnChartWidget/CnChartWidget.md +2 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
- package/src/components/CnChecklistRunWidget/index.js +27 -0
- 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/CnDashboardPage/CnDashboardPage.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +26 -8
- package/src/components/CnDetailPage/CnDetailPage.md +48 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
- package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/src/components/CnIndexPage/CnIndexPage.md +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +367 -27
- 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/components/CnOrderedListField/CnOrderedListField.vue +386 -0
- package/src/components/CnOrderedListField/index.js +7 -0
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +2 -0
- package/src/composables/indexSources.js +419 -4
- package/src/composables/useOrganisationStore.js +111 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +42 -5
- package/src/utils/builtInFormatters.js +33 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/matrixCells.js +154 -0
- package/src/utils/nextcloudDirectory.js +117 -0
- package/src/utils/queryPresets.js +13 -2
- package/src/utils/scopeListLayout.js +37 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnChecklistRunWidget — self-registers the `checklist-run` widget type (a
|
|
6
|
+
* checklist answered a page of questions at a time, with a note and photos)
|
|
7
|
+
* into the shared dashboardWidgetRegistry at module load. Renderer only: the
|
|
8
|
+
* questions and where the answers go are declared in the manifest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnChecklistRunWidget from './CnChecklistRunWidget.vue'
|
|
12
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('checklist-run', {
|
|
15
|
+
renderer: CnChecklistRunWidget,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
questions: { register: '', schema: '', filter: {} },
|
|
19
|
+
pageSize: 1,
|
|
20
|
+
},
|
|
21
|
+
displayName: 'Checklist run',
|
|
22
|
+
icon: 'ClipboardCheckOutline',
|
|
23
|
+
surfaces: ['detail-page', 'app-dashboard'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export { CnChecklistRunWidget }
|
|
27
|
+
export default CnChecklistRunWidget
|
|
@@ -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
|
*
|
|
@@ -408,6 +408,7 @@ import { fillTemplate, joinTemplateParts } from '../../utils/fillTemplate.js'
|
|
|
408
408
|
import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
|
|
409
409
|
import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
|
|
410
410
|
import { nextSortState } from '../../utils/multiColumnSort.js'
|
|
411
|
+
import { readPath } from '../../utils/readPath.js'
|
|
411
412
|
import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
412
413
|
import { DEFAULT_ROW_INDICATOR_CAP, resolveRowIndicators } from '../../utils/rowIndicators.js'
|
|
413
414
|
import { columnsFromSchema } from '../../utils/schema.js'
|
|
@@ -1841,8 +1842,11 @@ export default {
|
|
|
1841
1842
|
if (v === undefined) {
|
|
1842
1843
|
return this.aggregateLoading ? '…' : '—'
|
|
1843
1844
|
}
|
|
1845
|
+
if (v === null && !(typeof col.aggregate.zeroLabel === 'string' && col.aggregate.zeroLabel !== '')) {
|
|
1846
|
+
return '—'
|
|
1847
|
+
}
|
|
1844
1848
|
const zeroLabel = col.aggregate.zeroLabel
|
|
1845
|
-
if ((v === 0 || v === '0') && typeof zeroLabel === 'string' && zeroLabel !== '') {
|
|
1849
|
+
if ((v === 0 || v === '0' || v === null) && typeof zeroLabel === 'string' && zeroLabel !== '') {
|
|
1846
1850
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(zeroLabel) : zeroLabel
|
|
1847
1851
|
}
|
|
1848
1852
|
return v
|
|
@@ -1872,17 +1876,21 @@ export default {
|
|
|
1872
1876
|
},
|
|
1873
1877
|
|
|
1874
1878
|
/**
|
|
1875
|
-
* For every column that declares `aggregate
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1879
|
+
* For every column that declares `aggregate`, issue one request per
|
|
1880
|
+
* visible row against the related OpenRegister collection and cache the
|
|
1881
|
+
* answer in `aggregateValues`: `op: "count"` asks `_limit=0` for the
|
|
1882
|
+
* total, `op: "max"` asks for the one object with the highest `field`
|
|
1883
|
+
* (`_limit=1`, sorted descending) and keeps that value, null when there
|
|
1884
|
+
* is none (shown as `zeroLabel`, else a dash).
|
|
1878
1885
|
* Batched with `Promise.all`; a per-request failure degrades that one
|
|
1879
1886
|
* cell to `'—'` (logged), never the page. A monotonic request id
|
|
1880
1887
|
* discards a stale batch when `rows` / columns change mid-flight.
|
|
1881
1888
|
*
|
|
1882
1889
|
* @return {Promise<void>}
|
|
1890
|
+
* @spec openspec/changes/masked-formatter-max-aggregate/specs/index-page/spec.md#requirement-a-column-aggregate-can-take-the-highest-value
|
|
1883
1891
|
*/
|
|
1884
1892
|
async loadAggregates() {
|
|
1885
|
-
const aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && c.aggregate.op === 'count')
|
|
1893
|
+
const aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && (c.aggregate.op === 'count' || (c.aggregate.op === 'max' && c.aggregate.field)))
|
|
1886
1894
|
if (aggCols.length === 0) {
|
|
1887
1895
|
if (Object.keys(this.aggregateValues).length > 0) {
|
|
1888
1896
|
this.aggregateValues = {}
|
|
@@ -1903,11 +1911,21 @@ export default {
|
|
|
1903
1911
|
continue
|
|
1904
1912
|
}
|
|
1905
1913
|
const where = this.resolveAggregateWhere(agg.where, row)
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1914
|
+
// `max`: the highest `field` among the related objects, as the
|
|
1915
|
+
// first of them sorted on it descending ("Laatste contact").
|
|
1916
|
+
const isMax = agg.op === 'max'
|
|
1917
|
+
const params = isMax
|
|
1918
|
+
? { ...where, _limit: 1, [`_order[${agg.field}]`]: 'desc' }
|
|
1919
|
+
: { ...where, _limit: 0 }
|
|
1920
|
+
jobs.push(axios.get(generateUrl(`/apps/openregister/api/objects/${agg.register}/${agg.schema}`), { params })
|
|
1909
1921
|
.then((res) => {
|
|
1910
1922
|
const d = res && res.data
|
|
1923
|
+
if (isMax) {
|
|
1924
|
+
const first = d && Array.isArray(d.results) ? d.results[0] : undefined
|
|
1925
|
+
const top = first === undefined ? undefined : readPath(first, agg.field)
|
|
1926
|
+
next[rowKey][col.key] = top === undefined || top === '' ? null : top
|
|
1927
|
+
return
|
|
1928
|
+
}
|
|
1911
1929
|
next[rowKey][col.key] = (d && (d.total ?? (Array.isArray(d.results) ? d.results.length : undefined))) ?? 0
|
|
1912
1930
|
})
|
|
1913
1931
|
.catch((e) => {
|
|
@@ -434,6 +434,54 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
434
434
|
|
|
435
435
|
`previousTo` and `nextTo` (default `null`) take `useListNavigation`'s `previousRoute` / `nextRoute` and turn the previous and next controls into real links, so they can be opened in a new tab. The link navigates by itself; `previous-record` / `next-record` are still emitted, with `{ event, to }`, and `goPrevious` / `goNext` do not push again when they receive that payload.
|
|
436
436
|
|
|
437
|
+
## Records that are not objects (`config.entitySource`)
|
|
438
|
+
|
|
439
|
+
A detail page reads its record from the object store through `register` +
|
|
440
|
+
`schema`. A record that is not a register/schema object names a registered
|
|
441
|
+
entity source instead, the same registry and the same name `CnIndexPage`
|
|
442
|
+
lists it with. `organisations` reads the OpenRegister organisation whose uuid
|
|
443
|
+
is the route's id (`GET /apps/openregister/api/organisations/{uuid}`):
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"id": "organisation-detail",
|
|
448
|
+
"route": "/organisations/:id",
|
|
449
|
+
"type": "detail",
|
|
450
|
+
"config": {
|
|
451
|
+
"entitySource": "organisations",
|
|
452
|
+
"statusPill": { "field": "status" },
|
|
453
|
+
"typePill": { "field": "type" },
|
|
454
|
+
"headerFields": [{ "key": "slug" }],
|
|
455
|
+
"layout": [{ "id": "1", "widgetId": "facts", "gridX": 0, "gridY": 0, "gridWidth": 6 }],
|
|
456
|
+
"widgets": [{
|
|
457
|
+
"id": "facts",
|
|
458
|
+
"type": "facts",
|
|
459
|
+
"title": "Details",
|
|
460
|
+
"content": { "fields": [
|
|
461
|
+
{ "path": "slug", "label": "Slug" },
|
|
462
|
+
{ "path": "created", "label": "Created", "format": "date" },
|
|
463
|
+
{ "path": "contactEmail", "label": "Contact", "format": "email" },
|
|
464
|
+
{ "path": "kvk", "label": "KvK" }
|
|
465
|
+
] }
|
|
466
|
+
}]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The record feeds everything a fetched object feeds: the title, the pills, the
|
|
472
|
+
header fields, the widgets, and the `@objectId` / `@object.*` tokens. A tab
|
|
473
|
+
that lists register objects belonging to the organisation filters on
|
|
474
|
+
`@objectId`, e.g. `{ "type": "object-list", "content": { "register": "...",
|
|
475
|
+
"schema": "...", "filter": { "tenantRef": "@objectId" } } }`. The record's
|
|
476
|
+
`id` is its uuid, as an object's is.
|
|
477
|
+
|
|
478
|
+
`entitySource` wins over `register` + `schema`. A name that is unknown, or a
|
|
479
|
+
source that cannot read one record, shows no record and warns in the console.
|
|
480
|
+
A refusal (403) or a missing record (404) shows the not-found state; which
|
|
481
|
+
records the viewer may read is OpenRegister's decision. Refresh and
|
|
482
|
+
`cn:page:refresh` re-read the record. There is no Edit: a record source has
|
|
483
|
+
no schema form.
|
|
484
|
+
|
|
437
485
|
## Not found
|
|
438
486
|
|
|
439
487
|
When the schema-driven fetch answers 404 the page shows a not-found state whose back button is a router link to `notFoundRoute` (default `null`, which means the app root `/`). `notFoundRouteLabel` names that page in the button's "Back to …" label.
|