@conduction/nextcloud-vue 2.56.0-beta.6 → 2.56.0-beta.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/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +14 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +39 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +46 -4
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +224 -9
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +79 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js +42 -3
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js +39 -6
- package/dist/esm/components/CnBannerWidgetForm/CnBannerWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +46 -0
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +11 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js +10 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +39 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +16 -0
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +147 -89
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +259 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +131 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +10 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +116 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +24 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +503 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +300 -142
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js +237 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js +120 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewList.vue3.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js +10 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +77 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js +27 -0
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +169 -12
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +15 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js +37 -3
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js +44 -16
- package/dist/esm/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +132 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js +10 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js +150 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js +106 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js +9 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +47 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js +17 -0
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +49 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js +6 -0
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js +46 -0
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js +15 -3
- package/dist/esm/components/CnObjectKanban/CnObjectKanban.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +37 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +29 -13
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +50 -3
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js +10 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +186 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +46 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js +11 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +298 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +77 -0
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js +169 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js +48 -0
- package/dist/esm/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js +35 -0
- package/dist/esm/components/CnTabs/CnTab.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +120 -7
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js +57 -5
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +59 -4
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +6 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +484 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +115 -0
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js +10 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js +189 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue2.js.map +1 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js +74 -0
- package/dist/esm/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +82 -8
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/index.js +10 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +54 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +54 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +931 -4
- package/dist/esm/utils/dateVariant.js +124 -0
- package/dist/esm/utils/dateVariant.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +260 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -0
- package/dist/esm/utils/dueRule.js +84 -0
- package/dist/esm/utils/dueRule.js.map +1 -0
- package/dist/esm/utils/fetchAggregate.js +37 -1
- package/dist/esm/utils/fetchAggregate.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +186 -0
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +5 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/readPath.js +30 -0
- package/dist/esm/utils/readPath.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14970 -8991
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +931 -4
- package/l10n/en.json +54 -1
- package/l10n/nl.json +54 -1
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +3 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppNav/CnAppNav.md +26 -0
- package/src/components/CnAppNav/CnAppNav.vue +104 -1
- package/src/components/CnBannerWidget/CnBannerWidget.vue +365 -10
- package/src/components/CnBannerWidgetForm/CnBannerWidgetForm.vue +65 -3
- package/src/components/CnBoardView/CnBoardView.vue +77 -0
- package/src/components/CnBrandStripe/CnBrandStripe.md +22 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +100 -0
- package/src/components/CnBrandStripe/index.js +4 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +207 -1
- package/src/components/CnConversationThread/CnConversationThread.md +51 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +457 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +144 -0
- package/src/components/CnConversationThread/index.js +4 -0
- package/src/components/CnDetailPage/CnDetailPage.md +16 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +683 -48
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.md +55 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewList.vue +384 -0
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +108 -0
- package/src/components/CnDocumentReviewList/index.js +4 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +189 -14
- package/src/components/CnHeaderWidgetForm/CnHeaderWidgetForm.vue +53 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +135 -3
- package/src/components/CnNextStepCard/CnNextStepCard.md +40 -0
- package/src/components/CnNextStepCard/CnNextStepCard.vue +319 -0
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +59 -0
- package/src/components/CnNextStepCard/index.js +4 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +49 -0
- package/src/components/CnObjectDataWidgetForm/CnObjectDataWidgetForm.vue +6 -0
- package/src/components/CnObjectKanban/CnObjectKanban.vue +86 -2
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +3 -0
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +60 -1
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +50 -3
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +46 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +291 -0
- package/src/components/CnSegmentedControl/index.js +4 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.md +15 -0
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +414 -0
- package/src/components/CnStackedBarWidget/index.js +14 -0
- package/src/components/CnStackedBarWidgetForm/CnStackedBarWidgetForm.vue +224 -0
- package/src/components/CnStackedBarWidgetForm/index.js +4 -0
- package/src/components/CnTabs/CnTab.vue +35 -0
- package/src/components/CnTabs/CnTabs.vue +244 -10
- package/src/components/CnTabsWidget/CnTabsWidget.vue +63 -4
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.md +31 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +668 -0
- package/src/components/CnWeekStripWidget/index.js +14 -0
- package/src/components/CnWeekStripWidgetForm/CnWeekStripWidgetForm.vue +265 -0
- package/src/components/CnWeekStripWidgetForm/index.js +4 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +74 -0
- package/src/components/index.js +11 -0
- package/src/css/detail-page.css +47 -0
- package/src/index.js +9 -0
- package/src/schemas/app-manifest-v2.schema.json +389 -1
- package/src/schemas/app-manifest.schema.json +2 -1
- package/src/utils/dateVariant.js +120 -0
- package/src/utils/detailActionModel.js +256 -0
- package/src/utils/dueRule.js +94 -0
- package/src/utils/fetchAggregate.js +36 -0
- package/src/utils/fetchFilterCounts.js +182 -0
- package/src/utils/index.js +1 -0
- package/src/utils/libraryWidgetKeys.js +5 -0
- package/src/utils/readPath.js +27 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<section
|
|
8
|
+
class="cn-conversation-thread"
|
|
9
|
+
data-testid="cn-conversation-thread"
|
|
10
|
+
:aria-labelledby="title ? titleId : null"
|
|
11
|
+
:aria-label="title ? null : fallbackLabel">
|
|
12
|
+
<component
|
|
13
|
+
:is="titleTag"
|
|
14
|
+
v-if="title"
|
|
15
|
+
:id="titleId"
|
|
16
|
+
class="cn-conversation-thread__title">
|
|
17
|
+
{{ title }}
|
|
18
|
+
</component>
|
|
19
|
+
|
|
20
|
+
<p v-if="rows.length === 0" class="cn-conversation-thread__empty" data-testid="cn-conversation-empty">
|
|
21
|
+
{{ emptyText }}
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<ol v-else class="cn-conversation-thread__messages">
|
|
25
|
+
<li
|
|
26
|
+
v-for="row in rows"
|
|
27
|
+
:key="row.key"
|
|
28
|
+
class="cn-conversation-thread__message"
|
|
29
|
+
:class="`cn-conversation-thread__message--${row.side}`"
|
|
30
|
+
data-testid="cn-conversation-message"
|
|
31
|
+
:data-side="row.side">
|
|
32
|
+
<span class="cn-conversation-thread__avatar" aria-hidden="true">{{ row.initials }}</span>
|
|
33
|
+
<div class="cn-conversation-thread__bubble">
|
|
34
|
+
<span class="cn-conversation-thread__meta">
|
|
35
|
+
<span class="cn-conversation-thread__author">{{ row.author }}</span>
|
|
36
|
+
<time v-if="row.timeLabel" :datetime="row.datetime || null">{{ row.timeLabel }}</time>
|
|
37
|
+
</span>
|
|
38
|
+
<span class="cn-conversation-thread__text">{{ row.text }}</span>
|
|
39
|
+
</div>
|
|
40
|
+
</li>
|
|
41
|
+
</ol>
|
|
42
|
+
|
|
43
|
+
<form
|
|
44
|
+
v-if="allowReply"
|
|
45
|
+
class="cn-conversation-thread__reply"
|
|
46
|
+
data-testid="cn-conversation-reply"
|
|
47
|
+
@submit.prevent="onSend">
|
|
48
|
+
<label :for="inputId" class="cn-conversation-thread__reply-label">{{ replyLabel }}</label>
|
|
49
|
+
<textarea
|
|
50
|
+
:id="inputId"
|
|
51
|
+
v-model="draft"
|
|
52
|
+
class="cn-conversation-thread__input"
|
|
53
|
+
rows="2"
|
|
54
|
+
:placeholder="placeholder || null"
|
|
55
|
+
:disabled="sending"
|
|
56
|
+
:aria-describedby="errorMessage ? errorId : null"
|
|
57
|
+
data-testid="cn-conversation-input"
|
|
58
|
+
@keydown.ctrl.enter.prevent="onSend"
|
|
59
|
+
@keydown.meta.enter.prevent="onSend" />
|
|
60
|
+
<p
|
|
61
|
+
v-if="errorMessage"
|
|
62
|
+
:id="errorId"
|
|
63
|
+
class="cn-conversation-thread__error"
|
|
64
|
+
role="alert"
|
|
65
|
+
data-testid="cn-conversation-error">
|
|
66
|
+
{{ errorMessage }}
|
|
67
|
+
</p>
|
|
68
|
+
<div class="cn-conversation-thread__reply-actions">
|
|
69
|
+
<!-- @slot reply-actions Extra buttons before Send, for example "Save draft". -->
|
|
70
|
+
<!-- @binding {string} draft The text in the reply box. -->
|
|
71
|
+
<slot name="reply-actions" :draft="draft" />
|
|
72
|
+
<NcButton
|
|
73
|
+
variant="primary"
|
|
74
|
+
type="submit"
|
|
75
|
+
:disabled="!canSend"
|
|
76
|
+
data-testid="cn-conversation-send">
|
|
77
|
+
{{ sendLabel }}
|
|
78
|
+
</NcButton>
|
|
79
|
+
</div>
|
|
80
|
+
</form>
|
|
81
|
+
</section>
|
|
82
|
+
</template>
|
|
83
|
+
|
|
84
|
+
<script>
|
|
85
|
+
import axios from '@nextcloud/axios'
|
|
86
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
87
|
+
import { NcButton } from '@nextcloud/vue'
|
|
88
|
+
import { prefixUrl } from '../../utils/headers.js'
|
|
89
|
+
|
|
90
|
+
let conversationThreadUid = 0
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* CnConversationThread: the messages between you and the other party, and a box to answer.
|
|
94
|
+
*
|
|
95
|
+
* Each message has an author, a time and a side: `us` for the organisation,
|
|
96
|
+
* `them` for the resident or customer. The reply box emits `send`, and also
|
|
97
|
+
* posts to `endpoint` when one is set. It needs no Talk and no other app:
|
|
98
|
+
* where the messages are stored is the host's business.
|
|
99
|
+
*
|
|
100
|
+
* ```vue
|
|
101
|
+
* <CnConversationThread
|
|
102
|
+
* title="Contact with the resident"
|
|
103
|
+
* reply-label="Answer to the resident"
|
|
104
|
+
* :messages="[
|
|
105
|
+
* { id: 1, author: 'Sanne de Vries', time: '2026-10-04T10:52:00', side: 'them',
|
|
106
|
+
* text: 'Thank you. I would like to see all documents.' },
|
|
107
|
+
* { id: 2, time: '2026-10-04T10:24:00', side: 'us',
|
|
108
|
+
* text: 'Construction starts in March.' },
|
|
109
|
+
* ]"
|
|
110
|
+
* @send="saveReply" />
|
|
111
|
+
* ```
|
|
112
|
+
*
|
|
113
|
+
* The `conversation` detail widget type renders this thread from a field on
|
|
114
|
+
* the record.
|
|
115
|
+
*/
|
|
116
|
+
export default {
|
|
117
|
+
name: 'CnConversationThread',
|
|
118
|
+
|
|
119
|
+
components: {
|
|
120
|
+
NcButton,
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
props: {
|
|
124
|
+
/**
|
|
125
|
+
* The messages, oldest first: `{ id?, author?, time?, text, side? }`.
|
|
126
|
+
* `side` is `us` or `them` (default `them`). `time` is an ISO
|
|
127
|
+
* date, or any text to show as is.
|
|
128
|
+
*/
|
|
129
|
+
messages: {
|
|
130
|
+
type: Array,
|
|
131
|
+
default: () => [],
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** Heading of the thread. */
|
|
135
|
+
title: {
|
|
136
|
+
type: String,
|
|
137
|
+
default: '',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
/** Element the heading renders as. */
|
|
141
|
+
titleTag: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'h3',
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/** Whether the reply box renders. Turn it off for a closed or read-only record. */
|
|
147
|
+
allowReply: {
|
|
148
|
+
type: Boolean,
|
|
149
|
+
default: true,
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/** Label of the reply box. */
|
|
153
|
+
replyLabel: {
|
|
154
|
+
type: String,
|
|
155
|
+
default: () => t('nextcloud-vue', 'Reply'),
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/** Placeholder inside the reply box. */
|
|
159
|
+
placeholder: {
|
|
160
|
+
type: String,
|
|
161
|
+
default: '',
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
/** Label of the send button. */
|
|
165
|
+
sendLabel: {
|
|
166
|
+
type: String,
|
|
167
|
+
default: () => t('nextcloud-vue', 'Send'),
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/** Author shown on an `us` message that names none. */
|
|
171
|
+
usLabel: {
|
|
172
|
+
type: String,
|
|
173
|
+
default: () => t('nextcloud-vue', 'You'),
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* URL the reply is POSTed to as `{ message }`. Empty means the host
|
|
178
|
+
* handles `send` itself and nothing is requested.
|
|
179
|
+
*/
|
|
180
|
+
endpoint: {
|
|
181
|
+
type: String,
|
|
182
|
+
default: '',
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/** Extra fields sent along with `message` when `endpoint` is set. */
|
|
186
|
+
payload: {
|
|
187
|
+
type: Object,
|
|
188
|
+
default: () => ({}),
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/** Shown when there are no messages yet. */
|
|
192
|
+
emptyText: {
|
|
193
|
+
type: String,
|
|
194
|
+
default: () => t('nextcloud-vue', 'No messages yet.'),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
emits: [
|
|
199
|
+
/** A reply was submitted. Payload: the text. */
|
|
200
|
+
'send',
|
|
201
|
+
/** The endpoint accepted the reply. Payload: the response body. */
|
|
202
|
+
'sent',
|
|
203
|
+
/** The endpoint refused the reply. Payload: the error. */
|
|
204
|
+
'error',
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
data() {
|
|
208
|
+
conversationThreadUid += 1
|
|
209
|
+
return {
|
|
210
|
+
draft: '',
|
|
211
|
+
sending: false,
|
|
212
|
+
errorMessage: '',
|
|
213
|
+
titleId: `cn-conversation-title-${conversationThreadUid}`,
|
|
214
|
+
inputId: `cn-conversation-input-${conversationThreadUid}`,
|
|
215
|
+
errorId: `cn-conversation-error-${conversationThreadUid}`,
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
computed: {
|
|
220
|
+
/**
|
|
221
|
+
* The messages, with author, side, time and initials resolved.
|
|
222
|
+
*
|
|
223
|
+
* @return {Array<{key: string, author: string, initials: string, side: string, text: string, timeLabel: string, datetime: string}>} The rows.
|
|
224
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
225
|
+
*/
|
|
226
|
+
rows() {
|
|
227
|
+
return (Array.isArray(this.messages) ? this.messages : [])
|
|
228
|
+
.filter((message) => message && typeof message === 'object')
|
|
229
|
+
.map((message, index) => {
|
|
230
|
+
const side = message.side === 'us' ? 'us' : 'them'
|
|
231
|
+
const author = typeof message.author === 'string' && message.author !== ''
|
|
232
|
+
? message.author
|
|
233
|
+
: (side === 'us' ? this.usLabel : '')
|
|
234
|
+
const time = this.formatTime(message.time)
|
|
235
|
+
return {
|
|
236
|
+
key: message.id === undefined || message.id === null ? `message-${index}` : String(message.id),
|
|
237
|
+
author,
|
|
238
|
+
initials: this.initialsOf(author),
|
|
239
|
+
side,
|
|
240
|
+
text: message.text === undefined || message.text === null ? '' : String(message.text),
|
|
241
|
+
timeLabel: time.label,
|
|
242
|
+
datetime: time.datetime,
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
},
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Whether there is something to send and nothing in flight.
|
|
249
|
+
*
|
|
250
|
+
* @return {boolean} True when Send is enabled.
|
|
251
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
252
|
+
*/
|
|
253
|
+
canSend() {
|
|
254
|
+
return !this.sending && this.draft.trim() !== ''
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Accessible name for a thread without a title.
|
|
259
|
+
*
|
|
260
|
+
* @return {string} The label.
|
|
261
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
262
|
+
*/
|
|
263
|
+
fallbackLabel() {
|
|
264
|
+
return t('nextcloud-vue', 'Conversation')
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
methods: {
|
|
269
|
+
/**
|
|
270
|
+
* Up to two initials of a name, for the avatar.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} name The author.
|
|
273
|
+
* @return {string} The initials, upper case.
|
|
274
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
275
|
+
*/
|
|
276
|
+
initialsOf(name) {
|
|
277
|
+
const parts = String(name || '').trim().split(/\s+/).filter(Boolean)
|
|
278
|
+
if (parts.length === 0) {
|
|
279
|
+
return ''
|
|
280
|
+
}
|
|
281
|
+
const first = parts[0].charAt(0)
|
|
282
|
+
const last = parts.length > 1 ? parts[parts.length - 1].charAt(0) : ''
|
|
283
|
+
return (first + last).toUpperCase()
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Turn a message time into what the reader sees and what `<time>` carries.
|
|
288
|
+
*
|
|
289
|
+
* @param {unknown} value An ISO date, a Date, or text to show as is.
|
|
290
|
+
* @return {{label: string, datetime: string}} The visible label and the machine value.
|
|
291
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
292
|
+
*/
|
|
293
|
+
formatTime(value) {
|
|
294
|
+
if (value === null || value === undefined || value === '') {
|
|
295
|
+
return { label: '', datetime: '' }
|
|
296
|
+
}
|
|
297
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
298
|
+
const looksLikeDate = value instanceof Date || typeof value === 'number' || /^\d{4}-\d{2}-\d{2}/.test(String(value))
|
|
299
|
+
if (!looksLikeDate || Number.isNaN(date.getTime())) {
|
|
300
|
+
return { label: String(value), datetime: '' }
|
|
301
|
+
}
|
|
302
|
+
const label = date.toLocaleString(undefined, { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })
|
|
303
|
+
return { label, datetime: date.toISOString() }
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Submit the reply: tell the host, and post it when an endpoint is set.
|
|
308
|
+
*
|
|
309
|
+
* Without an endpoint the box clears straight away, because the host
|
|
310
|
+
* owns the send. With one it clears only when the request succeeds, so
|
|
311
|
+
* a refused reply is still there to try again.
|
|
312
|
+
*
|
|
313
|
+
* @return {Promise<void>}
|
|
314
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
315
|
+
*/
|
|
316
|
+
async onSend() {
|
|
317
|
+
if (!this.canSend) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
const text = this.draft.trim()
|
|
321
|
+
this.errorMessage = ''
|
|
322
|
+
this.$emit('send', text)
|
|
323
|
+
if (!this.endpoint) {
|
|
324
|
+
this.draft = ''
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
this.sending = true
|
|
328
|
+
try {
|
|
329
|
+
const response = await axios.post(prefixUrl(this.endpoint), { ...this.payload, message: text })
|
|
330
|
+
this.draft = ''
|
|
331
|
+
this.$emit('sent', response?.data)
|
|
332
|
+
} catch (error) {
|
|
333
|
+
this.errorMessage = t('nextcloud-vue', 'The message could not be sent. Try again.')
|
|
334
|
+
this.$emit('error', error)
|
|
335
|
+
} finally {
|
|
336
|
+
this.sending = false
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
</script>
|
|
342
|
+
|
|
343
|
+
<style scoped>
|
|
344
|
+
.cn-conversation-thread {
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
gap: calc(3.5 * var(--default-grid-baseline));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.cn-conversation-thread__title {
|
|
351
|
+
font-size: 1.1em;
|
|
352
|
+
font-weight: 700;
|
|
353
|
+
margin: 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.cn-conversation-thread__empty {
|
|
357
|
+
color: var(--color-text-maxcontrast);
|
|
358
|
+
margin: 0;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.cn-conversation-thread__messages {
|
|
362
|
+
display: flex;
|
|
363
|
+
flex-direction: column;
|
|
364
|
+
gap: calc(3.5 * var(--default-grid-baseline));
|
|
365
|
+
list-style: none;
|
|
366
|
+
margin: 0;
|
|
367
|
+
padding: 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.cn-conversation-thread__message {
|
|
371
|
+
display: flex;
|
|
372
|
+
gap: calc(3 * var(--default-grid-baseline));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.cn-conversation-thread__message--us {
|
|
376
|
+
flex-direction: row-reverse;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.cn-conversation-thread__avatar {
|
|
380
|
+
align-items: center;
|
|
381
|
+
background: var(--color-background-dark);
|
|
382
|
+
border-radius: 50%;
|
|
383
|
+
color: var(--color-main-text);
|
|
384
|
+
display: flex;
|
|
385
|
+
flex: none;
|
|
386
|
+
font-size: 0.8em;
|
|
387
|
+
font-weight: 700;
|
|
388
|
+
height: 34px;
|
|
389
|
+
justify-content: center;
|
|
390
|
+
width: 34px;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.cn-conversation-thread__bubble {
|
|
394
|
+
background: var(--color-background-hover);
|
|
395
|
+
border-radius: var(--border-radius-large);
|
|
396
|
+
border-start-start-radius: var(--border-radius-small, 4px);
|
|
397
|
+
color: var(--color-main-text);
|
|
398
|
+
display: flex;
|
|
399
|
+
flex: 1;
|
|
400
|
+
flex-direction: column;
|
|
401
|
+
gap: var(--default-grid-baseline);
|
|
402
|
+
min-width: 0;
|
|
403
|
+
padding: calc(3 * var(--default-grid-baseline)) calc(3.5 * var(--default-grid-baseline));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.cn-conversation-thread__message--us .cn-conversation-thread__bubble {
|
|
407
|
+
background: var(--color-primary-element-light);
|
|
408
|
+
border-start-end-radius: var(--border-radius-small, 4px);
|
|
409
|
+
border-start-start-radius: var(--border-radius-large);
|
|
410
|
+
color: var(--color-primary-element-light-text);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.cn-conversation-thread__meta {
|
|
414
|
+
display: flex;
|
|
415
|
+
flex-wrap: wrap;
|
|
416
|
+
font-size: 0.9em;
|
|
417
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.cn-conversation-thread__author {
|
|
421
|
+
font-weight: 600;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.cn-conversation-thread__text {
|
|
425
|
+
overflow-wrap: anywhere;
|
|
426
|
+
white-space: pre-wrap;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.cn-conversation-thread__reply {
|
|
430
|
+
display: flex;
|
|
431
|
+
flex-direction: column;
|
|
432
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.cn-conversation-thread__reply-label {
|
|
436
|
+
font-weight: 600;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.cn-conversation-thread__input {
|
|
440
|
+
box-sizing: border-box;
|
|
441
|
+
margin: 0;
|
|
442
|
+
resize: vertical;
|
|
443
|
+
width: 100%;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.cn-conversation-thread__error {
|
|
447
|
+
color: var(--color-error-text, var(--color-error));
|
|
448
|
+
margin: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.cn-conversation-thread__reply-actions {
|
|
452
|
+
display: flex;
|
|
453
|
+
flex-wrap: wrap;
|
|
454
|
+
gap: calc(2.5 * var(--default-grid-baseline));
|
|
455
|
+
justify-content: flex-end;
|
|
456
|
+
}
|
|
457
|
+
</style>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div class="cn-case-widget-card">
|
|
8
|
+
<CnConversationThread
|
|
9
|
+
:messages="messages"
|
|
10
|
+
:title="content.title || ''"
|
|
11
|
+
:allowReply="content.allowReply !== false"
|
|
12
|
+
:replyLabel="content.replyLabel || undefined"
|
|
13
|
+
:placeholder="content.placeholder || ''"
|
|
14
|
+
:sendLabel="content.sendLabel || undefined"
|
|
15
|
+
:endpoint="endpoint"
|
|
16
|
+
:emptyText="content.emptyText || undefined"
|
|
17
|
+
@sent="onSent" />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script>
|
|
22
|
+
import CnConversationThread from './CnConversationThread.vue'
|
|
23
|
+
import { readPath } from '../../utils/readPath.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CnConversationWidget: the `conversation` detail widget type.
|
|
27
|
+
*
|
|
28
|
+
* Renders CnConversationThread from a list on the bound record:
|
|
29
|
+
* `content: { field, endpoint?, authorField?, timeField?, textField?, sideField?, usValue? }`.
|
|
30
|
+
* `@objectId` in `endpoint` is replaced by the record's id. A reply the
|
|
31
|
+
* endpoint accepts is shown at once, until the record is read again.
|
|
32
|
+
* Resolved by its registry key, not exported.
|
|
33
|
+
*/
|
|
34
|
+
export default {
|
|
35
|
+
name: 'CnConversationWidget',
|
|
36
|
+
|
|
37
|
+
components: {
|
|
38
|
+
CnConversationThread,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
props: {
|
|
42
|
+
/** The widget's content. `field` is the dot-path to the messages on the record. */
|
|
43
|
+
content: {
|
|
44
|
+
type: Object,
|
|
45
|
+
default: () => ({}),
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/** The bound record. */
|
|
49
|
+
objectData: {
|
|
50
|
+
type: Object,
|
|
51
|
+
default: null,
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/** The bound record's id. */
|
|
55
|
+
objectId: {
|
|
56
|
+
type: [String, Number],
|
|
57
|
+
default: '',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
data() {
|
|
62
|
+
return {
|
|
63
|
+
/** Replies sent from this widget that the record does not hold yet. */
|
|
64
|
+
sentHere: [],
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
computed: {
|
|
69
|
+
/**
|
|
70
|
+
* The record's messages in the thread's shape, plus the replies sent here.
|
|
71
|
+
*
|
|
72
|
+
* @return {Array<object>} The messages.
|
|
73
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
74
|
+
*/
|
|
75
|
+
messages() {
|
|
76
|
+
const c = this.content
|
|
77
|
+
const value = readPath(this.objectData || {}, c.field || 'messages')
|
|
78
|
+
const usValue = String(c.usValue === undefined ? 'us' : c.usValue)
|
|
79
|
+
const stored = (Array.isArray(value) ? value : [])
|
|
80
|
+
.filter((message) => message && typeof message === 'object')
|
|
81
|
+
.map((message) => ({
|
|
82
|
+
id: message.id,
|
|
83
|
+
author: readPath(message, c.authorField || 'author'),
|
|
84
|
+
time: readPath(message, c.timeField || 'time'),
|
|
85
|
+
text: readPath(message, c.textField || 'text'),
|
|
86
|
+
side: String(readPath(message, c.sideField || 'side')) === usValue ? 'us' : 'them',
|
|
87
|
+
}))
|
|
88
|
+
return [...stored, ...this.sentHere]
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The endpoint with the record's id filled in.
|
|
93
|
+
*
|
|
94
|
+
* @return {string} The URL, or '' when the content names none.
|
|
95
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
96
|
+
*/
|
|
97
|
+
endpoint() {
|
|
98
|
+
const url = this.content.endpoint
|
|
99
|
+
if (typeof url !== 'string' || url === '') {
|
|
100
|
+
return ''
|
|
101
|
+
}
|
|
102
|
+
return url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
watch: {
|
|
107
|
+
// The record was read again, so it now holds what was sent from here.
|
|
108
|
+
objectData() {
|
|
109
|
+
this.sentHere = []
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
methods: {
|
|
114
|
+
/**
|
|
115
|
+
* Show an accepted reply at once, without waiting for the record.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} data The endpoint's response.
|
|
118
|
+
* @return {void}
|
|
119
|
+
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread
|
|
120
|
+
*/
|
|
121
|
+
onSent(data) {
|
|
122
|
+
const text = data?.message ?? data?.text
|
|
123
|
+
if (typeof text !== 'string' || text === '') {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
this.sentHere.push({
|
|
127
|
+
id: `sent-${this.sentHere.length}`,
|
|
128
|
+
time: new Date().toISOString(),
|
|
129
|
+
text,
|
|
130
|
+
side: 'us',
|
|
131
|
+
})
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<style scoped>
|
|
138
|
+
.cn-case-widget-card {
|
|
139
|
+
background: var(--color-main-background);
|
|
140
|
+
border: 1px solid var(--color-border);
|
|
141
|
+
border-radius: var(--border-radius-large);
|
|
142
|
+
padding: calc(5 * var(--default-grid-baseline));
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
@@ -405,6 +405,22 @@ The page header's overflow menu carries Refresh plus the mandatory trio Request
|
|
|
405
405
|
| `showReportBug` | Boolean | `true` | Whether the Report-a-bug entry renders in the page-header menu. |
|
|
406
406
|
| `showDocumentation` | Boolean | `true` | Whether the Documentation entry renders. Its target is resolved by the shared menu from the app-wide documentation base plus this page's id, so leaving it on costs the host nothing. |
|
|
407
407
|
|
|
408
|
+
## The action model
|
|
409
|
+
|
|
410
|
+
Four levels of action and the case surfaces around them. All opt in. See [the reference page](../../../docs/components/cn-detail-page.md#the-action-model) for manifest examples.
|
|
411
|
+
|
|
412
|
+
| Prop | Type | Default | Description |
|
|
413
|
+
|---|---|---|---|
|
|
414
|
+
| `primaryActionByStage` | Object \| null | `null` | Map from a stage value to the primary action: an action object or the id of a header action. Falls back to `primaryAction`. |
|
|
415
|
+
| `stageField` | String | `'status'` | Dot-path to the record's stage. |
|
|
416
|
+
| `quickActions` | Array | `[]` | At most three always visible header buttons. |
|
|
417
|
+
| `actionsMenu` | Object \| null | `null` | `{ showRefresh?, showHelpLinks?, label? }` for the overflow menu. |
|
|
418
|
+
| `nextStep` | Object \| null | `null` | The "what now" card per stage, rendered by `CnNextStepCard`. |
|
|
419
|
+
| `typePill` | Object \| null | `null` | `{ field, colorMap?, labels?, variant? }`, a pill above the title. |
|
|
420
|
+
| `statusPill` | Object \| null | `null` | A second pill, same shape. |
|
|
421
|
+
| `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
|
|
422
|
+
| `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
|
|
423
|
+
|
|
408
424
|
## The record as a place
|
|
409
425
|
|
|
410
426
|
`listNavigation`, `primaryAction` and `tabInAddress` make a record somewhere a handler stays. All three are off by default. See [the reference page](../../../docs/components/cn-detail-page.md#the-record-as-a-place).
|