@conduction/nextcloud-vue 2.101.0 → 2.103.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.
Files changed (140) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  11. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
  12. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  13. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
  14. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
  19. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +49 -6
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
  29. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +84 -4
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +8 -6
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/useNamedSource.js +26 -2
  35. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  36. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  37. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  38. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  45. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  46. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  47. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  48. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  54. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
  55. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
  56. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
  57. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
  58. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
  59. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
  60. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  61. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
  62. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  65. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  66. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
  67. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  70. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  71. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  72. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  74. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  78. package/dist/esm/composables/indexSources.js +9 -0
  79. package/dist/esm/composables/indexSources.js.map +1 -1
  80. package/dist/esm/index.js +1 -0
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/l10n/en.json.js +5 -1
  83. package/dist/esm/l10n/en.json.js.map +1 -1
  84. package/dist/esm/l10n/nl.json.js +5 -1
  85. package/dist/esm/l10n/nl.json.js.map +1 -1
  86. package/dist/esm/library-style.js +1 -1
  87. package/dist/esm/mixins/pageViews.js +8 -1
  88. package/dist/esm/mixins/pageViews.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +593 -459
  90. package/dist/esm/utils/builtInFormatters.js +24 -4
  91. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  92. package/dist/esm/utils/rowClassRules.js +43 -0
  93. package/dist/esm/utils/rowClassRules.js.map +1 -0
  94. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  95. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  96. package/dist/esm/utils/visibleWhen.js +32 -4
  97. package/dist/esm/utils/visibleWhen.js.map +1 -1
  98. package/dist/nextcloud-vue.cjs.js +10093 -8858
  99. package/dist/nextcloud-vue.cjs.js.map +2 -2
  100. package/dist/nextcloud-vue.css +593 -459
  101. package/l10n/en.json +5 -1
  102. package/l10n/nl.json +5 -1
  103. package/package.json +1 -1
  104. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  105. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  106. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  107. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  108. package/src/components/CnConversationThread/CnConversationThread.md +17 -0
  109. package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
  110. package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
  111. package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
  112. package/src/components/CnDashboardPage/CnDashboardPage.vue +50 -7
  113. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  114. package/src/components/CnFormDialog/CnFormDialog.md +1 -1
  115. package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
  116. package/src/components/CnIndexPage/CnIndexPage.md +15 -1
  117. package/src/components/CnIndexPage/CnIndexPage.vue +92 -9
  118. package/src/components/CnIndexPage/useNamedSource.js +26 -2
  119. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  120. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
  121. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  122. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  123. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
  124. package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
  125. package/src/components/CnSearchSelect/index.js +12 -0
  126. package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
  127. package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
  128. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  129. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  130. package/src/components/index.js +1 -0
  131. package/src/composables/indexSources.js +9 -0
  132. package/src/css/dialog.css +5 -3
  133. package/src/css/look-board-index.css +10 -0
  134. package/src/index.js +1 -0
  135. package/src/mixins/pageViews.js +8 -1
  136. package/src/schemas/app-manifest-v2.schema.json +107 -8
  137. package/src/utils/builtInFormatters.js +24 -4
  138. package/src/utils/rowClassRules.js +39 -0
  139. package/src/utils/validateManifestV2.compiled.js +3 -3
  140. package/src/utils/visibleWhen.js +32 -4
@@ -1,3 +1,5 @@
1
+ import { NcButton } from '@nextcloud/vue';
2
+ import __sfc_main$1 from '../CnIcon/CnIcon.vue.js';
1
3
  import __sfc_main from './CnConversationThread.vue.js';
2
4
  import { dispatchAction } from '../../utils/actionsDispatcher.js';
3
5
  import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js';
@@ -22,12 +24,20 @@ import { resolveDeepTokens, hasUnresolvedDeepTokens } from '../../utils/resolveF
22
24
  * `replyAction`, an action descriptor the app decides the meaning of: it is
23
25
  * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send
24
26
  * the source is read again.
