@conduction/nextcloud-vue 2.102.0 → 2.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
- package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +7 -7
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageViews.js +8 -1
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +586 -416
- package/dist/esm/utils/nextcloudDirectory.js +18 -1
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
- package/dist/esm/utils/sortOrder.js +61 -0
- package/dist/esm/utils/sortOrder.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +13 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12270 -10936
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +586 -416
- package/l10n/en.json +4 -0
- package/l10n/nl.json +4 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +1 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
- package/src/components/CnAppRoot/CnAppRoot.md +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
- package/src/components/CnConversationThread/CnConversationThread.md +17 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
- package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
- package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +49 -0
- package/src/components/CnDetailPage/CnDetailPage.md +2 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
- package/src/components/CnFormDialog/CnFormDialog.md +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
- package/src/components/CnIndexPage/CnIndexPage.md +14 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
- package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
- package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
- package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
- package/src/components/CnSearchSelect/index.js +12 -0
- package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +7 -7
- package/src/css/dialog.css +5 -3
- package/src/css/index-page.css +19 -0
- package/src/css/look-board-index.css +48 -0
- package/src/index.js +1 -0
- package/src/mixins/pageViews.js +8 -1
- package/src/schemas/app-manifest-v2.schema.json +75 -6
- package/src/utils/nextcloudDirectory.js +17 -0
- package/src/utils/sortOrder.js +58 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +13 -2
|
@@ -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":";;;;;;;;AA4CA;;AAEC;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;;;AAGD,aAAe;CACd,IAAI,EAAE,sBAAsB;;CAE5B,UAAU,EAAE;AACX,wBAAAA,UAAoB;AACpB,UAAAC,YAAM;AACN,EAAA,QAAQ;EACR;;AAED,CAAA,MAAM,EAAE;AACP,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,KAAK;;AAE5C,EAAA,gBAAgB,EAAE,EAAE,OAAO,EAAE,MAAM;EACnC;;AAED,CAAA,KAAK,EAAE;AACN;;;;;;AAMA,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;;AAGD,EAAA,QAAQ,EAAE;AACT,GAAA,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;GACtB,OAAO,EAAE,EAAE;GACX;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;;GAEN,QAAQ,EAAE,EAAE;AACZ;AACA,GAAA,MAAM,EAAE,EAAE;AACV;GACA,OAAO,EAAE,KAAK;AACf,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;AACA,EAAA,MAAM,GAAG;AACR,GAAA,MAAM,MAAK,GAAI,IAAI,CAAC,OAAO,EAAE,OAAK;AAClC,GAAA,OAAO,UAAU,OAAO,MAAK,KAAM,QAAO,IAAK,MAAM,CAAC,QAAS,IAAG,MAAM,CAAC,MAAO,GAAE,SAAS,IAAG;GAC9F;;;AAGD,EAAA,YAAY,GAAG;AACd,GAAA,MAAM,SAAS,IAAI,CAAC,OAAO,EAAE,aAAW;GACxC,OAAO,UAAU,OAAO,MAAO,KAAI,YAAY,OAAO,MAAM,CAAC,KAAM,KAAI,YAAY,MAAM,CAAC,KAAM,KAAI,KAAK,MAAO,GAAE,IAAG;GACrH;;AAED;AACA,EAAA,aAAa,GAAG;GACf,MAAM,GAAE,GAAI,IAAI,CAAC,OAAO,EAAE,UAAS,IAAK,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,QAAS,GAAE,IAAI,CAAC,OAAO,CAAC,UAAW,GAAE,GAAC;GACjH,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,KAAK,OAAO,KAAI,KAAM,YAAY,KAAM,KAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;GAClI;;;EAGD,WAAW,GAAG;AACb,GAAA,MAAM,SAAS,IAAI,CAAC,OAAO,EAAE,YAAU;GACvC,OAAO,UAAU,OAAO,MAAO,KAAI,WAAW,MAAO,GAAE,IAAG;GAC1D;;AAED;;AAEC;AACA;AACA;;AAEA;;EAED,QAAQ,GAAG;GACV,MAAM,IAAI,IAAI,CAAC,OAAQ,IAAG,GAAC;AAC3B,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,MAAO,GAAE,IAAI,CAAC,SAAS,QAAQ,CAAC,IAAI,CAAC,UAAW,IAAG,EAAE,EAAE,CAAC,CAAC,SAAS,UAAU,EAAA;GAC7F,MAAM,UAAU,MAAM,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,OAAM,IAAK,IAAI,EAAA;AACtD,GAAA,MAAM,UAAS,GAAI,CAAC,CAAC,UAAW,IAAG,OAAO,CAAC,CAAC,UAAS,KAAM,QAAS,GAAE,CAAC,CAAC,UAAS,GAAI,GAAC;AACtF,GAAA,MAAM,MAAK,GAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAA,GAAI,GAAI,GAAE,EAAE;KAC3C,MAAM,CAAC,CAAC,OAAO,KAAK,OAAM,IAAK,OAAO,OAAQ,KAAI,QAAQ,CAAA;KAC1D,GAAG,CAAC,CAAC,OAAO,EAAE,KAAK,KAAK;AACxB,KAAA,MAAM,OAAQ,GAAE,CAAC,CAAC,YAAW,GAAI,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,YAAY,CAAE,GAAE,UAAQ;AAC7E,KAAA,MAAM,UAAW,GAAE,YAAY,SAAU,IAAG,OAAQ,KAAI,IAAK,GAAE,EAAC,GAAI,MAAM,CAAC,OAAO,EAAA;AAClF,KAAA,OAAO;AACN,MAAA,EAAE,EAAE,OAAO,CAAC,EAAC,IAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;AACpC,MAAA,MAAM,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,WAAU,IAAK,CAAC,CAAC,WAAY,IAAG,QAAQ,CAAC;MACrE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,cAAe,IAAG,CAAC,CAAC,SAAQ,IAAK,MAAM,CAAC;MAClE,IAAI,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,SAAU,IAAG,CAAC,CAAC,SAAQ,IAAK,MAAM,CAAC;AAC7D,MAAA,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,cAAa,IAAK,CAAC,CAAC,SAAU,IAAG,MAAM,CAAC,CAAA,KAAM,OAAQ,GAAE,OAAO,MAAM;MACtG,OAAO,EAAE,UAAW,KAAI,EAAG,GAAE,EAAC,GAAI,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,UAAU,CAAA,GAAI,UAAU,CAAC,UAAU,IAAI,UAAU,CAAC;MACtH,KAAK,EAAE,KAAK;AACb,MAAA;KACA,EAAA;AACF,GAAA,MAAM,KAAK,CAAC,OAAO,KAAK;AACvB,IAAA,MAAM,EAAC,GAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAA;IAClC,OAAO,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA,GAAI,IAAK,GAAE,EAAC;AACnC,KAAA;AACA,GAAA,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK;IACrB,MAAM,KAAK,EAAE,CAAC,CAAC,EAAA;IACf,MAAM,KAAK,EAAE,CAAC,CAAC,EAAA;IACf,OAAO,CAAC,EAAG,KAAI,IAAK,IAAG,EAAG,KAAI,IAAG,IAAK,EAAG,KAAI,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,KAAI,GAAI,EAAG,GAAE,EAAC;IAC7E,EAAA;AACD,GAAA,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAA;GACnC;;AAED;AACC;AACA;;;;EAID,QAAQ,GAAG;GACV,MAAM,GAAE,GAAI,IAAI,CAAC,OAAO,CAAC,SAAO;AAChC,GAAA,IAAI,OAAO,GAAE,KAAM,QAAO,IAAK,GAAE,KAAM,EAAE,EAAE;AAC1C,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,kBAAkB,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAA;GAChF;;AAED;EACA,QAAQ,GAAG;GACV,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,UAAW,IAAG,EAAG,EAAA;GAChE;EACD;;AAED,CAAA,KAAK,EAAE;;EAEN,UAAU,GAAG;AACZ,GAAA,IAAI,CAAC,QAAO,GAAI,GAAC;GACjB;;AAED,EAAA,QAAQ,EAAE;AACT,GAAA,SAAS,EAAE,IAAI;AACf,GAAA,OAAO,GAAG;AACT,IAAA,IAAI,CAAC,UAAU,GAAC;IAChB;GACD;EACD;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;AACA;AACA;;AAED,EAAA,EAAE,CAAC,GAAG,EAAE;AACP,GAAA,OAAO,GAAI,IAAG,OAAO,IAAI,CAAC,WAAU,KAAM,aAAa,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA,IAAK,GAAE,IAAK,EAAE,CAAA;GACzF;;AAED;;;AAGC;;AAEA;;AAED,EAAA,MAAM,UAAU,GAAG;AAClB,GAAA,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;IACjB,MAAK;AACN,IAAA;AACA,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,MAAM,CAAC,MAAK,IAAK,GAAC;GACnC,MAAM,MAAK,GAAI,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAA;AACnD,GAAA,MAAM,UAAU,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,GAAG,CAAC,GAAG,CAAA,KAAM,YAAY,GAAG,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,CAAE,KAAI,MAAM,CAAC,GAAG,MAAM,MAAM,MAAM,CAAC,GAAG,CAAE,KAAI,QAAQ,MAAM,CAAC,GAAG,CAAE,KAAI,SAAS,CAAC,EAAA;AACpL,GAAA,IAAI,OAAM,IAAK,uBAAuB,CAAC,MAAM,CAAC,EAAE;IAC/C,IAAI,CAAC,MAAO,GAAE,GAAC;IACf,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,IAAI;AACH,IAAA,IAAI,CAAC,MAAK,GAAI,MAAM,qBAAqB,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,KAAI,IAAK,KAAK,EAAA;AACjJ,IAAA,IAAI,CAAC,QAAO,GAAI,GAAC;AAClB,IAAA,CAAE,MAAM;IACP,IAAI,CAAC,MAAO,GAAE,GAAC;AAChB,IAAA,SAAU;AACT,IAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;AACpB,IAAA;GACA;;AAED;;AAEC;AACA;;;;;AAKD,EAAA,MAAM,MAAM,CAAC,IAAI,EAAE;AAClB,GAAA,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE;IACtB,MAAK;AACN,IAAA;AACA,GAAA,MAAM,QAAO,GAAI,iBAAiB,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,EAAA;GAClE,MAAM,MAAK,GAAI,EAAE,GAAG,QAAQ,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,QAAQ,CAAC,SAAS,EAAE,CAAC,EAAE,IAAG,EAAI,GAAA;GACpH,MAAM,MAAO,GAAE,OAAO,IAAI,CAAC,qBAAqB,UAAS;MACtD,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAA;MAC5B,cAAc,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,OAAQ,IAAG,IAAG,EAAG,EAAA;AAC1D,GAAA,IAAI;IACH,MAAM,UAAU,MAAM,OAAK;AAC3B,IAAA,IAAI,OAAQ,KAAI,KAAK,EAAE;KACtB,MAAK;AACN,KAAA;AACD,IAAA,CAAE,MAAM;IACP,MAAK;AACN,IAAA;AACA,GAAA,IAAI,IAAI,CAAC,MAAM,EAAE;AAChB,IAAA,MAAM,IAAI,CAAC,UAAU,GAAC;IACtB,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAA;GACpB;;AAED;;AAEC;;;AAGD,EAAA,MAAM,cAAc,GAAG;GACtB,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE;IACvB,MAAK;AACN,IAAA;AACA,GAAA,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAG,KAAM,iBAAiB,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,QAAQ,EAAA;GACnF,MAAM,MAAO,GAAE,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAS,EAAC,CAAE,GAAA;AAC9D,GAAA,IAAI;IACH,MAAM,OAAM,GAAI,OAAO,OAAO,IAAI,CAAC,gBAAiB,KAAI,UAAS;OAC9D,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAA;OAC5B,cAAc,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,OAAM,IAAK,IAAK,EAAC,CAAC,EAAA;AAC3D,IAAA,IAAI,OAAQ,KAAI,KAAK,EAAE;KACtB,MAAK;AACN,KAAA;AACD,IAAA,CAAE,MAAM;IACP,MAAK;AACN,IAAA;AACA,GAAA,MAAM,IAAI,CAAC,UAAU,GAAC;GACtB;;AAED;;AAEC;AACA;;;;EAID,MAAM,CAAC,IAAI,EAAE;AACZ,GAAA,MAAM,IAAK,GAAE,IAAI,EAAE,OAAM,IAAK,IAAI,EAAE,KAAG;AACvC,GAAA,IAAI,OAAO,IAAG,KAAM,QAAO,IAAK,SAAS,EAAE,EAAE;IAC5C,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;AAClB,IAAA,EAAE,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;IAC9B,IAAI;AACJ,IAAA,IAAI,EAAE,IAAI;IACV,EAAA;GACD;EACD;AACF;;;;"}
|
|
@@ -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
|
-
},
|
|
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-
|
|
5
|
+
script.__scopeId = "data-v-5b9ebdc3";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -826,6 +826,9 @@ var script = {
|
|
|
826
826
|
* and renders exactly as it did before this existed, which is the
|
|
827
827
|
* property that makes shipping it safe on every dashboard at once.
|
|
828
828
|
*
|
|
829
|
+
* Deprecated: layout belongs to Buildiq. Kept working for one release.
|
|
830
|
+
*
|
|
831
|
+
* @deprecated
|
|
829
832
|
* @type {boolean}
|
|
830
833
|
*/
|
|
831
834
|
userLayout: {
|
|
@@ -1203,9 +1206,25 @@ var script = {
|
|
|
1203
1206
|
* @return {boolean}
|
|
1204
1207
|
*/
|
|
1205
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() {
|
|
1206
1221
|
const e = this.cnEditingBody;
|
|
1207
|
-
|
|
1208
|
-
|
|
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)
|
|
1209
1228
|
},
|
|
1210
1229
|
|
|
1211
1230
|
/**
|
|
@@ -1266,9 +1285,7 @@ var script = {
|
|
|
1266
1285
|
* @return {Array<object>} The layout.
|
|
1267
1286
|
*/
|
|
1268
1287
|
renderedLayout() {
|
|
1269
|
-
const base =
|
|
1270
|
-
? this.userLayoutItems
|
|
1271
|
-
: this.layout;
|
|
1288
|
+
const base = this.showsUserLayout ? this.userLayoutItems : this.layout;
|
|
1272
1289
|
// The KPI row is not part of the grid.
|
|
1273
1290
|
if (Array.isArray(this.kpiRow) && this.kpiRow.length > 0 && Array.isArray(base)) {
|
|
1274
1291
|
return base.filter((item) => !this.kpiRow.includes(item.widgetId))
|
|
@@ -2564,7 +2581,8 @@ var script = {
|
|
|
2564
2581
|
// write below is for the admin editor, which edits the page for
|
|
2565
2582
|
// everybody; doing it for a user rearranging their own dashboard
|
|
2566
2583
|
// would rewrite the page for everyone who opens it after them.
|
|
2567
|
-
|
|
2584
|
+
// While Buildiq edits, the drag is the admin's and goes below.
|
|
2585
|
+
if (this.showsUserLayout) {
|
|
2568
2586
|
this.userLayoutDirty = true;
|
|
2569
2587
|
this.userLayoutItems = this.userLayoutItems.map((item) => {
|
|
2570
2588
|
const u = (Array.isArray(updated) ? updated : []).find((candidate) => String(candidate.id) === String(item.id)
|
|
@@ -2573,6 +2591,11 @@ var script = {
|
|
|
2573
2591
|
return (u ? { ...item, ...this.geometryFrom(u) } : item)
|
|
2574
2592
|
});
|
|
2575
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
|
+
}
|
|
2576
2599
|
|
|
2577
2600
|
return
|
|
2578
2601
|
}
|