27
+ *
28
+ * `channelChips: true` draws a chip per channel above the thread (every
29
+ * `channelMap` label, in map order, then any other channel a message uses).
30
+ * `headerAction` is a button beside the title (`{ label, icon?, ...action }`,
31
+ * DqZaakContact's "Contact vastleggen"), dispatched like `replyAction` with
32
+ * `args: [{ objectId }]`; after it the source is read again.
25
33
  */
26
34
  var script = {
27
35
  name: 'CnConversationWidget',
28
36
 
29
37
  components: {
30
38
  CnConversationThread: __sfc_main,
39
+ CnIcon: __sfc_main$1,
40
+ NcButton,
31
41
  },
32
42
 
33
43
  inject: {
@@ -79,6 +89,18 @@ var script = {
79
89
  return source && typeof source === 'object' && source.register && source.schema ? source : null
80
90
  },
81
91
 
92
+ /** @return {object|null} The header action, or null without a label. */
93
+ headerAction() {
94
+ const action = this.content?.headerAction;
95
+ return action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '' ? action : null
96
+ },
97
+
98
+ /** @return {Array<string>} The `channelMap` labels, translated, each once. */
99
+ channelLabels() {
100
+ const map = this.content?.channelMap && typeof this.content.channelMap === 'object' ? this.content.channelMap : {};
101
+ return [...new Set(Object.values(map).filter((label) => typeof label === 'string' && label !== '').map((label) => this.tr(label)))]
102
+ },
103
+
82
104
  /** @return {object|null} The reply action, or null. */
83
105
  replyAction() {
84
106
  const action = this.content?.replyAction;
@@ -231,6 +253,30 @@ var script = {
231
253
  this.onSent({ text });
232
254
  },
233
255
 
256
+ /**
257
+ * Run `headerAction` with the record id, then read the source again.
258
+ *
259
+ * @return {Promise<void>}
260
+ */
261
+ async onHeaderAction() {
262
+ if (!this.headerAction) {
263
+ return
264
+ }
265
+ const { label, icon, ...rest } = resolveDeepTokens(this.headerAction, this.tokenCtx);
266
+ const action = { ...rest, args: [{ objectId: this.objectId }] };
267
+ try {
268
+ const outcome = await (typeof this.cnDispatchAction === 'function'
269
+ ? this.cnDispatchAction(action)
270
+ : dispatchAction(action, { router: this.$router || null }));
271
+ if (outcome === false) {
272
+ return
273
+ }
274
+ } catch {
275
+ return
276
+ }
277
+ await this.loadSource();
278
+ },
279
+
234
280
  /**
235
281
  * Show an accepted reply at once, without waiting for the record.
236
282
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CnConversationWidget.vue2.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-case-widget-card\">\n\t\t<CnConversationThread\n\t\t\t:messages=\"messages\"\n\t\t\t:title=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\" />\n\t</div>\n</template>\n\n<script>\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Replies sent from this widget that the record does not hold yet. */\n\t\t\tsentHere: [],\n\t\t\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * sent here.\n\t\t *\n\t\t * @return {Array<object>} The messages.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\t\t\t})\n\t\t\treturn [...stored, ...this.sentHere]\n\t\t},\n\n\t\t/**\n\t\t * The endpoint with the record's id filled in.\n\t\t *\n\t\t * @return {string} The URL, or '' when the content names none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tendpoint() {\n\t\t\tconst url = this.content.endpoint\n\t\t\tif (typeof url !== 'string' || url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))\n\t\t},\n\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\n\t\t},\n\t},\n\n\twatch: {\n\t\t// The record was read again, so it now holds what was sent from here.\n\t\tobjectData() {\n\t\t\tthis.sentHere = []\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\n\t\t/**\n\t\t * Show an accepted reply at once, without waiting for the record.\n\t\t *\n\t\t * @param {object} data The endpoint's response.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tonSent(data) {\n\t\t\tconst text = data?.message ?? data?.text\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sentHere.push({\n\t\t\t\tid: `sent-${this.sentHere.length}`,\n\t\t\t\ttime: new Date().toISOString(),\n\t\t\t\ttext,\n\t\t\t\tside: 'us',\n\t\t\t})\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-case-widget-card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n</style>\n"],"names":["CnConversationThread"],"mappings":"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"}
1
+ {"version":3,"file":"CnConversationWidget.vue2.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-case-widget-card\">\n\t\t<CnConversationThread\n\t\t\t:messages=\"messages\"\n\t\t\t:title=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t:channelFilter=\"content.channelChips === true\"\n\t\t\t:channels=\"channelLabels\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\">\n\t\t\t<template v-if=\"headerAction\" #header-actions>\n\t\t\t\t<NcButton\n\t\t\t\t\tdata-testid=\"cn-conversation-header-action\"\n\t\t\t\t\t@click=\"onHeaderAction\">\n\t\t\t\t\t<template v-if=\"headerAction.icon\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"headerAction.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ tr(headerAction.label) }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnConversationThread>\n\t</div>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n *\n * `channelChips: true` draws a chip per channel above the thread (every\n * `channelMap` label, in map order, then any other channel a message uses).\n * `headerAction` is a button beside the title (`{ label, icon?, ...action }`,\n * DqZaakContact's \"Contact vastleggen\"), dispatched like `replyAction` with\n * `args: [{ objectId }]`; after it the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Replies sent from this widget that the record does not hold yet. */\n\t\t\tsentHere: [],\n\t\t\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The header action, or null without a label. */\n\t\theaderAction() {\n\t\t\tconst action = this.content?.headerAction\n\t\t\treturn action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '' ? action : null\n\t\t},\n\n\t\t/** @return {Array<string>} The `channelMap` labels, translated, each once. */\n\t\tchannelLabels() {\n\t\t\tconst map = this.content?.channelMap && typeof this.content.channelMap === 'object' ? this.content.channelMap : {}\n\t\t\treturn [...new Set(Object.values(map).filter((label) => typeof label === 'string' && label !== '').map((label) => this.tr(label)))]\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * sent here.\n\t\t *\n\t\t * @return {Array<object>} The messages.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\t\t\t})\n\t\t\treturn [...stored, ...this.sentHere]\n\t\t},\n\n\t\t/**\n\t\t * The endpoint with the record's id filled in.\n\t\t *\n\t\t * @return {string} The URL, or '' when the content names none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tendpoint() {\n\t\t\tconst url = this.content.endpoint\n\t\t\tif (typeof url !== 'string' || url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))\n\t\t},\n\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\n\t\t},\n\t},\n\n\twatch: {\n\t\t// The record was read again, so it now holds what was sent from here.\n\t\tobjectData() {\n\t\t\tthis.sentHere = []\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\n\t\t/**\n\t\t * Run `headerAction` with the record id, then read the source again.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onHeaderAction() {\n\t\t\tif (!this.headerAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst { label, icon, ...rest } = resolveDeepTokens(this.headerAction, this.tokenCtx)\n\t\t\tconst action = { ...rest, args: [{ objectId: this.objectId }] }\n\t\t\ttry {\n\t\t\t\tconst outcome = await (typeof this.cnDispatchAction === 'function'\n\t\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t\t: dispatchAction(action, { router: this.$router || null }))\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tawait this.loadSource()\n\t\t},\n\n\t\t/**\n\t\t * Show an accepted reply at once, without waiting for the record.\n\t\t *\n\t\t * @param {object} data The endpoint's response.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tonSent(data) {\n\t\t\tconst text = data?.message ?? data?.text\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sentHere.push({\n\t\t\t\tid: `sent-${this.sentHere.length}`,\n\t\t\t\ttime: new Date().toISOString(),\n\t\t\t\ttext,\n\t\t\t\tside: 'us',\n\t\t\t})\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-case-widget-card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n</style>\n"],"names":["CnConversationThread","CnIcon"],"mappings":"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"}
@@ -1,8 +1,10 @@
1
- import { resolveComponent, openBlock, createElementBlock, createVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, createVNode, createSlots, withCtx, createTextVNode, toDisplayString } from 'vue';
2
2
 
3
3
  const _hoisted_1 = { class: "cn-case-widget-card" };
4
4
 
5
5
  function render(_ctx, _cache, $props, $setup, $data, $options) {
6
+ const _component_CnIcon = resolveComponent("CnIcon");
7
+ const _component_NcButton = resolveComponent("NcButton");
6
8
  const _component_CnConversationThread = resolveComponent("CnConversationThread");
7
9
 
8
10
  return (openBlock(), createElementBlock("div", _hoisted_1, [
@@ -16,9 +18,42 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
16
18
  endpoint: $options.replyAction ? '' : $options.endpoint,
17
19
  loading: $data.loading,
18
20
  emptyText: $props.content.emptyText ? $options.tr($props.content.emptyText) : undefined,
21
+ channelFilter: $props.content.channelChips === true,
22
+ channels: $options.channelLabels,
19
23
  onSend: $options.onSend,
20
24
  onSent: $options.onSent
21
- }, null, 8 /* PROPS */, ["messages", "title", "allowReply", "replyLabel", "placeholder", "sendLabel", "endpoint", "loading", "emptyText", "onSend", "onSent"])
25
+ }, createSlots({ _: 2 /* DYNAMIC */ }, [
26
+ ($options.headerAction)
27
+ ? {
28
+ name: "header-actions",
29
+ fn: withCtx(() => [
30
+ createVNode(_component_NcButton, {
31
+ "data-testid": "cn-conversation-header-action",
32
+ onClick: $options.onHeaderAction
33
+ }, createSlots({
34
+ default: withCtx(() => [
35
+ createTextVNode(" " + toDisplayString($options.tr($options.headerAction.label)), 1 /* TEXT */)
36
+ ]),
37
+ _: 2 /* DYNAMIC */
38
+ }, [
39
+ ($options.headerAction.icon)
40
+ ? {
41
+ name: "icon",
42
+ fn: withCtx(() => [
43
+ createVNode(_component_CnIcon, {
44
+ name: $options.headerAction.icon,
45
+ size: 20
46
+ }, null, 8 /* PROPS */, ["name"])
47
+ ]),
48
+ key: "0"
49
+ }
50
+ : undefined
51
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["onClick"])
52
+ ]),
53
+ key: "0"
54
+ }
55
+ : undefined
56
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["messages", "title", "allowReply", "replyLabel", "placeholder", "sendLabel", "endpoint", "loading", "emptyText", "channelFilter", "channels", "onSend", "onSent"])
22
57
  ]))
23
58
  }
24
59
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnConversationWidget.vue3.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue?vue&type=template&id=257593ce&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-case-widget-card\">\n\t\t<CnConversationThread\n\t\t\t:messages=\"messages\"\n\t\t\t:title=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\" />\n\t</div>\n</template>\n\n<script>\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Replies sent from this widget that the record does not hold yet. */\n\t\t\tsentHere: [],\n\t\t\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * sent here.\n\t\t *\n\t\t * @return {Array<object>} The messages.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\t\t\t})\n\t\t\treturn [...stored, ...this.sentHere]\n\t\t},\n\n\t\t/**\n\t\t * The endpoint with the record's id filled in.\n\t\t *\n\t\t * @return {string} The URL, or '' when the content names none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tendpoint() {\n\t\t\tconst url = this.content.endpoint\n\t\t\tif (typeof url !== 'string' || url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))\n\t\t},\n\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\n\t\t},\n\t},\n\n\twatch: {\n\t\t// The record was read again, so it now holds what was sent from here.\n\t\tobjectData() {\n\t\t\tthis.sentHere = []\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\n\t\t/**\n\t\t * Show an accepted reply at once, without waiting for the record.\n\t\t *\n\t\t * @param {object} data The endpoint's response.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tonSent(data) {\n\t\t\tconst text = data?.message ?? data?.text\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sentHere.push({\n\t\t\t\tid: `sent-${this.sentHere.length}`,\n\t\t\t\ttime: new Date().toISOString(),\n\t\t\t\ttext,\n\t\t\t\tside: 'us',\n\t\t\t})\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-case-widget-card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode"],"mappings":";;AAWM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;AAA9B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;IACAC;AACC,MAAA,QAAQ,EAAE,QAAQ,CAAA,QAAA;AAClB,MAAA,KAAK,EAAE,QAAA,CAAA,EAAE,CAAC,MAAA,CAAA,OAAO,CAAC,KAAI,IAAA,EAAA,CAAA;MACtB,UAAU,EAAE,MAAI,CAAA,OAAA,CAAA,UAAA,KAAA,KAAA;MAChB,UAAU,EAAE,MAAO,CAAA,OAAA,CAAC,UAAS,GAAI,QAAE,CAAA,EAAA,CAAC,MAAO,CAAA,OAAA,CAAC,UAAS,CAAA,GAAK,SAAM;AAChE,MAAA,WAAW,EAAC,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,WAAA,IAAA,EAAA,CAAA;MACZ,SAAS,EAAE,MAAI,CAAA,OAAA,CAAA,SAAA,GAAA,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,SAAA,CAAA,GAAA,SAAA;MACf,QAAE,EAAA,QAAA,CAAA,WAAA,GAAA,EAAA,GAAA,QAAA,CAAA,QAAA;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnConversationWidget.vue3.js","sources":["../../../../src/components/CnConversationThread/CnConversationWidget.vue?vue&type=template&id=0533e246&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-case-widget-card\">\n\t\t<CnConversationThread\n\t\t\t:messages=\"messages\"\n\t\t\t:title=\"tr(content.title || '')\"\n\t\t\t:allowReply=\"content.allowReply !== false\"\n\t\t\t:replyLabel=\"content.replyLabel ? tr(content.replyLabel) : undefined\"\n\t\t\t:placeholder=\"tr(content.placeholder || '')\"\n\t\t\t:sendLabel=\"content.sendLabel ? tr(content.sendLabel) : undefined\"\n\t\t\t:endpoint=\"replyAction ? '' : endpoint\"\n\t\t\t:loading=\"loading\"\n\t\t\t:emptyText=\"content.emptyText ? tr(content.emptyText) : undefined\"\n\t\t\t:channelFilter=\"content.channelChips === true\"\n\t\t\t:channels=\"channelLabels\"\n\t\t\t@send=\"onSend\"\n\t\t\t@sent=\"onSent\">\n\t\t\t<template v-if=\"headerAction\" #header-actions>\n\t\t\t\t<NcButton\n\t\t\t\t\tdata-testid=\"cn-conversation-header-action\"\n\t\t\t\t\t@click=\"onHeaderAction\">\n\t\t\t\t\t<template v-if=\"headerAction.icon\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"headerAction.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ tr(headerAction.label) }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnConversationThread>\n\t</div>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnConversationThread from './CnConversationThread.vue'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnConversationWidget: the `conversation` detail widget type.\n *\n * Renders CnConversationThread from the messages of a record, oldest first.\n * The messages are a list on the bound record (`field`), or the objects of a\n * register list (`source: { register, schema, filter?, limit? }`, the filter\n * token-resolved like a record-list's: `{ \"case\": \"@objectId\" }`).\n *\n * Mapping keys name where each message keeps its parts: `textField` (alias\n * `bodyField`), `timeField` (alias `timestampField`), `authorField` (alias\n * `senderField`), `sideField` (alias `directionField`) with `usValue` (alias\n * `outValue`) for the messages the organisation sent, and `channelField` with\n * an optional `channelMap` from stored value to label (\"email\" to \"E-mail\").\n *\n * The reply box posts to `endpoint` (`@objectId` filled in), or runs\n * `replyAction`, an action descriptor the app decides the meaning of: it is\n * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send\n * the source is read again.\n *\n * `channelChips: true` draws a chip per channel above the thread (every\n * `channelMap` label, in map order, then any other channel a message uses).\n * `headerAction` is a button beside the title (`{ label, icon?, ...action }`,\n * DqZaakContact's \"Contact vastleggen\"), dispatched like `replyAction` with\n * `args: [{ objectId }]`; after it the source is read again.\n */\nexport default {\n\tname: 'CnConversationWidget',\n\n\tcomponents: {\n\t\tCnConversationThread,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** Pre-bound action dispatcher of the page, for `replyAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's content: `field` (the dot-path to the messages on the\n\t\t * record) or `source`, the mapping keys, `endpoint` or `replyAction`,\n\t\t * and the texts (`title`, `replyLabel`, `placeholder`, `sendLabel`,\n\t\t * `emptyText`, translated).\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The bound record. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Replies sent from this widget that the record does not hold yet. */\n\t\t\tsentHere: [],\n\t\t\t/** The messages read from `source`. */\n\t\t\tlisted: [],\n\t\t\t/** Whether `source` is being read. */\n\t\t\tloading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object|null} The register list the messages come from, or null. */\n\t\tsource() {\n\t\t\tconst source = this.content?.source\n\t\t\treturn source && typeof source === 'object' && source.register && source.schema ? source : null\n\t\t},\n\n\t\t/** @return {object|null} The header action, or null without a label. */\n\t\theaderAction() {\n\t\t\tconst action = this.content?.headerAction\n\t\t\treturn action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '' ? action : null\n\t\t},\n\n\t\t/** @return {Array<string>} The `channelMap` labels, translated, each once. */\n\t\tchannelLabels() {\n\t\t\tconst map = this.content?.channelMap && typeof this.content.channelMap === 'object' ? this.content.channelMap : {}\n\t\t\treturn [...new Set(Object.values(map).filter((label) => typeof label === 'string' && label !== '').map((label) => this.tr(label)))]\n\t\t},\n\n\t\t/** @return {object|null} The reply action, or null. */\n\t\treplyAction() {\n\t\t\tconst action = this.content?.replyAction\n\t\t\treturn action && typeof action === 'object' ? action : null\n\t\t},\n\n\t\t/**\n\t\t * The messages in the thread's shape, oldest first, plus the replies\n\t\t * sent here.\n\t\t *\n\t\t * @return {Array<object>} The messages.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tmessages() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst raw = this.source ? this.listed : readPath(this.objectData || {}, c.field || 'messages')\n\t\t\tconst usValue = String(c.outValue ?? c.usValue ?? 'us')\n\t\t\tconst channelMap = c.channelMap && typeof c.channelMap === 'object' ? c.channelMap : {}\n\t\t\tconst stored = (Array.isArray(raw) ? raw : [])\n\t\t\t\t.filter((message) => message && typeof message === 'object')\n\t\t\t\t.map((message, index) => {\n\t\t\t\t\tconst channel = c.channelField ? readPath(message, c.channelField) : undefined\n\t\t\t\t\tconst channelKey = channel === undefined || channel === null ? '' : String(channel)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: message.id ?? `message-${index}`,\n\t\t\t\t\t\tauthor: readPath(message, c.senderField || c.authorField || 'author'),\n\t\t\t\t\t\ttime: readPath(message, c.timestampField || c.timeField || 'time'),\n\t\t\t\t\t\ttext: readPath(message, c.bodyField || c.textField || 'text'),\n\t\t\t\t\t\tside: String(readPath(message, c.directionField || c.sideField || 'side')) === usValue ? 'us' : 'them',\n\t\t\t\t\t\tchannel: channelKey === '' ? '' : this.tr(Object.hasOwn(channelMap, channelKey) ? channelMap[channelKey] : channelKey),\n\t\t\t\t\t\torder: index,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\tconst at = (message) => {\n\t\t\t\tconst ms = Date.parse(message.time)\n\t\t\t\treturn Number.isNaN(ms) ? null : ms\n\t\t\t}\n\t\t\tstored.sort((a, b) => {\n\t\t\t\tconst ta = at(a)\n\t\t\t\tconst tb = at(b)\n\t\t\t\treturn (ta === null || tb === null || ta === tb) ? a.order - b.order : ta - tb\n\t\t\t})\n\t\t\treturn [...stored, ...this.sentHere]\n\t\t},\n\n\t\t/**\n\t\t * The endpoint with the record's id filled in.\n\t\t *\n\t\t * @return {string} The URL, or '' when the content names none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tendpoint() {\n\t\t\tconst url = this.content.endpoint\n\t\t\tif (typeof url !== 'string' || url === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn url.replace(/@objectId/g, encodeURIComponent(String(this.objectId ?? '')))\n\t\t},\n\n\t\t/** @return {object} The token context for `source.filter` and `replyAction`. */\n\t\ttokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.objectData || {} }\n\t\t},\n\t},\n\n\twatch: {\n\t\t// The record was read again, so it now holds what was sent from here.\n\t\tobjectData() {\n\t\t\tthis.sentHere = []\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadSource()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * Read the messages from `source`. A filter whose token does not\n\t\t * resolve sends no request; a failed read leaves the thread empty.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-conversation-reads-its-messages-from-a-list-and-maps-their-parts\n\t\t */\n\t\tasync loadSource() {\n\t\t\tif (!this.source) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst raw = this.source.filter || {}\n\t\t\tconst filter = resolveDeepTokens(raw, this.tokenCtx)\n\t\t\tconst emptied = Object.keys(raw).some((key) => typeof raw[key] === 'string' && raw[key].startsWith('@') && (filter[key] === '' || filter[key] === null || filter[key] === undefined))\n\t\t\tif (emptied || hasUnresolvedDeepTokens(filter)) {\n\t\t\t\tthis.listed = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.listed = await fetchReferenceObjects({ register: this.source.register, schema: this.source.schema, filter, limit: this.source.limit || 200 })\n\t\t\t\tthis.sentHere = []\n\t\t\t} catch {\n\t\t\t\tthis.listed = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run `replyAction` with the reply, when there is one; the endpoint\n\t\t * path is the thread's own.\n\t\t *\n\t\t * @param {string} text The reply.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/conversation-source/specs/conversation-source/spec.md#requirement-a-reply-can-run-an-app-action\n\t\t */\n\t\tasync onSend(text) {\n\t\t\tif (!this.replyAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(this.replyAction, this.tokenCtx)\n\t\t\tconst action = { ...resolved, args: [{ text, objectId: this.objectId }], props: { ...(resolved.props || {}), text } }\n\t\t\tconst result = typeof this.cnDispatchAction === 'function'\n\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t: dispatchAction(action, { router: this.$router || null })\n\t\t\ttry {\n\t\t\t\tconst outcome = await result\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.source) {\n\t\t\t\tawait this.loadSource()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.onSent({ text })\n\t\t},\n\n\t\t/**\n\t\t * Run `headerAction` with the record id, then read the source again.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onHeaderAction() {\n\t\t\tif (!this.headerAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst { label, icon, ...rest } = resolveDeepTokens(this.headerAction, this.tokenCtx)\n\t\t\tconst action = { ...rest, args: [{ objectId: this.objectId }] }\n\t\t\ttry {\n\t\t\t\tconst outcome = await (typeof this.cnDispatchAction === 'function'\n\t\t\t\t\t? this.cnDispatchAction(action)\n\t\t\t\t\t: dispatchAction(action, { router: this.$router || null }))\n\t\t\t\tif (outcome === false) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tawait this.loadSource()\n\t\t},\n\n\t\t/**\n\t\t * Show an accepted reply at once, without waiting for the record.\n\t\t *\n\t\t * @param {object} data The endpoint's response.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-conversation-thread\n\t\t */\n\t\tonSent(data) {\n\t\t\tconst text = data?.message ?? data?.text\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sentHere.push({\n\t\t\t\tid: `sent-${this.sentHere.length}`,\n\t\t\t\ttime: new Date().toISOString(),\n\t\t\t\ttext,\n\t\t\t\tside: 'us',\n\t\t\t})\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-case-widget-card {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createTextVNode"],"mappings":";;AAWM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,qBAAqB,GAAA;;;;;;;AAA9B,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;IACAC;AACC,MAAA,QAAQ,EAAE,QAAQ,CAAA,QAAA;AAClB,MAAA,KAAK,EAAE,QAAA,CAAA,EAAE,CAAC,MAAA,CAAA,OAAO,CAAC,KAAI,IAAA,EAAA,CAAA;MACtB,UAAU,EAAE,MAAI,CAAA,OAAA,CAAA,UAAA,KAAA,KAAA;MAChB,UAAU,EAAE,MAAO,CAAA,OAAA,CAAC,UAAS,GAAI,QAAE,CAAA,EAAA,CAAC,MAAO,CAAA,OAAA,CAAC,UAAS,CAAA,GAAK,SAAM;AAChE,MAAA,WAAW,EAAE,QAAA,CAAA,EAAE,CAAC,MAAA,CAAA,OAAO,CAAC;MACxB,SAAS,EAAE,MAAO,CAAA,OAAA,CAAC,SAAI,GAAA,QAAA,CAAA,EAAA,CAAA,MAAA,CAAA,OAAA,CAAA,SAAA,CAAA,GAAA,SAAA;MACvB,QAAQ,EAAE,QAAE,CAAA,WAAA,GAAA,EAAA,GAAA,QAAA,CAAA,QAAA;AACZ,MAAA,OAAO,EAAE,KAAI,CAAA,OAAA;MACb,SAAS,EAAE,MAAO,CAAA,OAAA,CAAC,SAAU,GAAE,QAAE,CAAA,EAAA,CAAC,MAAO,CAAA,OAAA,CAAC,SAAE,CAAA,GAAA,SAAA;MAC5C,aAAQ,EAAA,MAAA,CAAA,OAAA,CAAA,YAAA,KAAA,IAAA;AACP,MAAA,QAAO,EAAE,QAAa,CAAA,aAAA;YACnB,EAAE,QAAM,CAAA,MAAA;AACX,MAAA,MAAG,EAAE,QAAM,CAAA,MAAA;;OACG,QAAY,CAAA,YAAA;;kBAAG,gBAAc;;;AAE3C,gBAAA,aAAW,EAAC,+BAAe;AAC1B,gBAAA,OAAK,EAAE,QAAC,CAAA,cAAA;;iCAGR;AAAA,kBAAAC,eAAA,CAAA;;;;AAFO,gBAAA,CAAA,QAAA,CAAA,YAAA,CAAA,IAAA;;4BAAA,MAAA;kCACP,MAAkB;wBAAlBD,WAAkB,CAAA,iBAAA,EAAA;0BAAT,IAAI,EAAE,QAAG,CAAA,YAAA,CAAA,IAAA;AAAA,0BAAA,IAAA,EAAA,EAAA;;;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDashboardPage.vue2.js';
2
2
  import { render } from './CnDashboardPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-54c2437f";
5
+ script.__scopeId = "data-v-5b9ebdc3";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -92,6 +92,7 @@ const CHART_PROP_KEYS = [
92
92
  'footnote',
93
93
  'highlight',
94
94
  'cellSource',
95
+ 'emptyCellLabel',
95
96
  ];
96
97
 
97
98
  /**
@@ -825,6 +826,9 @@ var script = {
825
826
  * and renders exactly as it did before this existed, which is the
826
827
  * property that makes shipping it safe on every dashboard at once.
827
828
  *
829
+ * Deprecated: layout belongs to Buildiq. Kept working for one release.
830
+ *
831
+ * @deprecated
828
832
  * @type {boolean}
829
833
  */
830
834
  userLayout: {
@@ -1202,9 +1206,25 @@ var script = {
1202
1206
  * @return {boolean}
1203
1207
  */
1204
1208
  gridEditable() {
1209
+ return this.isEditing || this.editingBody
1210
+ },
1211
+
1212
+ /**
1213
+ * Whether Buildiq is editing the page (the injected `cnEditingBody`
1214
+ * ref, or a plain boolean). Buildiq is the layout editor: while it
1215
+ * edits, the grid shows and changes the manifest layout, also on a
1216
+ * page with `userLayout`.
1217
+ *
1218
+ * @return {boolean}
1219
+ */
1220
+ editingBody() {
1205
1221
  const e = this.cnEditingBody;
1206
- const buildiqEditing = Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e);
1207
- return this.isEditing || buildiqEditing
1222
+ return Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)
1223
+ },
1224
+
1225
+ /** @return {boolean} Whether the grid shows this user's arrangement. */
1226
+ showsUserLayout() {
1227
+ return this.userLayout && !this.editingBody && Array.isArray(this.userLayoutItems)
1208
1228
  },
1209
1229
 
1210
1230
  /**
@@ -1265,9 +1285,7 @@ var script = {
1265
1285
  * @return {Array<object>} The layout.
1266
1286
  */
1267
1287
  renderedLayout() {
1268
- const base = (this.userLayout && Array.isArray(this.userLayoutItems))
1269
- ? this.userLayoutItems
1270
- : this.layout;
1288
+ const base = this.showsUserLayout ? this.userLayoutItems : this.layout;
1271
1289
  // The KPI row is not part of the grid.
1272
1290
  if (Array.isArray(this.kpiRow) && this.kpiRow.length > 0 && Array.isArray(base)) {
1273
1291
  return base.filter((item) => !this.kpiRow.includes(item.widgetId))
@@ -1387,6 +1405,25 @@ var script = {
1387
1405
  return (this.pageFilters || []).filter((pf) => pf && pf.key && !inDescription.includes(pf))
1388
1406
  },
1389
1407
 
1408
+ /**
1409
+ * The edit button's label through the app's translate: a manifest's
1410
+ * `editLabel` ("Edit layout", DqDashboard) reads in the user's language.
1411
+ *
1412
+ * @return {string}
1413
+ */
1414
+ shownEditLabel() {
1415
+ return this.effectiveTranslate(this.editLabel)
1416
+ },
1417
+
1418
+ /**
1419
+ * The done button's label through the app's translate.
1420
+ *
1421
+ * @return {string}
1422
+ */
1423
+ shownDoneLabel() {
1424
+ return this.effectiveTranslate(this.doneLabel)
1425
+ },
1426
+
1390
1427
  /**
1391
1428
  * The preset list passed to the picker. Falls back to the
1392
1429
  * `DEFAULT_DATE_RANGE_PRESETS` constant when the consumer
@@ -2544,7 +2581,8 @@ var script = {
2544
2581
  // write below is for the admin editor, which edits the page for
2545
2582
  // everybody; doing it for a user rearranging their own dashboard
2546
2583
  // would rewrite the page for everyone who opens it after them.
2547
- if (this.userLayout && Array.isArray(this.userLayoutItems)) {
2584
+ // While Buildiq edits, the drag is the admin's and goes below.
2585
+ if (this.showsUserLayout) {
2548
2586
  this.userLayoutDirty = true;
2549
2587
  this.userLayoutItems = this.userLayoutItems.map((item) => {
2550
2588
  const u = (Array.isArray(updated) ? updated : []).find((candidate) => String(candidate.id) === String(item.id)
@@ -2553,6 +2591,11 @@ var script = {
2553
2591
  return (u ? { ...item, ...this.geometryFrom(u) } : item)
2554
2592
  });
2555
2593
  this.$emit('layout-change', this.userLayoutItems);
2594
+ // Outside an in-page edit session no Done will come to save
2595
+ // the arrangement, so it is saved now.
2596
+ if (!this.isEditing) {
2597
+ this.saveUserLayout();
2598
+ }
2556
2599
 
2557
2600
  return
2558
2601
  }