@conduction/nextcloud-vue 2.141.0 → 2.143.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
  10. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
  12. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -0
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  27. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  28. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  31. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +21 -0
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +70 -5
  47. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +5 -3
  49. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  51. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  52. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  53. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +36 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  59. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  66. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  67. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  84. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  85. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  86. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  87. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  88. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  89. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  90. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  91. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  92. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  93. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  94. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +10 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +12 -3
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +976 -592
  101. package/dist/esm/utils/buildViewTree.js +23 -8
  102. package/dist/esm/utils/buildViewTree.js.map +1 -1
  103. package/dist/nextcloud-vue.cjs.js +1363 -734
  104. package/dist/nextcloud-vue.cjs.js.map +2 -2
  105. package/dist/nextcloud-vue.css +976 -592
  106. package/l10n/en.json +10 -1
  107. package/l10n/nl.json +12 -3
  108. package/package.json +1 -1
  109. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  110. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  111. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  112. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  113. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  114. package/src/components/CnCellRenderer/CnCellRenderer.vue +15 -1
  115. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  116. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +22 -1
  119. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  120. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  121. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +72 -7
  122. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  123. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  124. package/src/components/CnIndexPage/CnIndexPage.vue +37 -2
  125. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  127. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  128. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  129. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  130. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  131. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  132. package/src/css/dialog.css +18 -0
  133. package/src/css/look-board-detail.css +126 -0
  134. package/src/css/look-board-index.css +95 -1
  135. package/src/css/look-board.css +90 -1
  136. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  137. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  138. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  139. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  140. package/src/utils/buildViewTree.js +23 -8
@@ -1 +1 @@
1
- {"version":3,"file":"CnTimelineManage.vue3.js","sources":["../../../../src/components/CnTimelineWidget/CnTimelineManage.vue?vue&type=template&id=a4c73a6a&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n\n CnTimelineManage — the `manage` mode of CnTimelineWidget: one chronological\n read of OpenRegister's timeline on an object (notes, calls, messages), newest\n first with pinned entries on top, plus what a reader who may manage it can do\n there: filter by kind and visibility, pin, close a follow-up, write a note\n (optionally from a standard text block, optionally visible outside), export\n the shown entries as CSV, and run configured entry actions such as a reply\n dialog.\n\n The list read is filtered by OpenRegister whether or not a filter is asked\n for; the response says whether the reader may manage (`canManage`), and the\n controls are drawn from that rather than guessed. A failed read is not an\n empty history.\n-->\n<template>\n\t<div class=\"cn-timeline-manage\"\n\t\t:class=\"{ 'cn-timeline-manage--composer-only': composerOnly }\"\n\t\tdata-testid=\"cn-timeline-manage\">\n\t\t<template v-if=\"!composerOnly\">\n\t\t\t<div class=\"cn-timeline-manage__header\">\n\t\t\t\t<div class=\"cn-timeline-manage__heading\">\n\t\t\t\t\t<h3 class=\"cn-timeline-manage__title\">\n\t\t\t\t\t\t{{ titleText }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p class=\"cn-timeline-manage__subtitle\">\n\t\t\t\t\t\t{{ subtitleText }}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-timeline-manage__header-actions\">\n\t\t\t\t\t<NcButton v-if=\"canManage && (composerEnabled || composerSeparate)\"\n\t\t\t\t\t\tdata-testid=\"cn-timeline-add-note\"\n\t\t\t\t\t\t@click=\"focusComposer\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Add note') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<NcButton data-testid=\"cn-timeline-export\" @click=\"exportEntries\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Download :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-timeline-manage__chips\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Kind')\"\n\t\t\t\tdata-testid=\"cn-timeline-kind-filter\">\n\t\t\t\t<button v-for=\"chip in kindChips\"\n\t\t\t\t\t:key=\"chip.key\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-timeline-manage__chip\"\n\t\t\t\t\t:class=\"{ 'cn-timeline-manage__chip--active': chip.active }\"\n\t\t\t\t\t:aria-pressed=\"chip.active ? 'true' : 'false'\"\n\t\t\t\t\t:data-testid=\"'cn-timeline-chip-' + chip.key\"\n\t\t\t\t\t@click=\"chooseKind(chip.id)\">\n\t\t\t\t\t{{ chip.label }}\n\t\t\t\t\t<span class=\"cn-timeline-manage__chip-count\">{{ chip.count }}</span>\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-timeline-manage__controls\">\n\t\t\t\t<NcSelect v-if=\"canManage\"\n\t\t\t\t\tv-model=\"visibilityFilter\"\n\t\t\t\t\tclass=\"cn-timeline-manage__filter\"\n\t\t\t\t\t:options=\"visibilityOptions\"\n\t\t\t\t\t:clearable=\"true\"\n\t\t\t\t\tlabel=\"label\"\n\t\t\t\t\t:inputLabel=\"visibilityLabelShown ? t('nextcloud-vue', 'Visibility') : undefined\"\n\t\t\t\t\t:labelOutside=\"!visibilityLabelShown\"\n\t\t\t\t\t:ariaLabelCombobox=\"t('nextcloud-vue', 'Visibility')\"\n\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Internal and public')\"\n\t\t\t\t\tdata-testid=\"cn-timeline-visibility-filter\"\n\t\t\t\t\t@update:modelValue=\"load\" />\n\t\t\t</div>\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"32\" />\n\n\t\t\t<NcNoteCard v-else-if=\"error\" type=\"error\" data-testid=\"cn-timeline-error\">\n\t\t\t\t{{ error }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<NcEmptyContent v-else-if=\"entries.length === 0\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'Nothing recorded yet')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Notes, calls and messages appear here in the order they happened.')\"\n\t\t\t\tdata-testid=\"cn-timeline-empty\" />\n\n\t\t\t<ul v-else class=\"cn-timeline-manage__list\" data-testid=\"cn-timeline-list\">\n\t\t\t\t<li v-for=\"entry in entries\"\n\t\t\t\t\t:key=\"entry.id\"\n\t\t\t\t\tclass=\"cn-timeline-manage__entry\"\n\t\t\t\t\t:class=\"{ 'cn-timeline-manage__entry--pinned': entry.pinned }\"\n\t\t\t\t\t:data-kind=\"entry.kind || 'note'\"\n\t\t\t\t\tdata-testid=\"cn-timeline-entry\">\n\t\t\t\t\t<div class=\"cn-timeline-manage__head\">\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__kind\">{{ kindLabel(entry) }}</span>\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__author\">{{ entry.author }}</span>\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__moment\">{{ moment(entry.created) }}</span>\n\t\t\t\t\t\t<span v-if=\"entry.visibility === 'public'\"\n\t\t\t\t\t\t\tclass=\"cn-timeline-manage__badge\"\n\t\t\t\t\t\t\tdata-testid=\"cn-timeline-public\">\n\t\t\t\t\t\t\t{{ publicLabel }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-if=\"entry.followUp === 'open'\"\n\t\t\t\t\t\t\tclass=\"cn-timeline-manage__badge cn-timeline-manage__badge--open\"\n\t\t\t\t\t\t\tdata-testid=\"cn-timeline-followup-open\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Needs an answer') }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<p class=\"cn-timeline-manage__message\">\n\t\t\t\t\t\t{{ entry.message }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<p v-if=\"entry.siblings && entry.siblings.length\"\n\t\t\t\t\t\tclass=\"cn-timeline-manage__siblings\"\n\t\t\t\t\t\tdata-testid=\"cn-timeline-siblings\">\n\t\t\t\t\t\t{{ n('nextcloud-vue', 'Also written on %n other record', 'Also written on %n other records', entry.siblings.length) }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<div v-if=\"canManage && !entry.isRead\" class=\"cn-timeline-manage__actions\">\n\t\t\t\t\t\t<NcButton variant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-pin-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"togglePin(entry)\">\n\t\t\t\t\t\t\t{{ entry.pinned ? t('nextcloud-vue', 'Unpin') : t('nextcloud-vue', 'Pin') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton v-for=\"action in actionsFor(entry)\"\n\t\t\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-action-' + action.id + '-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"openAction(action, entry)\">\n\t\t\t\t\t\t\t{{ translate(action.label) }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton v-if=\"entry.followUp === 'open'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-followup-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"closeFollowUp(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Mark as answered') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<div v-if=\"(!loading && !error && allEntries.length > 0) || auditTrailLinkLabel || readsToggleLabel\"\n\t\t\t\tclass=\"cn-timeline-manage__footer\">\n\t\t\t\t<p v-if=\"!loading && !error && allEntries.length > 0\"\n\t\t\t\t\tclass=\"cn-timeline-manage__count\"\n\t\t\t\t\tdata-testid=\"cn-timeline-count\">\n\t\t\t\t\t{{ countLine }}\n\t\t\t\t</p>\n\t\t\t\t<NcCheckboxRadioSwitch v-if=\"readsToggleLabel && readsAllowed\"\n\t\t\t\t\tv-model=\"includeReads\"\n\t\t\t\t\tdata-testid=\"cn-timeline-include-reads\"\n\t\t\t\t\t@update:modelValue=\"load\">\n\t\t\t\t\t{{ readsToggleLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<NcButton v-if=\"auditTrailLinkLabel\"\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\tclass=\"cn-timeline-manage__audit-link\"\n\t\t\t\t\tdata-testid=\"cn-timeline-audit-link\"\n\t\t\t\t\t@click=\"openAuditTrail\">\n\t\t\t\t\t{{ auditTrailLinkLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<div v-if=\"composerOnly && canManage && composerEnabled\"\n\t\t\tclass=\"cn-timeline-manage__header\"\n\t\t\tdata-testid=\"cn-timeline-composer-heading\">\n\t\t\t<div class=\"cn-timeline-manage__heading\">\n\t\t\t\t<h3 class=\"cn-timeline-manage__title\">\n\t\t\t\t\t{{ composerTitleText }}\n\t\t\t\t</h3>\n\t\t\t\t<p class=\"cn-timeline-manage__subtitle\">\n\t\t\t\t\t{{ composerSubtitleText }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcNoteCard v-if=\"composerOnly && error\" type=\"error\" data-testid=\"cn-timeline-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\n\t\t<form v-if=\"canManage && composerEnabled\"\n\t\t\tref=\"composer\"\n\t\t\tclass=\"cn-timeline-manage__composer\"\n\t\t\tdata-testid=\"cn-timeline-composer\"\n\t\t\t@submit.prevent=\"submit\">\n\t\t\t<template v-if=\"!composerOnly\">\n\t\t\t\t<h3 class=\"cn-timeline-manage__composer-title\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add note') }}\n\t\t\t\t</h3>\n\t\t\t\t<p class=\"cn-timeline-manage__composer-hint\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible') }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t\t<NcSelect v-if=\"textBlocks.length\"\n\t\t\t\tv-model=\"chosenBlock\"\n\t\t\t\tclass=\"cn-timeline-manage__filter\"\n\t\t\t\t:options=\"blockOptions\"\n\t\t\t\t:clearable=\"true\"\n\t\t\t\tlabel=\"label\"\n\t\t\t\t:inputLabel=\"t('nextcloud-vue', 'Template')\"\n\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Write your own')\"\n\t\t\t\tdata-testid=\"cn-timeline-block\"\n\t\t\t\t@update:modelValue=\"pickBlock\" />\n\n\t\t\t<label class=\"cn-timeline-manage__draft-label\" :for=\"draftId\">\n\t\t\t\t{{ t('nextcloud-vue', 'Note') }}\n\t\t\t</label>\n\t\t\t<textarea :id=\"draftId\"\n\t\t\t\tv-model=\"draft\"\n\t\t\t\tclass=\"cn-timeline-manage__draft\"\n\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Write a note on this record')\"\n\t\t\t\tdata-testid=\"cn-timeline-draft\" />\n\n\t\t\t<div class=\"cn-timeline-manage__composer-foot\">\n\t\t\t\t<NcCheckboxRadioSwitch v-model=\"draftIsPublic\" data-testid=\"cn-timeline-draft-public\">\n\t\t\t\t\t{{ publicLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"saving || !draft.trim()\"\n\t\t\t\t\tdata-testid=\"cn-timeline-save\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add it to the timeline') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<component :is=\"activeModal.component\"\n\t\t\tv-if=\"activeModal\"\n\t\t\tv-bind=\"activeModal.props\"\n\t\t\tv-on=\"activeModal.listeners\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'\nimport Download from 'vue-material-design-icons/Download.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { prefixUrl } from '../../utils/headers.js'\n\nlet instanceCount = 0\n\n/** The kind of a read row (`readsToggle`), never a kind OpenRegister declares. */\nconst READ_KIND = 'cn-read'\n\n/** Event-bus channel: a note was added to an object's timeline (`{ objectId, source }`). */\nconst TIMELINE_CHANGED = 'cn:timeline:changed'\n\n/** Event-bus channel: the list's \"Add note\" asks the separate form card to take focus (`{ objectId }`). */\nconst TIMELINE_COMPOSE = 'cn:timeline:compose'\n\n/**\n * Read a dotted path off a value.\n *\n * @param {object} value The value.\n * @param {string} path The path.\n * @return {unknown} What is there, or undefined.\n */\nfunction readPath(value, path) {\n\treturn String(path).split('.').reduce((at, key) => (at === null || at === undefined ? undefined : at[key]), value)\n}\n\nexport default {\n\tname: 'CnTimelineManage',\n\n\tcomponents: { Download, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect, Plus },\n\n\tinject: {\n\t\t/** The app's component registry; entry actions resolve their modal here. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Consumer translation function; manifest labels are authored in English. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's object sidebar channel; the audit trail link opens its tab. */\n\t\tobjectSidebarState: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Register slug. */\n\t\tregister: { type: String, default: '' },\n\t\t/** Schema slug. */\n\t\tschema: { type: String, default: '' },\n\t\t/** The object's id. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** OpenRegister API base. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/**\n\t\t * The widget content: `{ title?, subtitle?, publicLabel?, exportName?,\n\t\t * composer?, textBlocks?, entryActions?, auditTrailLink? }`.\n\t\t * `composer`: `true` (default) draws the note form under the list,\n\t\t * `false` none, `\"separate\"` none but keeps the \"Add note\" button,\n\t\t * which brings a `\"only\"` instance (the form alone, a card of its own\n\t\t * in a stack) into view; a note saved there reloads the list.\n\t\t * `readsToggle` (`true` or a label) adds a footer switch, off by\n\t\t * default, that puts who read the record (its audit trail's reads)\n\t\t * among the entries; the count then says \"changes only\" while it is off.\n\t\t * `auditTrailLink` (`true` or a label) draws a footer link that opens\n\t\t * the object sidebar on its audit trail tab, `auditTrailTab` when the\n\t\t * page declares that tab under its own id.\n\t\t */\n\t\tcontent: { type: Object, default: () => ({}) },\n\t},\n\n\tdata() {\n\t\tinstanceCount += 1\n\t\treturn {\n\t\t\tuid: instanceCount,\n\t\t\tallEntries: [],\n\t\t\tentries: [],\n\t\t\tkinds: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tcanManage: false,\n\t\t\tkindFilter: null,\n\t\t\tvisibilityFilter: null,\n\t\t\ttextBlocks: [],\n\t\t\tchosenBlock: null,\n\t\t\tdraft: '',\n\t\t\tdraftIsPublic: false,\n\t\t\tsaving: false,\n\t\t\tactiveModal: null,\n\t\t\t/** Whether the list also shows who read the record (`readsToggle`). */\n\t\t\tincludeReads: false,\n\t\t\t/** Whether OpenRegister gives this reader read entries (`readEventsVisible`); null until asked. */\n\t\t\treadsAllowed: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\ttitleText() {\n\t\t\treturn this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'History')\n\t\t},\n\n\t\tsubtitleText() {\n\t\t\treturn this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'What happened, who did it and when, newest first')\n\t\t},\n\n\t\t/** @return {string} The title of the note form's own card (`composer: \"only\"`): `content.title`, else Add note. */\n\t\tcomposerTitleText() {\n\t\t\treturn this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'Add note')\n\t\t},\n\n\t\t/** @return {string} The line under that title: `content.subtitle`, else where the note goes. */\n\t\tcomposerSubtitleText() {\n\t\t\treturn this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible')\n\t\t},\n\n\t\tpublicLabel() {\n\t\t\treturn this.content.publicLabel ? this.translate(this.content.publicLabel) : t('nextcloud-vue', 'Visible to the requester')\n\t\t},\n\n\t\t/**\n\t\t * The audit trail link's words, or '' when it is not drawn.\n\t\t *\n\t\t * @spec openspec/changes/timeline-audit-trail-link/specs/timeline/spec.md#requirement-the-manage-timeline-can-point-at-the-audit-trail-in-the-sidebar\n\t\t * @return {string}\n\t\t */\n\t\tauditTrailLinkLabel() {\n\t\t\tconst link = this.content.auditTrailLink\n\t\t\tif (!link || !this.objectSidebarState) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof link === 'string' ? this.translate(link) : t('nextcloud-vue', 'Full audit trail in the sidebar')\n\t\t},\n\n\t\t/**\n\t\t * Whether this instance draws the note form: inline (`composer` true or\n\t\t * unset) or as the whole widget (`\"only\"`).\n\t\t *\n\t\t * @spec openspec/changes/timeline-composer-card/specs/timeline/spec.md#requirement-the-note-form-can-be-its-own-card\n\t\t * @return {boolean}\n\t\t */\n\t\tcomposerEnabled() {\n\t\t\treturn this.content.composer !== false && this.content.composer !== 'separate'\n\t\t},\n\n\t\t/**\n\t\t * The words of the \"also who read it\" switch, or '' when the widget\n\t\t * does not offer it (`readsToggle`: true or a label).\n\t\t *\n\t\t * @spec openspec/changes/timeline-reads-toggle/specs/timeline/spec.md#requirement-the-history-can-show-who-read-the-record\n\t\t * @return {string}\n\t\t */\n\t\treadsToggleLabel() {\n\t\t\tconst toggle = this.content.readsToggle\n\t\t\tif (!toggle) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof toggle === 'string' ? this.translate(toggle) : t('nextcloud-vue', 'Also who read it')\n\t\t},\n\n\t\t/**\n\t\t * \"3 of 10 events\", and \", changes only\" while reads can be shown but are not.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tcountLine() {\n\t\t\tconst values = { shown: this.entries.length, total: this.allEntries.length }\n\t\t\treturn this.readsToggleLabel && this.readsAllowed && !this.includeReads\n\t\t\t\t? t('nextcloud-vue', '{shown} of {total} events, changes only', values)\n\t\t\t\t: t('nextcloud-vue', '{shown} of {total} events', values)\n\t\t},\n\n\t\t/**\n\t\t * Whether the visibility filter shows its \"Visibility\" label. With\n\t\t * `visibilityLabel: false` it keeps only its accessible name and the\n\t\t * select reads \"Internal and public\" on its own (board DqZaakHistorie).\n\t\t *\n\t\t * @spec openspec/changes/timeline-visibility-filter-unlabelled/specs/timeline/spec.md#requirement-the-visibility-filter-can-go-without-a-visible-label\n\t\t * @return {boolean}\n\t\t */\n\t\tvisibilityLabelShown() {\n\t\t\treturn this.content.visibilityLabel !== false\n\t\t},\n\n\t\t/** @return {boolean} The widget is the note form alone (`composer: \"only\"`), a card of its own beside the list. */\n\t\tcomposerOnly() {\n\t\t\treturn this.content.composer === 'only'\n\t\t},\n\n\t\t/** @return {boolean} The list draws no form; its \"Add note\" brings the separate form card into view (`composer: \"separate\"`). */\n\t\tcomposerSeparate() {\n\t\t\treturn this.content.composer === 'separate'\n\t\t},\n\n\t\tdraftId() {\n\t\t\treturn `cn-timeline-draft-${this.uid}`\n\t\t},\n\n\t\t/**\n\t\t * The kind options, the plain note first. A note is an entry with NO\n\t\t * kind, so \"Note\" filters on its absence.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tkindOptions() {\n\t\t\tconst options = [{ id: '', label: t('nextcloud-vue', 'Note') }].concat(this.kinds.map((kind) => ({ id: kind.slug, label: kind.title || kind.slug })))\n\t\t\treturn this.includeReads ? options.concat([{ id: READ_KIND, label: t('nextcloud-vue', 'Viewed') }]) : options\n\t\t},\n\n\t\t/**\n\t\t * All, then every kind the object has entries of, each with its count.\n\t\t *\n\t\t * @spec openspec/changes/timeline-all-chip-host-label/specs/timeline/spec.md#requirement-the-timelines-all-chip-reads-the-hosts-word\n\t\t * @return {Array<object>} The chips.\n\t\t */\n\t\tkindChips() {\n\t\t\tconst chosen = this.kindFilter ? (this.kindFilter.id || '') : null\n\t\t\tconst counts = new Map()\n\t\t\tfor (const entry of this.allEntries) {\n\t\t\t\tconst kind = entry.kind || ''\n\t\t\t\tcounts.set(kind, (counts.get(kind) || 0) + 1)\n\t\t\t}\n\t\t\t// The host's word first, as its own filter chips read (\"Alles\"), then the library's.\n\t\t\tconst hostAll = typeof this.cnTranslate === 'function' ? this.cnTranslate('All') : 'All'\n\t\t\tconst allLabel = typeof hostAll === 'string' && hostAll !== '' && hostAll !== 'All' ? hostAll : t('nextcloud-vue', 'All')\n\t\t\tconst chips = [{ key: 'all', id: null, label: allLabel, count: this.allEntries.length, active: chosen === null }]\n\t\t\tfor (const option of this.kindOptions) {\n\t\t\t\tconst count = counts.get(option.id) || 0\n\t\t\t\tif (count > 0) {\n\t\t\t\t\tchips.push({ key: option.id || 'note', id: option.id, label: option.label, count, active: chosen === option.id })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn chips\n\t\t},\n\n\t\tblockOptions() {\n\t\t\treturn this.textBlocks.map((block) => ({ id: block.slug, label: block.title || block.slug }))\n\t\t},\n\n\t\tvisibilityOptions() {\n\t\t\treturn [\n\t\t\t\t{ id: 'internal', label: t('nextcloud-vue', 'Internal only') },\n\t\t\t\t{ id: 'public', label: this.publicLabel },\n\t\t\t]\n\t\t},\n\n\t\tbase() {\n\t\t\treturn this.apiBase.replace(/\\/$/, '')\n\t\t},\n\n\t\ttimelineUrl() {\n\t\t\tif (!this.objectId || !this.register || !this.schema) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn prefixUrl(`${this.base}/objects/${this.register}/${this.schema}/${this.objectId}/timeline`)\n\t\t},\n\n\t\tentryActions() {\n\t\t\treturn Array.isArray(this.content.entryActions) ? this.content.entryActions.filter((action) => action && action.id) : []\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.onTimelineChanged = (payload) => {\n\t\t\tif (!payload || payload.objectId !== this.objectId || payload.source === this.uid || this.composerOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.load()\n\t\t}\n\t\tthis.onComposeRequested = (payload) => {\n\t\t\tif (this.composerOnly && payload && payload.objectId === this.objectId) {\n\t\t\t\tthis.focusComposer()\n\t\t\t}\n\t\t}\n\t\tsubscribe(TIMELINE_CHANGED, this.onTimelineChanged)\n\t\tsubscribe(TIMELINE_COMPOSE, this.onComposeRequested)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(TIMELINE_CHANGED, this.onTimelineChanged)\n\t\tunsubscribe(TIMELINE_COMPOSE, this.onComposeRequested)\n\t},\n\n\tmounted() {\n\t\tthis.loadKinds()\n\t\tif (this.content.textBlocks !== false) {\n\t\t\tthis.loadTextBlocks()\n\t\t}\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\t\tn,\n\n\t\ttranslate(text) {\n\t\t\treturn text ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/** Read the declared kinds, so the filter names them as administered. */\n\t\tasync loadKinds() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(prefixUrl(`${this.base}/timeline/kinds`))\n\t\t\t\tthis.kinds = response.data?.results || []\n\t\t\t} catch {\n\t\t\t\tthis.kinds = []\n\t\t\t}\n\t\t},\n\n\t\t/** Read the seeded standard notes. */\n\t\tasync loadTextBlocks() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(prefixUrl(`${this.base}/timeline/text-blocks`))\n\t\t\t\tthis.textBlocks = response.data?.results || []\n\t\t\t} catch {\n\t\t\t\tthis.textBlocks = []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Note which standard note was picked. The BLOCK is sent, not its body:\n\t\t * OpenRegister fills the object's own values into it on the way in.\n\t\t *\n\t\t * @param {object|null} block The picked option.\n\t\t */\n\t\tpickBlock(block) {\n\t\t\tthis.chosenBlock = block || null\n\t\t\tif (block) {\n\t\t\t\tconst declared = this.textBlocks.find((entry) => entry.slug === block.id)\n\t\t\t\tthis.draft = declared?.title || block.label\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write the drafted note onto this object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync submit() {\n\t\t\tif (!this.draft.trim() || !this.timelineUrl) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst payload = { visibility: this.draftIsPublic ? 'public' : 'internal' }\n\t\t\t\tif (this.chosenBlock?.id) {\n\t\t\t\t\tpayload.textBlock = this.chosenBlock.id\n\t\t\t\t} else {\n\t\t\t\t\tpayload.message = this.draft\n\t\t\t\t}\n\t\t\t\tawait axios.post(this.timelineUrl, payload)\n\t\t\t\tthis.draft = ''\n\t\t\t\tthis.chosenBlock = null\n\t\t\t\tthis.draftIsPublic = false\n\t\t\t\temit(TIMELINE_CHANGED, { objectId: this.objectId, source: this.uid })\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'That note could not be saved.')\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the timeline with the visibility filter, if one is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tif (!this.timelineUrl) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst params = {}\n\t\t\t\tif (this.visibilityFilter?.id) {\n\t\t\t\t\tparams.visibility = this.visibilityFilter.id\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(this.timelineUrl, { params })\n\t\t\t\tthis.canManage = response.data?.canManage === true\n\t\t\t\tconst entries = response.data?.results || []\n\t\t\t\tif (this.readsToggleLabel && this.readsAllowed === null) {\n\t\t\t\t\tawait this.readEntries()\n\t\t\t\t}\n\t\t\t\tthis.allEntries = this.readsToggleLabel && this.readsAllowed && this.includeReads ? this.withReads(entries, await this.readEntries()) : entries\n\t\t\t\tthis.entries = this.applyKindFilter(this.allEntries)\n\t\t\t} catch {\n\t\t\t\tthis.allEntries = []\n\t\t\t\tthis.entries = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'The timeline could not be read. Try again, or ask an administrator to check OpenRegister.')\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 * Who read the record, from its audit trail (`action=read`), as timeline\n\t\t * rows. OpenRegister gives read entries only to the groups named for\n\t\t * them and says so in `readEventsVisible`, which decides whether the\n\t\t * switch is offered at all; a read it refuses or that fails leaves the\n\t\t * reads out and offers no switch.\n\t\t *\n\t\t * @spec openspec/changes/timeline-reads-toggle/specs/timeline/spec.md#requirement-the-history-can-show-who-read-the-record\n\t\t * @return {Promise<Array<object>>} The read rows.\n\t\t */\n\t\tasync readEntries() {\n\t\t\ttry {\n\t\t\t\tconst url = prefixUrl(`${this.base}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails`)\n\t\t\t\tconst response = await axios.get(url, { params: { action: 'read', limit: 50 } })\n\t\t\t\tthis.readsAllowed = response.data?.readEventsVisible === true\n\t\t\t\tif (!this.readsAllowed) {\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t\tconst list = response.data?.results || []\n\t\t\t\treturn list\n\t\t\t\t\t.filter((entry) => String(entry.action || '').toLowerCase() === 'read')\n\t\t\t\t\t.map((entry) => ({\n\t\t\t\t\t\tid: `read-${entry.id}`,\n\t\t\t\t\t\tkind: READ_KIND,\n\t\t\t\t\t\tisRead: true,\n\t\t\t\t\t\tauthor: entry.actorDisplayName || entry.userName || entry.user || '',\n\t\t\t\t\t\tcreated: entry.created || entry.creationDateTime || '',\n\t\t\t\t\t\tmessage: t('nextcloud-vue', 'Opened the record'),\n\t\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.readsAllowed = false\n\t\t\t\treturn []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The timeline's entries with the reads among them, newest first; the\n\t\t * pinned entries stay on top.\n\t\t *\n\t\t * @param {Array<object>} entries The timeline's entries.\n\t\t * @param {Array<object>} reads The read rows.\n\t\t * @return {Array<object>} The merged list.\n\t\t */\n\t\twithReads(entries, reads) {\n\t\t\tconst time = (entry) => new Date(entry.created || 0).getTime() || 0\n\t\t\tconst pinned = entries.filter((entry) => entry.pinned)\n\t\t\tconst rest = [...entries.filter((entry) => !entry.pinned), ...reads].sort((a, b) => time(b) - time(a))\n\t\t\treturn [...pinned, ...rest]\n\t\t},\n\n\t\t/**\n\t\t * Show one kind, or every kind for `null`, without reading again.\n\t\t *\n\t\t * @param {string|null} id The kind slug, '' for a note, null for all.\n\t\t */\n\t\tchooseKind(id) {\n\t\t\tthis.kindFilter = id === null ? null : { id }\n\t\t\tthis.entries = this.applyKindFilter(this.allEntries)\n\t\t},\n\n\t\t/**\n\t\t * The kind filter is applied in the reading: the list endpoint does not\n\t\t * read a kind parameter, so sending one would be dropped in silence.\n\t\t *\n\t\t * @param {Array<object>} results The entries as read.\n\t\t * @return {Array<object>} The entries to show.\n\t\t */\n\t\tapplyKindFilter(results) {\n\t\t\tif (!this.kindFilter) {\n\t\t\t\treturn results\n\t\t\t}\n\t\t\tconst wanted = this.kindFilter.id || ''\n\t\t\treturn results.filter((entry) => (entry.kind || '') === wanted)\n\t\t},\n\n\t\t/**\n\t\t * The shown entries as CSV: when, kind, who, visibility and what.\n\t\t *\n\t\t * @return {string} The CSV text.\n\t\t */\n\t\tentriesCsv() {\n\t\t\tconst cell = (value) => `\"${String(value ?? '').replace(/\"/g, '\"\"')}\"`\n\t\t\tconst rows = [[t('nextcloud-vue', 'When'), t('nextcloud-vue', 'Kind'), t('nextcloud-vue', 'Who'), t('nextcloud-vue', 'Visibility'), t('nextcloud-vue', 'Message')]]\n\t\t\tfor (const entry of this.entries) {\n\t\t\t\trows.push([entry.created, this.kindLabel(entry), entry.author, entry.visibility, entry.message])\n\t\t\t}\n\t\t\treturn rows.map((row) => row.map(cell).join(',')).join('\\r\\n')\n\t\t},\n\n\t\t/** Download the shown entries as a CSV file. */\n\t\texportEntries() {\n\t\t\tconst blob = new Blob([this.entriesCsv()], { type: 'text/csv;charset=utf-8' })\n\t\t\tconst url = URL.createObjectURL(blob)\n\t\t\tconst link = document.createElement('a')\n\t\t\tlink.href = url\n\t\t\tlink.download = `${this.content.exportName || 'history'}-${this.objectId || 'record'}.csv`\n\t\t\tdocument.body.appendChild(link)\n\t\t\tlink.click()\n\t\t\tlink.remove()\n\t\t\tURL.revokeObjectURL(url)\n\t\t},\n\n\t\t/**\n\t\t * Open the object sidebar on its audit trail tab.\n\t\t *\n\t\t * @spec openspec/changes/timeline-audit-trail-link/specs/timeline/spec.md#requirement-the-manage-timeline-can-point-at-the-audit-trail-in-the-sidebar\n\t\t */\n\t\topenAuditTrail() {\n\t\t\tthis.objectSidebarState.open = true\n\t\t\tthis.objectSidebarState.requestedTab = this.content.auditTrailTab || 'auditTrail'\n\t\t},\n\n\t\t/** Bring the composer into view and focus its text; a separate form card is asked to do so. */\n\t\tfocusComposer() {\n\t\t\tif (this.composerSeparate) {\n\t\t\t\temit(TIMELINE_COMPOSE, { objectId: this.objectId })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst form = this.$refs.composer\n\t\t\tif (!form) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tform.scrollIntoView?.({ behavior: 'smooth', block: 'center' })\n\t\t\tform.querySelector('textarea')?.focus()\n\t\t},\n\n\t\t/**\n\t\t * The label of one entry's kind, falling back to \"Note\".\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {string} The label.\n\t\t */\n\t\tkindLabel(entry) {\n\t\t\tif (entry.kind === READ_KIND) {\n\t\t\t\treturn t('nextcloud-vue', 'Viewed')\n\t\t\t}\n\t\t\tif (!entry.kind) {\n\t\t\t\treturn t('nextcloud-vue', 'Note')\n\t\t\t}\n\t\t\tconst declared = this.kinds.find((kind) => kind.slug === entry.kind)\n\t\t\treturn declared?.title || entry.kind\n\t\t},\n\n\t\t/**\n\t\t * One moment in the reader's locale.\n\t\t *\n\t\t * @param {string} value An ISO 8601 stamp.\n\t\t * @return {string} The moment, or the raw value.\n\t\t */\n\t\tmoment(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst parsed = new Date(value)\n\t\t\treturn isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n\t\t},\n\n\t\tasync togglePin(entry) {\n\t\t\tawait this.patch(entry, { pinned: !entry.pinned })\n\t\t},\n\n\t\tasync closeFollowUp(entry) {\n\t\t\tawait this.patch(entry, { followUp: 'done' })\n\t\t},\n\n\t\t/**\n\t\t * Write one change onto one entry and read the timeline back (pinning\n\t\t * changes the order, so patching in place would look like nothing).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {object} changes The fields to write.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync patch(entry, changes) {\n\t\t\ttry {\n\t\t\t\tawait axios.patch(`${this.timelineUrl}/${entry.id}`, changes)\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'That change could not be saved.')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The configured entry actions that apply to one entry (`kinds` limits\n\t\t * an action to entries of those kinds).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {Array<object>} The actions.\n\t\t */\n\t\tactionsFor(entry) {\n\t\t\treturn this.entryActions.filter((action) => !Array.isArray(action.kinds) || action.kinds.includes(entry.kind || ''))\n\t\t},\n\n\t\t/**\n\t\t * Resolve `@objectId` and `@row.<path>` tokens in an action's props.\n\t\t *\n\t\t * @param {object} props The props as declared.\n\t\t * @param {object} entry The entry.\n\t\t * @return {object} The resolved props.\n\t\t */\n\t\tresolveProps(props, entry) {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(props || {})) {\n\t\t\t\tif (value === '@objectId') {\n\t\t\t\t\tout[key] = this.objectId\n\t\t\t\t} else if (typeof value === 'string' && value.startsWith('@row.')) {\n\t\t\t\t\tconst found = readPath(entry, value.slice('@row.'.length))\n\t\t\t\t\tout[key] = found === undefined || found === null ? '' : found\n\t\t\t\t} else {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Open an entry action's modal from the app's registry. On its\n\t\t * `doneEvent` (default `done`) the entry's open follow-up is closed when\n\t\t * the action says so; otherwise the timeline is read again.\n\t\t *\n\t\t * @param {object} action The action.\n\t\t * @param {object} entry The entry.\n\t\t */\n\t\topenAction(action, entry) {\n\t\t\tconst registered = (this.cnRegistry || {})[action.modal]\n\t\t\tconst component = registered && (registered.component || registered)\n\t\t\tif (!component) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnTimelineWidget] entry action \"${action.id}\" names modal \"${action.modal}\", which the registry does not hold.`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst close = () => {\n\t\t\t\tthis.activeModal = null\n\t\t\t}\n\t\t\tconst done = async () => {\n\t\t\t\tthis.activeModal = null\n\t\t\t\tif (action.closesFollowUp === true && entry.followUp === 'open') {\n\t\t\t\t\tawait this.closeFollowUp(entry)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tawait this.load()\n\t\t\t}\n\t\t\tthis.activeModal = {\n\t\t\t\tcomponent,\n\t\t\t\tprops: this.resolveProps(action.props, entry),\n\t\t\t\tlisteners: { [action.doneEvent || 'done']: done, close },\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-timeline-manage__header {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin-block-end: 16px;\n}\n\n.cn-timeline-manage__heading {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-timeline-manage__header-actions {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-timeline-manage__title {\n\tmargin: 0;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n}\n\n.cn-timeline-manage__subtitle {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n\n.cn-timeline-manage__chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n\tmargin-block-end: 12px;\n}\n\n.cn-timeline-manage__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tbox-sizing: border-box;\n\theight: 36px;\n\tpadding: 0 12px;\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 18px;\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcursor: pointer;\n}\n\n/* The chosen kind is a filled chip with no outline. */\n.cn-timeline-manage__chip--active {\n\tborder: 0;\n\tbackground: var(--color-main-text);\n\tcolor: var(--color-main-background);\n}\n\n.cn-timeline-manage__chip-count {\n\tpadding: 1px 7px;\n\tborder-radius: 10px;\n\tbackground: var(--color-border);\n\tcolor: var(--color-main-text);\n\tfont-size: 12px;\n}\n\n/* On the filled chip the count is a lighter ink bubble with light text. */\n.cn-timeline-manage__chip--active .cn-timeline-manage__chip-count {\n\tbackground: color-mix(in srgb, var(--color-main-background) 18%, var(--color-main-text));\n\tcolor: var(--color-main-background);\n}\n\n.cn-timeline-manage__footer {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n\tmargin: 12px 0 0;\n}\n\n.cn-timeline-manage__count {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-manage__composer {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin-block-start: 24px;\n\tpadding-block-start: 16px;\n\tborder-block-start: 1px solid var(--color-border);\n}\n\n.cn-timeline-manage--composer-only .cn-timeline-manage__composer {\n\tmargin-block-start: 0;\n\tpadding-block-start: 0;\n\tborder-block-start: none;\n}\n\n/* The checkbox at the start, the button at the end, as one row. */\n.cn-timeline-manage__composer-foot {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-timeline-manage__composer-title {\n\tmargin: 0;\n\tfont-size: 16px;\n\tfont-weight: bold;\n}\n\n.cn-timeline-manage__composer-hint {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-manage__draft-label {\n\tfont-weight: bold;\n}\n\n.cn-timeline-manage__draft {\n\twidth: 100%;\n\tmin-height: 70px;\n}\n\n.cn-timeline-manage__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\tflex-wrap: wrap;\n\tmargin-block-end: 12px;\n}\n\n.cn-timeline-manage__filter {\n\tmin-width: 200px;\n}\n\n.cn-timeline-manage__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-timeline-manage__entry {\n\tborder-block-end: 1px solid var(--color-border);\n\tpadding-block: 12px;\n}\n\n.cn-timeline-manage__entry--pinned {\n\tborder-inline-start: 3px solid var(--color-primary-element);\n\tpadding-inline-start: 8px;\n}\n\n.cn-timeline-manage__head {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-timeline-manage__kind {\n\tfont-weight: bold;\n\tcolor: var(--color-main-text);\n}\n\n.cn-timeline-manage__badge {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 0 6px;\n}\n\n.cn-timeline-manage__badge--open {\n\tborder-color: var(--color-warning);\n\tcolor: var(--color-warning-text);\n}\n\n.cn-timeline-manage__message {\n\tmargin-block: 4px 0;\n\twhite-space: pre-wrap;\n}\n\n.cn-timeline-manage__siblings {\n\tmargin-block: 4px 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-timeline-manage__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_Fragment","_createElementVNode","_toDisplayString","_createBlock","_createVNode","_createTextVNode","_openBlock","_renderList"],"mappings":"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"}
1
+ {"version":3,"file":"CnTimelineManage.vue3.js","sources":["../../../../src/components/CnTimelineWidget/CnTimelineManage.vue?vue&type=template&id=31e6673c&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n\n CnTimelineManage — the `manage` mode of CnTimelineWidget: one chronological\n read of OpenRegister's timeline on an object (notes, calls, messages), newest\n first with pinned entries on top, plus what a reader who may manage it can do\n there: filter by kind and visibility, pin, close a follow-up, write a note\n (optionally from a standard text block, optionally visible outside), export\n the shown entries as CSV, and run configured entry actions such as a reply\n dialog.\n\n The list read is filtered by OpenRegister whether or not a filter is asked\n for; the response says whether the reader may manage (`canManage`), and the\n controls are drawn from that rather than guessed. A failed read is not an\n empty history.\n-->\n<template>\n\t<div class=\"cn-timeline-manage\"\n\t\t:class=\"{ 'cn-timeline-manage--composer-only': composerOnly }\"\n\t\tdata-testid=\"cn-timeline-manage\">\n\t\t<template v-if=\"!composerOnly\">\n\t\t\t<div class=\"cn-timeline-manage__header\">\n\t\t\t\t<div class=\"cn-timeline-manage__heading\">\n\t\t\t\t\t<h3 class=\"cn-timeline-manage__title\">\n\t\t\t\t\t\t{{ titleText }}\n\t\t\t\t\t</h3>\n\t\t\t\t\t<p class=\"cn-timeline-manage__subtitle\">\n\t\t\t\t\t\t{{ subtitleText }}\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"cn-timeline-manage__header-actions\">\n\t\t\t\t\t<NcButton v-if=\"canManage && (composerEnabled || composerSeparate)\"\n\t\t\t\t\t\tdata-testid=\"cn-timeline-add-note\"\n\t\t\t\t\t\t@click=\"focusComposer\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Add note') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<NcButton data-testid=\"cn-timeline-export\" @click=\"exportEntries\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Download :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Export') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-timeline-manage__chips\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Kind')\"\n\t\t\t\tdata-testid=\"cn-timeline-kind-filter\">\n\t\t\t\t<button v-for=\"chip in kindChips\"\n\t\t\t\t\t:key=\"chip.key\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-timeline-manage__chip\"\n\t\t\t\t\t:class=\"{ 'cn-timeline-manage__chip--active': chip.active }\"\n\t\t\t\t\t:aria-pressed=\"chip.active ? 'true' : 'false'\"\n\t\t\t\t\t:data-testid=\"'cn-timeline-chip-' + chip.key\"\n\t\t\t\t\t@click=\"chooseKind(chip.id)\">\n\t\t\t\t\t{{ chip.label }}\n\t\t\t\t\t<span class=\"cn-timeline-manage__chip-count\">{{ chip.count }}</span>\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<div class=\"cn-timeline-manage__controls\">\n\t\t\t\t<NcSelect v-if=\"canManage\"\n\t\t\t\t\tv-model=\"visibilityFilter\"\n\t\t\t\t\tclass=\"cn-timeline-manage__filter\"\n\t\t\t\t\t:options=\"visibilityOptions\"\n\t\t\t\t\t:clearable=\"true\"\n\t\t\t\t\tlabel=\"label\"\n\t\t\t\t\t:inputLabel=\"visibilityLabelShown ? t('nextcloud-vue', 'Visibility') : undefined\"\n\t\t\t\t\t:labelOutside=\"!visibilityLabelShown\"\n\t\t\t\t\t:ariaLabelCombobox=\"t('nextcloud-vue', 'Visibility')\"\n\t\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Internal and public')\"\n\t\t\t\t\tdata-testid=\"cn-timeline-visibility-filter\"\n\t\t\t\t\t@update:modelValue=\"load\" />\n\t\t\t</div>\n\n\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"32\" />\n\n\t\t\t<NcNoteCard v-else-if=\"error\" type=\"error\" data-testid=\"cn-timeline-error\">\n\t\t\t\t{{ error }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<NcEmptyContent v-else-if=\"entries.length === 0\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'Nothing recorded yet')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'Notes, calls and messages appear here in the order they happened.')\"\n\t\t\t\tdata-testid=\"cn-timeline-empty\" />\n\n\t\t\t<ul v-else class=\"cn-timeline-manage__list\" data-testid=\"cn-timeline-list\">\n\t\t\t\t<li v-for=\"entry in entries\"\n\t\t\t\t\t:key=\"entry.id\"\n\t\t\t\t\tclass=\"cn-timeline-manage__entry\"\n\t\t\t\t\t:class=\"{ 'cn-timeline-manage__entry--pinned': entry.pinned }\"\n\t\t\t\t\t:data-kind=\"entry.kind || 'note'\"\n\t\t\t\t\tdata-testid=\"cn-timeline-entry\">\n\t\t\t\t\t<div class=\"cn-timeline-manage__head\">\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__kind\">{{ kindLabel(entry) }}</span>\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__author\">{{ entry.author }}</span>\n\t\t\t\t\t\t<span class=\"cn-timeline-manage__moment\">{{ moment(entry.created) }}</span>\n\t\t\t\t\t\t<span v-if=\"entry.visibility === 'public'\"\n\t\t\t\t\t\t\tclass=\"cn-timeline-manage__badge\"\n\t\t\t\t\t\t\tdata-testid=\"cn-timeline-public\">\n\t\t\t\t\t\t\t{{ publicLabel }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span v-if=\"entry.followUp === 'open'\"\n\t\t\t\t\t\t\tclass=\"cn-timeline-manage__badge cn-timeline-manage__badge--open\"\n\t\t\t\t\t\t\tdata-testid=\"cn-timeline-followup-open\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Needs an answer') }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<p class=\"cn-timeline-manage__message\">\n\t\t\t\t\t\t{{ entry.message }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<p v-if=\"entry.siblings && entry.siblings.length\"\n\t\t\t\t\t\tclass=\"cn-timeline-manage__siblings\"\n\t\t\t\t\t\tdata-testid=\"cn-timeline-siblings\">\n\t\t\t\t\t\t{{ n('nextcloud-vue', 'Also written on %n other record', 'Also written on %n other records', entry.siblings.length) }}\n\t\t\t\t\t</p>\n\n\t\t\t\t\t<div v-if=\"canManage && !entry.isRead\" class=\"cn-timeline-manage__actions\">\n\t\t\t\t\t\t<NcButton variant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-pin-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"togglePin(entry)\">\n\t\t\t\t\t\t\t{{ entry.pinned ? t('nextcloud-vue', 'Unpin') : t('nextcloud-vue', 'Pin') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton v-for=\"action in actionsFor(entry)\"\n\t\t\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-action-' + action.id + '-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"openAction(action, entry)\">\n\t\t\t\t\t\t\t{{ translate(action.label) }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton v-if=\"entry.followUp === 'open'\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-timeline-followup-' + entry.id\"\n\t\t\t\t\t\t\t@click=\"closeFollowUp(entry)\">\n\t\t\t\t\t\t\t{{ t('nextcloud-vue', 'Mark as answered') }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<div v-if=\"(!loading && !error && allEntries.length > 0) || auditTrailLinkLabel || readsToggleLabel\"\n\t\t\t\tclass=\"cn-timeline-manage__footer\">\n\t\t\t\t<p v-if=\"!loading && !error && allEntries.length > 0\"\n\t\t\t\t\tclass=\"cn-timeline-manage__count\"\n\t\t\t\t\tdata-testid=\"cn-timeline-count\">\n\t\t\t\t\t{{ countLine }}\n\t\t\t\t</p>\n\t\t\t\t<NcCheckboxRadioSwitch v-if=\"readsToggleLabel && readsAllowed\"\n\t\t\t\t\tv-model=\"includeReads\"\n\t\t\t\t\tdata-testid=\"cn-timeline-include-reads\"\n\t\t\t\t\t@update:modelValue=\"load\">\n\t\t\t\t\t{{ readsToggleLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<NcButton v-if=\"auditTrailLinkLabel\"\n\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\tclass=\"cn-timeline-manage__audit-link\"\n\t\t\t\t\tdata-testid=\"cn-timeline-audit-link\"\n\t\t\t\t\t@click=\"openAuditTrail\">\n\t\t\t\t\t{{ auditTrailLinkLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<div v-if=\"composerOnly && canManage && composerEnabled\"\n\t\t\tclass=\"cn-timeline-manage__header\"\n\t\t\tdata-testid=\"cn-timeline-composer-heading\">\n\t\t\t<div class=\"cn-timeline-manage__heading\">\n\t\t\t\t<h3 class=\"cn-timeline-manage__title\">\n\t\t\t\t\t{{ composerTitleText }}\n\t\t\t\t</h3>\n\t\t\t\t<p class=\"cn-timeline-manage__subtitle\">\n\t\t\t\t\t{{ composerSubtitleText }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<NcNoteCard v-if=\"composerOnly && error\" type=\"error\" data-testid=\"cn-timeline-error\">\n\t\t\t{{ error }}\n\t\t</NcNoteCard>\n\n\t\t<form v-if=\"canManage && composerEnabled\"\n\t\t\tref=\"composer\"\n\t\t\tclass=\"cn-timeline-manage__composer\"\n\t\t\tdata-testid=\"cn-timeline-composer\"\n\t\t\t@submit.prevent=\"submit\">\n\t\t\t<template v-if=\"!composerOnly\">\n\t\t\t\t<h3 class=\"cn-timeline-manage__composer-title\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add note') }}\n\t\t\t\t</h3>\n\t\t\t\t<p class=\"cn-timeline-manage__composer-hint\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible') }}\n\t\t\t\t</p>\n\t\t\t</template>\n\t\t\t<label v-if=\"textBlocks.length\" class=\"cn-timeline-manage__draft-label\" :for=\"blockId\">\n\t\t\t\t{{ t('nextcloud-vue', 'Template') }}\n\t\t\t</label>\n\t\t\t<NcSelect v-if=\"textBlocks.length\"\n\t\t\t\tv-model=\"chosenBlock\"\n\t\t\t\tclass=\"cn-timeline-manage__filter\"\n\t\t\t\t:options=\"blockOptions\"\n\t\t\t\t:clearable=\"true\"\n\t\t\t\tlabel=\"label\"\n\t\t\t\t:inputId=\"blockId\"\n\t\t\t\t:labelOutside=\"true\"\n\t\t\t\t:ariaLabelCombobox=\"t('nextcloud-vue', 'Template')\"\n\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Write your own')\"\n\t\t\t\tdata-testid=\"cn-timeline-block\"\n\t\t\t\t@update:modelValue=\"pickBlock\" />\n\n\t\t\t<label class=\"cn-timeline-manage__draft-label\" :for=\"draftId\">\n\t\t\t\t{{ t('nextcloud-vue', 'Note') }}\n\t\t\t</label>\n\t\t\t<textarea :id=\"draftId\"\n\t\t\t\tv-model=\"draft\"\n\t\t\t\tclass=\"cn-timeline-manage__draft\"\n\t\t\t\t:placeholder=\"t('nextcloud-vue', 'Write a note on this record')\"\n\t\t\t\tdata-testid=\"cn-timeline-draft\" />\n\n\t\t\t<div class=\"cn-timeline-manage__composer-foot\">\n\t\t\t\t<NcCheckboxRadioSwitch v-model=\"draftIsPublic\" data-testid=\"cn-timeline-draft-public\">\n\t\t\t\t\t{{ publicLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\n\t\t\t\t<NcButton variant=\"primary\"\n\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t:disabled=\"saving || !draft.trim()\"\n\t\t\t\t\tdata-testid=\"cn-timeline-save\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Add it to the timeline') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</form>\n\n\t\t<component :is=\"activeModal.component\"\n\t\t\tv-if=\"activeModal\"\n\t\t\tv-bind=\"activeModal.props\"\n\t\t\tv-on=\"activeModal.listeners\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'\nimport Download from 'vue-material-design-icons/Download.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { prefixUrl } from '../../utils/headers.js'\n\nlet instanceCount = 0\n\n/** The kind of a read row (`readsToggle`), never a kind OpenRegister declares. */\nconst READ_KIND = 'cn-read'\n\n/** Event-bus channel: a note was added to an object's timeline (`{ objectId, source }`). */\nconst TIMELINE_CHANGED = 'cn:timeline:changed'\n\n/** Event-bus channel: the list's \"Add note\" asks the separate form card to take focus (`{ objectId }`). */\nconst TIMELINE_COMPOSE = 'cn:timeline:compose'\n\n/**\n * Read a dotted path off a value.\n *\n * @param {object} value The value.\n * @param {string} path The path.\n * @return {unknown} What is there, or undefined.\n */\nfunction readPath(value, path) {\n\treturn String(path).split('.').reduce((at, key) => (at === null || at === undefined ? undefined : at[key]), value)\n}\n\nexport default {\n\tname: 'CnTimelineManage',\n\n\tcomponents: { Download, NcButton, NcCheckboxRadioSwitch, NcEmptyContent, NcLoadingIcon, NcNoteCard, NcSelect, Plus },\n\n\tinject: {\n\t\t/** The app's component registry; entry actions resolve their modal here. */\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/** Consumer translation function; manifest labels are authored in English. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's object sidebar channel; the audit trail link opens its tab. */\n\t\tobjectSidebarState: { default: null },\n\t},\n\n\tprops: {\n\t\t/** Register slug. */\n\t\tregister: { type: String, default: '' },\n\t\t/** Schema slug. */\n\t\tschema: { type: String, default: '' },\n\t\t/** The object's id. */\n\t\tobjectId: { type: String, default: '' },\n\t\t/** OpenRegister API base. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/**\n\t\t * The widget content: `{ title?, subtitle?, publicLabel?, exportName?,\n\t\t * composer?, textBlocks?, entryActions?, auditTrailLink? }`.\n\t\t * `composer`: `true` (default) draws the note form under the list,\n\t\t * `false` none, `\"separate\"` none but keeps the \"Add note\" button,\n\t\t * which brings a `\"only\"` instance (the form alone, a card of its own\n\t\t * in a stack) into view; a note saved there reloads the list.\n\t\t * `readsToggle` (`true` or a label) adds a footer switch, off by\n\t\t * default, that puts who read the record (its audit trail's reads)\n\t\t * among the entries; the count then says \"changes only\" while it is off.\n\t\t * `auditTrailLink` (`true` or a label) draws a footer link that opens\n\t\t * the object sidebar on its audit trail tab, `auditTrailTab` when the\n\t\t * page declares that tab under its own id.\n\t\t */\n\t\tcontent: { type: Object, default: () => ({}) },\n\t},\n\n\tdata() {\n\t\tinstanceCount += 1\n\t\treturn {\n\t\t\tuid: instanceCount,\n\t\t\tallEntries: [],\n\t\t\tentries: [],\n\t\t\tkinds: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\tcanManage: false,\n\t\t\tkindFilter: null,\n\t\t\tvisibilityFilter: null,\n\t\t\ttextBlocks: [],\n\t\t\tchosenBlock: null,\n\t\t\tdraft: '',\n\t\t\tdraftIsPublic: false,\n\t\t\tsaving: false,\n\t\t\tactiveModal: null,\n\t\t\t/** Whether the list also shows who read the record (`readsToggle`). */\n\t\t\tincludeReads: false,\n\t\t\t/** Whether OpenRegister gives this reader read entries (`readEventsVisible`); null until asked. */\n\t\t\treadsAllowed: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\ttitleText() {\n\t\t\treturn this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'History')\n\t\t},\n\n\t\tsubtitleText() {\n\t\t\treturn this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'What happened, who did it and when, newest first')\n\t\t},\n\n\t\t/** @return {string} The title of the note form's own card (`composer: \"only\"`): `content.title`, else Add note. */\n\t\tcomposerTitleText() {\n\t\t\treturn this.content.title ? this.translate(this.content.title) : t('nextcloud-vue', 'Add note')\n\t\t},\n\n\t\t/** @return {string} The line under that title: `content.subtitle`, else where the note goes. */\n\t\tcomposerSubtitleText() {\n\t\t\treturn this.content.subtitle ? this.translate(this.content.subtitle) : t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible')\n\t\t},\n\n\t\tpublicLabel() {\n\t\t\treturn this.content.publicLabel ? this.translate(this.content.publicLabel) : t('nextcloud-vue', 'Visible to the requester')\n\t\t},\n\n\t\t/**\n\t\t * The audit trail link's words, or '' when it is not drawn.\n\t\t *\n\t\t * @spec openspec/changes/timeline-audit-trail-link/specs/timeline/spec.md#requirement-the-manage-timeline-can-point-at-the-audit-trail-in-the-sidebar\n\t\t * @return {string}\n\t\t */\n\t\tauditTrailLinkLabel() {\n\t\t\tconst link = this.content.auditTrailLink\n\t\t\tif (!link || !this.objectSidebarState) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof link === 'string' ? this.translate(link) : t('nextcloud-vue', 'Full audit trail in the sidebar')\n\t\t},\n\n\t\t/**\n\t\t * Whether this instance draws the note form: inline (`composer` true or\n\t\t * unset) or as the whole widget (`\"only\"`).\n\t\t *\n\t\t * @spec openspec/changes/timeline-composer-card/specs/timeline/spec.md#requirement-the-note-form-can-be-its-own-card\n\t\t * @return {boolean}\n\t\t */\n\t\tcomposerEnabled() {\n\t\t\treturn this.content.composer !== false && this.content.composer !== 'separate'\n\t\t},\n\n\t\t/**\n\t\t * The words of the \"also who read it\" switch, or '' when the widget\n\t\t * does not offer it (`readsToggle`: true or a label).\n\t\t *\n\t\t * @spec openspec/changes/timeline-reads-toggle/specs/timeline/spec.md#requirement-the-history-can-show-who-read-the-record\n\t\t * @return {string}\n\t\t */\n\t\treadsToggleLabel() {\n\t\t\tconst toggle = this.content.readsToggle\n\t\t\tif (!toggle) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof toggle === 'string' ? this.translate(toggle) : t('nextcloud-vue', 'Also who read it')\n\t\t},\n\n\t\t/**\n\t\t * \"3 of 10 events\", and \", changes only\" while reads can be shown but are not.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tcountLine() {\n\t\t\tconst values = { shown: this.entries.length, total: this.allEntries.length }\n\t\t\treturn this.readsToggleLabel && this.readsAllowed && !this.includeReads\n\t\t\t\t? t('nextcloud-vue', '{shown} of {total} events, changes only', values)\n\t\t\t\t: t('nextcloud-vue', '{shown} of {total} events', values)\n\t\t},\n\n\t\t/**\n\t\t * Whether the visibility filter shows its \"Visibility\" label. With\n\t\t * `visibilityLabel: false` it keeps only its accessible name and the\n\t\t * select reads \"Internal and public\" on its own (board DqZaakHistorie).\n\t\t *\n\t\t * @spec openspec/changes/timeline-visibility-filter-unlabelled/specs/timeline/spec.md#requirement-the-visibility-filter-can-go-without-a-visible-label\n\t\t * @return {boolean}\n\t\t */\n\t\tvisibilityLabelShown() {\n\t\t\treturn this.content.visibilityLabel !== false\n\t\t},\n\n\t\t/** @return {boolean} The widget is the note form alone (`composer: \"only\"`), a card of its own beside the list. */\n\t\tcomposerOnly() {\n\t\t\treturn this.content.composer === 'only'\n\t\t},\n\n\t\t/** @return {boolean} The list draws no form; its \"Add note\" brings the separate form card into view (`composer: \"separate\"`). */\n\t\tcomposerSeparate() {\n\t\t\treturn this.content.composer === 'separate'\n\t\t},\n\n\t\tdraftId() {\n\t\t\treturn `cn-timeline-draft-${this.uid}`\n\t\t},\n\n\t\tblockId() {\n\t\t\treturn `cn-timeline-block-${this.uid}`\n\t\t},\n\n\t\t/**\n\t\t * The kind options, the plain note first. A note is an entry with NO\n\t\t * kind, so \"Note\" filters on its absence.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tkindOptions() {\n\t\t\tconst options = [{ id: '', label: t('nextcloud-vue', 'Note') }].concat(this.kinds.map((kind) => ({ id: kind.slug, label: kind.title || kind.slug })))\n\t\t\treturn this.includeReads ? options.concat([{ id: READ_KIND, label: t('nextcloud-vue', 'Viewed') }]) : options\n\t\t},\n\n\t\t/**\n\t\t * All, then every kind the object has entries of, each with its count.\n\t\t *\n\t\t * @spec openspec/changes/timeline-all-chip-host-label/specs/timeline/spec.md#requirement-the-timelines-all-chip-reads-the-hosts-word\n\t\t * @return {Array<object>} The chips.\n\t\t */\n\t\tkindChips() {\n\t\t\tconst chosen = this.kindFilter ? (this.kindFilter.id || '') : null\n\t\t\tconst counts = new Map()\n\t\t\tfor (const entry of this.allEntries) {\n\t\t\t\tconst kind = entry.kind || ''\n\t\t\t\tcounts.set(kind, (counts.get(kind) || 0) + 1)\n\t\t\t}\n\t\t\t// The host's word first, as its own filter chips read (\"Alles\"), then the library's.\n\t\t\tconst hostAll = typeof this.cnTranslate === 'function' ? this.cnTranslate('All') : 'All'\n\t\t\tconst allLabel = typeof hostAll === 'string' && hostAll !== '' && hostAll !== 'All' ? hostAll : t('nextcloud-vue', 'All')\n\t\t\tconst chips = [{ key: 'all', id: null, label: allLabel, count: this.allEntries.length, active: chosen === null }]\n\t\t\tfor (const option of this.kindOptions) {\n\t\t\t\tconst count = counts.get(option.id) || 0\n\t\t\t\tif (count > 0) {\n\t\t\t\t\tchips.push({ key: option.id || 'note', id: option.id, label: option.label, count, active: chosen === option.id })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn chips\n\t\t},\n\n\t\tblockOptions() {\n\t\t\treturn this.textBlocks.map((block) => ({ id: block.slug, label: block.title || block.slug }))\n\t\t},\n\n\t\tvisibilityOptions() {\n\t\t\treturn [\n\t\t\t\t{ id: 'internal', label: t('nextcloud-vue', 'Internal only') },\n\t\t\t\t{ id: 'public', label: this.publicLabel },\n\t\t\t]\n\t\t},\n\n\t\tbase() {\n\t\t\treturn this.apiBase.replace(/\\/$/, '')\n\t\t},\n\n\t\ttimelineUrl() {\n\t\t\tif (!this.objectId || !this.register || !this.schema) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn prefixUrl(`${this.base}/objects/${this.register}/${this.schema}/${this.objectId}/timeline`)\n\t\t},\n\n\t\tentryActions() {\n\t\t\treturn Array.isArray(this.content.entryActions) ? this.content.entryActions.filter((action) => action && action.id) : []\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: 'load',\n\t},\n\n\tcreated() {\n\t\tthis.onTimelineChanged = (payload) => {\n\t\t\tif (!payload || payload.objectId !== this.objectId || payload.source === this.uid || this.composerOnly) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.load()\n\t\t}\n\t\tthis.onComposeRequested = (payload) => {\n\t\t\tif (this.composerOnly && payload && payload.objectId === this.objectId) {\n\t\t\t\tthis.focusComposer()\n\t\t\t}\n\t\t}\n\t\tsubscribe(TIMELINE_CHANGED, this.onTimelineChanged)\n\t\tsubscribe(TIMELINE_COMPOSE, this.onComposeRequested)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(TIMELINE_CHANGED, this.onTimelineChanged)\n\t\tunsubscribe(TIMELINE_COMPOSE, this.onComposeRequested)\n\t},\n\n\tmounted() {\n\t\tthis.loadKinds()\n\t\tif (this.content.textBlocks !== false) {\n\t\t\tthis.loadTextBlocks()\n\t\t}\n\t\tthis.load()\n\t},\n\n\tmethods: {\n\t\tt,\n\t\tn,\n\n\t\ttranslate(text) {\n\t\t\treturn text ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/** Read the declared kinds, so the filter names them as administered. */\n\t\tasync loadKinds() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(prefixUrl(`${this.base}/timeline/kinds`))\n\t\t\t\tthis.kinds = response.data?.results || []\n\t\t\t} catch {\n\t\t\t\tthis.kinds = []\n\t\t\t}\n\t\t},\n\n\t\t/** Read the seeded standard notes. */\n\t\tasync loadTextBlocks() {\n\t\t\ttry {\n\t\t\t\tconst response = await axios.get(prefixUrl(`${this.base}/timeline/text-blocks`))\n\t\t\t\tthis.textBlocks = response.data?.results || []\n\t\t\t} catch {\n\t\t\t\tthis.textBlocks = []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Note which standard note was picked. The BLOCK is sent, not its body:\n\t\t * OpenRegister fills the object's own values into it on the way in.\n\t\t *\n\t\t * @param {object|null} block The picked option.\n\t\t */\n\t\tpickBlock(block) {\n\t\t\tthis.chosenBlock = block || null\n\t\t\tif (block) {\n\t\t\t\tconst declared = this.textBlocks.find((entry) => entry.slug === block.id)\n\t\t\t\tthis.draft = declared?.title || block.label\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write the drafted note onto this object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync submit() {\n\t\t\tif (!this.draft.trim() || !this.timelineUrl) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.saving = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst payload = { visibility: this.draftIsPublic ? 'public' : 'internal' }\n\t\t\t\tif (this.chosenBlock?.id) {\n\t\t\t\t\tpayload.textBlock = this.chosenBlock.id\n\t\t\t\t} else {\n\t\t\t\t\tpayload.message = this.draft\n\t\t\t\t}\n\t\t\t\tawait axios.post(this.timelineUrl, payload)\n\t\t\t\tthis.draft = ''\n\t\t\t\tthis.chosenBlock = null\n\t\t\t\tthis.draftIsPublic = false\n\t\t\t\temit(TIMELINE_CHANGED, { objectId: this.objectId, source: this.uid })\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'That note could not be saved.')\n\t\t\t} finally {\n\t\t\t\tthis.saving = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the timeline with the visibility filter, if one is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tif (!this.timelineUrl) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst params = {}\n\t\t\t\tif (this.visibilityFilter?.id) {\n\t\t\t\t\tparams.visibility = this.visibilityFilter.id\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(this.timelineUrl, { params })\n\t\t\t\tthis.canManage = response.data?.canManage === true\n\t\t\t\tconst entries = response.data?.results || []\n\t\t\t\tif (this.readsToggleLabel && this.readsAllowed === null) {\n\t\t\t\t\tawait this.readEntries()\n\t\t\t\t}\n\t\t\t\tthis.allEntries = this.readsToggleLabel && this.readsAllowed && this.includeReads ? this.withReads(entries, await this.readEntries()) : entries\n\t\t\t\tthis.entries = this.applyKindFilter(this.allEntries)\n\t\t\t} catch {\n\t\t\t\tthis.allEntries = []\n\t\t\t\tthis.entries = []\n\t\t\t\tthis.error = t('nextcloud-vue', 'The timeline could not be read. Try again, or ask an administrator to check OpenRegister.')\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 * Who read the record, from its audit trail (`action=read`), as timeline\n\t\t * rows. OpenRegister gives read entries only to the groups named for\n\t\t * them and says so in `readEventsVisible`, which decides whether the\n\t\t * switch is offered at all; a read it refuses or that fails leaves the\n\t\t * reads out and offers no switch.\n\t\t *\n\t\t * @spec openspec/changes/timeline-reads-toggle/specs/timeline/spec.md#requirement-the-history-can-show-who-read-the-record\n\t\t * @return {Promise<Array<object>>} The read rows.\n\t\t */\n\t\tasync readEntries() {\n\t\t\ttry {\n\t\t\t\tconst url = prefixUrl(`${this.base}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trails`)\n\t\t\t\tconst response = await axios.get(url, { params: { action: 'read', limit: 50 } })\n\t\t\t\tthis.readsAllowed = response.data?.readEventsVisible === true\n\t\t\t\tif (!this.readsAllowed) {\n\t\t\t\t\treturn []\n\t\t\t\t}\n\t\t\t\tconst list = response.data?.results || []\n\t\t\t\treturn list\n\t\t\t\t\t.filter((entry) => String(entry.action || '').toLowerCase() === 'read')\n\t\t\t\t\t.map((entry) => ({\n\t\t\t\t\t\tid: `read-${entry.id}`,\n\t\t\t\t\t\tkind: READ_KIND,\n\t\t\t\t\t\tisRead: true,\n\t\t\t\t\t\tauthor: entry.actorDisplayName || entry.userName || entry.user || '',\n\t\t\t\t\t\tcreated: entry.created || entry.creationDateTime || '',\n\t\t\t\t\t\tmessage: t('nextcloud-vue', 'Opened the record'),\n\t\t\t\t\t}))\n\t\t\t} catch {\n\t\t\t\tthis.readsAllowed = false\n\t\t\t\treturn []\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The timeline's entries with the reads among them, newest first; the\n\t\t * pinned entries stay on top.\n\t\t *\n\t\t * @param {Array<object>} entries The timeline's entries.\n\t\t * @param {Array<object>} reads The read rows.\n\t\t * @return {Array<object>} The merged list.\n\t\t */\n\t\twithReads(entries, reads) {\n\t\t\tconst time = (entry) => new Date(entry.created || 0).getTime() || 0\n\t\t\tconst pinned = entries.filter((entry) => entry.pinned)\n\t\t\tconst rest = [...entries.filter((entry) => !entry.pinned), ...reads].sort((a, b) => time(b) - time(a))\n\t\t\treturn [...pinned, ...rest]\n\t\t},\n\n\t\t/**\n\t\t * Show one kind, or every kind for `null`, without reading again.\n\t\t *\n\t\t * @param {string|null} id The kind slug, '' for a note, null for all.\n\t\t */\n\t\tchooseKind(id) {\n\t\t\tthis.kindFilter = id === null ? null : { id }\n\t\t\tthis.entries = this.applyKindFilter(this.allEntries)\n\t\t},\n\n\t\t/**\n\t\t * The kind filter is applied in the reading: the list endpoint does not\n\t\t * read a kind parameter, so sending one would be dropped in silence.\n\t\t *\n\t\t * @param {Array<object>} results The entries as read.\n\t\t * @return {Array<object>} The entries to show.\n\t\t */\n\t\tapplyKindFilter(results) {\n\t\t\tif (!this.kindFilter) {\n\t\t\t\treturn results\n\t\t\t}\n\t\t\tconst wanted = this.kindFilter.id || ''\n\t\t\treturn results.filter((entry) => (entry.kind || '') === wanted)\n\t\t},\n\n\t\t/**\n\t\t * The shown entries as CSV: when, kind, who, visibility and what.\n\t\t *\n\t\t * @return {string} The CSV text.\n\t\t */\n\t\tentriesCsv() {\n\t\t\tconst cell = (value) => `\"${String(value ?? '').replace(/\"/g, '\"\"')}\"`\n\t\t\tconst rows = [[t('nextcloud-vue', 'When'), t('nextcloud-vue', 'Kind'), t('nextcloud-vue', 'Who'), t('nextcloud-vue', 'Visibility'), t('nextcloud-vue', 'Message')]]\n\t\t\tfor (const entry of this.entries) {\n\t\t\t\trows.push([entry.created, this.kindLabel(entry), entry.author, entry.visibility, entry.message])\n\t\t\t}\n\t\t\treturn rows.map((row) => row.map(cell).join(',')).join('\\r\\n')\n\t\t},\n\n\t\t/** Download the shown entries as a CSV file. */\n\t\texportEntries() {\n\t\t\tconst blob = new Blob([this.entriesCsv()], { type: 'text/csv;charset=utf-8' })\n\t\t\tconst url = URL.createObjectURL(blob)\n\t\t\tconst link = document.createElement('a')\n\t\t\tlink.href = url\n\t\t\tlink.download = `${this.content.exportName || 'history'}-${this.objectId || 'record'}.csv`\n\t\t\tdocument.body.appendChild(link)\n\t\t\tlink.click()\n\t\t\tlink.remove()\n\t\t\tURL.revokeObjectURL(url)\n\t\t},\n\n\t\t/**\n\t\t * Open the object sidebar on its audit trail tab.\n\t\t *\n\t\t * @spec openspec/changes/timeline-audit-trail-link/specs/timeline/spec.md#requirement-the-manage-timeline-can-point-at-the-audit-trail-in-the-sidebar\n\t\t */\n\t\topenAuditTrail() {\n\t\t\tthis.objectSidebarState.open = true\n\t\t\tthis.objectSidebarState.requestedTab = this.content.auditTrailTab || 'auditTrail'\n\t\t},\n\n\t\t/** Bring the composer into view and focus its text; a separate form card is asked to do so. */\n\t\tfocusComposer() {\n\t\t\tif (this.composerSeparate) {\n\t\t\t\temit(TIMELINE_COMPOSE, { objectId: this.objectId })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst form = this.$refs.composer\n\t\t\tif (!form) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tform.scrollIntoView?.({ behavior: 'smooth', block: 'center' })\n\t\t\tform.querySelector('textarea')?.focus()\n\t\t},\n\n\t\t/**\n\t\t * The label of one entry's kind, falling back to \"Note\".\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {string} The label.\n\t\t */\n\t\tkindLabel(entry) {\n\t\t\tif (entry.kind === READ_KIND) {\n\t\t\t\treturn t('nextcloud-vue', 'Viewed')\n\t\t\t}\n\t\t\tif (!entry.kind) {\n\t\t\t\treturn t('nextcloud-vue', 'Note')\n\t\t\t}\n\t\t\tconst declared = this.kinds.find((kind) => kind.slug === entry.kind)\n\t\t\treturn declared?.title || entry.kind\n\t\t},\n\n\t\t/**\n\t\t * One moment in the reader's locale.\n\t\t *\n\t\t * @param {string} value An ISO 8601 stamp.\n\t\t * @return {string} The moment, or the raw value.\n\t\t */\n\t\tmoment(value) {\n\t\t\tif (!value) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst parsed = new Date(value)\n\t\t\treturn isNaN(parsed.getTime()) ? value : parsed.toLocaleString()\n\t\t},\n\n\t\tasync togglePin(entry) {\n\t\t\tawait this.patch(entry, { pinned: !entry.pinned })\n\t\t},\n\n\t\tasync closeFollowUp(entry) {\n\t\t\tawait this.patch(entry, { followUp: 'done' })\n\t\t},\n\n\t\t/**\n\t\t * Write one change onto one entry and read the timeline back (pinning\n\t\t * changes the order, so patching in place would look like nothing).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {object} changes The fields to write.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync patch(entry, changes) {\n\t\t\ttry {\n\t\t\t\tawait axios.patch(`${this.timelineUrl}/${entry.id}`, changes)\n\t\t\t\tawait this.load()\n\t\t\t} catch {\n\t\t\t\tthis.error = t('nextcloud-vue', 'That change could not be saved.')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The configured entry actions that apply to one entry (`kinds` limits\n\t\t * an action to entries of those kinds).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {Array<object>} The actions.\n\t\t */\n\t\tactionsFor(entry) {\n\t\t\treturn this.entryActions.filter((action) => !Array.isArray(action.kinds) || action.kinds.includes(entry.kind || ''))\n\t\t},\n\n\t\t/**\n\t\t * Resolve `@objectId` and `@row.<path>` tokens in an action's props.\n\t\t *\n\t\t * @param {object} props The props as declared.\n\t\t * @param {object} entry The entry.\n\t\t * @return {object} The resolved props.\n\t\t */\n\t\tresolveProps(props, entry) {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(props || {})) {\n\t\t\t\tif (value === '@objectId') {\n\t\t\t\t\tout[key] = this.objectId\n\t\t\t\t} else if (typeof value === 'string' && value.startsWith('@row.')) {\n\t\t\t\t\tconst found = readPath(entry, value.slice('@row.'.length))\n\t\t\t\t\tout[key] = found === undefined || found === null ? '' : found\n\t\t\t\t} else {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Open an entry action's modal from the app's registry. On its\n\t\t * `doneEvent` (default `done`) the entry's open follow-up is closed when\n\t\t * the action says so; otherwise the timeline is read again.\n\t\t *\n\t\t * @param {object} action The action.\n\t\t * @param {object} entry The entry.\n\t\t */\n\t\topenAction(action, entry) {\n\t\t\tconst registered = (this.cnRegistry || {})[action.modal]\n\t\t\tconst component = registered && (registered.component || registered)\n\t\t\tif (!component) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnTimelineWidget] entry action \"${action.id}\" names modal \"${action.modal}\", which the registry does not hold.`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst close = () => {\n\t\t\t\tthis.activeModal = null\n\t\t\t}\n\t\t\tconst done = async () => {\n\t\t\t\tthis.activeModal = null\n\t\t\t\tif (action.closesFollowUp === true && entry.followUp === 'open') {\n\t\t\t\t\tawait this.closeFollowUp(entry)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tawait this.load()\n\t\t\t}\n\t\t\tthis.activeModal = {\n\t\t\t\tcomponent,\n\t\t\t\tprops: this.resolveProps(action.props, entry),\n\t\t\t\tlisteners: { [action.doneEvent || 'done']: done, close },\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-timeline-manage__header {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px;\n\tmargin-block-end: 16px;\n}\n\n.cn-timeline-manage__heading {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-timeline-manage__header-actions {\n\tdisplay: flex;\n\tgap: 8px;\n}\n\n.cn-timeline-manage__title {\n\tmargin: 0;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n}\n\n.cn-timeline-manage__subtitle {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 14px;\n}\n\n.cn-timeline-manage__chips {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n\tmargin-block-end: 12px;\n}\n\n.cn-timeline-manage__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tbox-sizing: border-box;\n\theight: 36px;\n\tpadding: 0 12px;\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 18px;\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcursor: pointer;\n}\n\n/* The chosen kind is a filled chip with no outline. */\n.cn-timeline-manage__chip--active {\n\tborder: 0;\n\tbackground: var(--color-main-text);\n\tcolor: var(--color-main-background);\n}\n\n.cn-timeline-manage__chip-count {\n\tpadding: 1px 7px;\n\tborder-radius: 10px;\n\tbackground: var(--color-border);\n\tcolor: var(--color-main-text);\n\tfont-size: 12px;\n}\n\n/* On the filled chip the count is a lighter ink bubble with light text. */\n.cn-timeline-manage__chip--active .cn-timeline-manage__chip-count {\n\tbackground: color-mix(in srgb, var(--color-main-background) 18%, var(--color-main-text));\n\tcolor: var(--color-main-background);\n}\n\n.cn-timeline-manage__footer {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n\tmargin: 12px 0 0;\n}\n\n.cn-timeline-manage__count {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-manage__composer {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin-block-start: 24px;\n\tpadding-block-start: 16px;\n\tborder-block-start: 1px solid var(--color-border);\n}\n\n.cn-timeline-manage--composer-only .cn-timeline-manage__composer {\n\tmargin-block-start: 0;\n\tpadding-block-start: 0;\n\tborder-block-start: none;\n}\n\n/* The checkbox at the start, the button at the end, as one row. */\n.cn-timeline-manage__composer-foot {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-timeline-manage__composer-title {\n\tmargin: 0;\n\tfont-size: 16px;\n\tfont-weight: bold;\n}\n\n.cn-timeline-manage__composer-hint {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-timeline-manage__draft-label {\n\tfont-weight: bold;\n}\n\n.cn-timeline-manage__draft {\n\twidth: 100%;\n\tmin-height: 70px;\n}\n\n.cn-timeline-manage__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\tflex-wrap: wrap;\n\tmargin-block-end: 12px;\n}\n\n.cn-timeline-manage__filter {\n\tmin-width: 200px;\n}\n\n.cn-timeline-manage__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-timeline-manage__entry {\n\tborder-block-end: 1px solid var(--color-border);\n\tpadding-block: 12px;\n}\n\n.cn-timeline-manage__entry--pinned {\n\tborder-inline-start: 3px solid var(--color-primary-element);\n\tpadding-inline-start: 8px;\n}\n\n.cn-timeline-manage__head {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-timeline-manage__kind {\n\tfont-weight: bold;\n\tcolor: var(--color-main-text);\n}\n\n.cn-timeline-manage__badge {\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius);\n\tpadding: 0 6px;\n}\n\n.cn-timeline-manage__badge--open {\n\tborder-color: var(--color-warning);\n\tcolor: var(--color-warning-text);\n}\n\n.cn-timeline-manage__message {\n\tmargin-block: 4px 0;\n\twhite-space: pre-wrap;\n}\n\n.cn-timeline-manage__siblings {\n\tmargin-block: 4px 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-timeline-manage__actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_Fragment","_createElementVNode","_toDisplayString","_createBlock","_createVNode","_createTextVNode","_openBlock","_renderList"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnContactsTab.vue2.js';
2
2
  import { render } from './CnContactsTab.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-ec6b0008";
5
+ script.__scopeId = "data-v-a0d90184";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,5 +1,5 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
- import { NcAvatar, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue';
2
+ import { NcActionButton, NcActions, NcAvatar, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue';
3
3
  import AccountMultipleOutlineIcon from 'vue-material-design-icons/AccountMultipleOutline.vue';
4
4
  import AccountPlus from 'vue-material-design-icons/AccountPlus.vue';
5
5
  import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue';
@@ -14,6 +14,7 @@ import __sfc_main$1 from '../../../components/CnContactPicker/CnContactPicker.vu
14
14
  import __sfc_main from '../../../components/CnStatusBadge/CnStatusBadge.vue.js';
15
15
  import __sfc_main$3 from '../../../components/CnWidgetWrapper/CnWidgetHeaderActionButtons.vue.js';
16
16
  import { normalizeLook } from '../../../composables/useLook.js';
17
+ import { formatBoardDate } from '../../../utils/boardDate.js';
17
18
  import { WIDGET_BODY_ACTIONS } from '../../../utils/widgetHeaderActions.js';
18
19
  import { prefixUrl, buildHeaders } from '../../../utils/headers.js';
19
20
 
@@ -44,6 +45,8 @@ var script = {
44
45
  name: 'CnContactsTab',
45
46
 
46
47
  components: {
48
+ NcActionButton,
49
+ NcActions,
47
50
  NcAvatar,
48
51
  NcButton,
49
52
  NcEmptyContent,
@@ -134,6 +137,11 @@ var script = {
134
137
  },
135
138
 
136
139
  computed: {
140
+ /** @return {boolean} The board look draws the rows as DqZaakBetrokkenen does. */
141
+ isBoard() {
142
+ return normalizeLook(this.cnLook) === 'board'
143
+ },
144
+
137
145
  /**
138
146
  * The two header buttons in the order and weight of the look: the
139
147
  * board look draws Link contact then New contact, both outlined; the
@@ -426,6 +434,49 @@ var script = {
426
434
  *
427
435
  * @return {string} The window.
428
436
  */
437
+ /**
438
+ * A person's details on one line for the board look: e-mail, phone,
439
+ * organisation and note, separated by middots.
440
+ *
441
+ * @param {object} contact The contact link.
442
+ * @return {string}
443
+ */
444
+ boardMetaOf(contact) {
445
+ return [contact?.email, contact?.phone, contact?.org, contact?.note].filter((part) => typeof part === 'string' && part !== '').join(' · ')
446
+ },
447
+
448
+ /**
449
+ * The validity window in the board's short dates: "Vanaf 3 okt",
450
+ * "Tot 31 okt" or "3 okt – 31 okt".
451
+ *
452
+ * @param {object} contact The contact link.
453
+ * @return {string}
454
+ */
455
+ boardPeriodOf(contact) {
456
+ const from = formatBoardDate(contact?.validFrom || '');
457
+ const until = formatBoardDate(contact?.validUntil || '');
458
+ if (from === '' && until === '') {
459
+ return ''
460
+ }
461
+ if (until === '') {
462
+ return translate('nextcloud-vue', 'From {date}', { date: from })
463
+ }
464
+ if (from === '') {
465
+ return translate('nextcloud-vue', 'Until {date}', { date: until })
466
+ }
467
+ return `${from} – ${until}`
468
+ },
469
+
470
+ /**
471
+ * The label of a person's actions menu.
472
+ *
473
+ * @param {object} contact The contact link.
474
+ * @return {string}
475
+ */
476
+ moreActionsLabel(contact) {
477
+ return translate('nextcloud-vue', 'More actions for {name}', { name: contact?.displayName || this.unknownLabel })
478
+ },
479
+
429
480
  periodOf(contact) {
430
481
  const from = contact?.validFrom || '';
431
482
  const until = contact?.validUntil || '';
@@ -1 +1 @@
1
- {"version":3,"file":"CnContactsTab.vue2.js","sources":["../../../../../src/integrations/builtin/contacts/CnContactsTab.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n -\n - CnContactsTab — bespoke sidebar tab for the `contacts` integration leaf.\n -\n - Renders the people linked to an OR object grouped by role. Since\n - people-on-objects a person is a Nextcloud user or a vCard contact, and\n - the grouping is the server's when it sends one: the listing carries\n - `byRole` and the schema's `roles`, and the tab falls back to its own\n - buckets (Applicants / Handlers / Advisors / Other, AD-1 of\n - `openspec/changes/integration-contacts/design.md`) for a server that\n - does not. Each card shows avatar, display name, email and role, and a\n - link whose validity has passed reads as inactive.\n -\n - Clicking a contact opens the reverse-lookup flyout (AD-3) listing every\n - OR object linked to that vCard.\n -\n - Tier-2 (this revision): the tab now hosts two header actions — \"Link\n - existing contact\" (opens `CnContactPicker`) and \"Add new contact\"\n - (opens `CnContactCreate`). Both dialogs are loaded lazily via\n - dynamic import to keep the bundle below the integration-tab\n - threshold, and both delegate the actual API call back to this tab\n - so the loading + refresh handshake stays here.\n -\n - Backed by:\n - GET /api/objects/{register}/{schema}/{id}/contacts\n - POST /api/objects/{register}/{schema}/{id}/contacts — link existing\n - POST /api/objects/{register}/{schema}/{id}/contacts/new — create + link\n - DELETE /api/objects/{register}/{schema}/{id}/contacts/{uid} — unlink\n -\n - The payload shape mirrors `ContactLink::jsonSerialize()` —\n - `{id, contactUid, displayName, email, phone, org, avatarUrl, role,\n - addressbookId, contactUri, schemaId, metadata, linkedAt}`.\n -\n - @spec openspec/changes/integration-contacts-tier2/specs/integrations/contacts/spec.md\n -->\n<template>\n\t<div class=\"cn-contacts-tab\">\n\t\t<!-- Header actions: link existing / create new. The board look draws\n\t\t Link contact first and both outlined (DqZaakBetrokkenen); the\n\t\t Nextcloud look keeps Add new contact first and Link as the primary. -->\n\t\t<div class=\"cn-contacts-tab__header\">\n\t\t\t<NcButton\n\t\t\t\tv-for=\"button in headerButtons\"\n\t\t\t\t:key=\"button.key\"\n\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t:aria-label=\"button.label\"\n\t\t\t\t@click=\"button.key === 'link' ? (showLinkDialog = true) : (showCreateDialog = true)\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant v-if=\"button.key === 'link'\" :size=\"20\" />\n\t\t\t\t\t<AccountPlus v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ button.label }}\n\t\t\t</NcButton>\n\t\t\t<CnWidgetHeaderActionButtons v-if=\"bodyActions.length > 0\" :actions=\"bodyActions\" />\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"error\"\n\t\t\t:name=\"errorLabel\"\n\t\t\t:description=\"error\">\n\t\t\t<template #icon>\n\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<!-- Empty -->\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"contactsArray.length === 0\"\n\t\t\t:name=\"emptyTitleLabel\"\n\t\t\t:description=\"emptyDescriptionLabel\">\n\t\t\t<template #icon>\n\t\t\t\t<AccountMultipleOutline :size=\"48\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<!-- Grouped list -->\n\t\t<div v-else class=\"cn-contacts-tab__groups\">\n\t\t\t<section\n\t\t\t\tv-for=\"group in groupedContacts\"\n\t\t\t\t:key=\"group.key\"\n\t\t\t\tclass=\"cn-contacts-tab__group\">\n\t\t\t\t<h3 class=\"cn-contacts-tab__group-title\">\n\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t<span class=\"cn-contacts-tab__group-count\">{{ group.items.length }}</span>\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-contacts-tab__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"contact in group.items\"\n\t\t\t\t\t\t:key=\"contact.id\"\n\t\t\t\t\t\tclass=\"cn-contacts-tab__item\"\n\t\t\t\t\t\t@click=\"openReverseLookup(contact)\">\n\t\t\t\t\t\t<div class=\"cn-contacts-tab__avatar\">\n\t\t\t\t\t\t\t<NcAvatar\n\t\t\t\t\t\t\t\t:size=\"40\"\n\t\t\t\t\t\t\t\t:displayName=\"contact.displayName || unknownLabel\"\n\t\t\t\t\t\t\t\t:user=\"avatarSeed(contact)\"\n\t\t\t\t\t\t\t\t:url=\"contact.avatarUrl || undefined\"\n\t\t\t\t\t\t\t\t:isNoUser=\"true\"\n\t\t\t\t\t\t\t\t:disableMenu=\"true\"\n\t\t\t\t\t\t\t\t:disableTooltip=\"true\"\n\t\t\t\t\t\t\t\thideStatus />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"cn-contacts-tab__details\">\n\t\t\t\t\t\t\t<div class=\"cn-contacts-tab__name-row\">\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__name\">\n\t\t\t\t\t\t\t\t\t{{ contact.displayName || unknownLabel }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\t\tv-if=\"contact.role\"\n\t\t\t\t\t\t\t\t\t:label=\"roleLabel(contact)\"\n\t\t\t\t\t\t\t\t\t:variant=\"roleVariant(contact)\"\n\t\t\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\t\tv-if=\"contact.active === false\"\n\t\t\t\t\t\t\t\t\t:label=\"inactiveLabel\"\n\t\t\t\t\t\t\t\t\tvariant=\"default\"\n\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-inactive\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div v-if=\"contact.email\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t<Email :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.email }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div v-if=\"contact.phone\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t<Phone :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.phone }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div v-if=\"contact.org\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t<OfficeBuildingOutline :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.org }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tv-if=\"periodOf(contact)\"\n\t\t\t\t\t\t\t\tclass=\"cn-contacts-tab__sub\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-period\">\n\t\t\t\t\t\t\t\t<CalendarRange :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ periodOf(contact) }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div v-if=\"contact.note\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.note }}</span>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-contacts-tab__unlink\"\n\t\t\t\t\t\t\t:aria-label=\"unlinkLabel\"\n\t\t\t\t\t\t\t:title=\"unlinkLabel\"\n\t\t\t\t\t\t\t@click.stop=\"unlink(contact)\">\n\t\t\t\t\t\t\t<Close :size=\"18\" />\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</div>\n\n\t\t<!-- Link existing contact dialog -->\n\t\t<CnContactPicker\n\t\t\tv-if=\"showLinkDialog\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\tv-bind=\"pickerRoleOptions\"\n\t\t\t@link=\"onPickerLink\"\n\t\t\t@close=\"showLinkDialog = false\" />\n\n\t\t<!-- Create new contact dialog -->\n\t\t<CnContactCreate\n\t\t\tv-if=\"showCreateDialog\"\n\t\t\t:loading=\"createLoading\"\n\t\t\t@create=\"onCreateSubmit\"\n\t\t\t@close=\"showCreateDialog = false\" />\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcAvatar, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport AccountMultipleOutline from 'vue-material-design-icons/AccountMultipleOutline.vue'\nimport AccountPlus from 'vue-material-design-icons/AccountPlus.vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport CalendarRange from 'vue-material-design-icons/CalendarRange.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Email from 'vue-material-design-icons/Email.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport OfficeBuildingOutline from 'vue-material-design-icons/OfficeBuildingOutline.vue'\nimport Phone from 'vue-material-design-icons/Phone.vue'\nimport CnContactCreate from '../../../components/CnContactCreate/CnContactCreate.vue'\nimport CnContactPicker from '../../../components/CnContactPicker/CnContactPicker.vue'\nimport CnStatusBadge from '../../../components/CnStatusBadge/CnStatusBadge.vue'\nimport CnWidgetHeaderActionButtons from '../../../components/CnWidgetWrapper/CnWidgetHeaderActionButtons.vue'\nimport { normalizeLook } from '../../../composables/useLook.js'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\nimport { WIDGET_BODY_ACTIONS } from '../../../utils/widgetHeaderActions.js'\n\n/**\n * Normalised role keys for grouping. Free-text vCard roles are\n * normalised by lowercasing + trimming; unknown roles fall into `other`.\n *\n * @type {Array<{key: string, match: (r: ?string) => boolean, labelKey: string}>}\n */\nconst ROLE_BUCKETS = [\n\t{ key: 'applicant', match: (r) => /^(applicant|aanvrager|requestor)$/i.test(r ?? ''), labelKey: 'Applicants' },\n\t{ key: 'handler', match: (r) => /^(handler|behandelaar|owner|assignee)$/i.test(r ?? ''), labelKey: 'Handlers' },\n\t{ key: 'advisor', match: (r) => /^(advisor|advisor|adviseur|reviewer)$/i.test(r ?? ''), labelKey: 'Advisors' },\n]\n\n/**\n * CnContactsTab — vCard contacts linked to an OR object, grouped by role.\n *\n * Basic usage\n * ```vue\n * <CnContactsTab\n * register=\"my-register\"\n * schema=\"case\"\n * object-id=\"abc-123\" />\n * ```\n */\nexport default {\n\tname: 'CnContactsTab',\n\n\tcomponents: {\n\t\tNcAvatar,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tAccountPlus,\n\t\tAccountMultipleOutline,\n\t\tAlertCircleOutline,\n\t\tCalendarRange,\n\t\tClose,\n\t\tEmail,\n\t\tLinkVariant,\n\t\tOfficeBuildingOutline,\n\t\tPhone,\n\t\tCnStatusBadge,\n\t\tCnContactPicker,\n\t\tCnContactCreate,\n\t\tCnWidgetHeaderActionButtons,\n\t},\n\n\tinject: {\n\t\t/** The look the app is drawn in (`board` or `nextcloud`), from CnAppRoot or the page. */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/** The widget's manifest `bodyActions`, drawn after this tab's own buttons. */\n\t\tcnWidgetBodyActions: { from: WIDGET_BODY_ACTIONS, default: null },\n\t},\n\n\tprops: {\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL for OpenRegister. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\n\t\t// --- Pre-translated labels (consumer-overridable for i18n flexibility) ---\n\t\t/** Label for the button that links an existing contact. */\n\t\tlinkExistingLabel: { type: String, default: () => t('nextcloud-vue', 'Link contact') },\n\t\t/** Label for the button that creates a contact; empty takes the look's own (New contact, or Add new contact). */\n\t\taddNewLabel: { type: String, default: '' },\n\t\t/** Title of the empty state shown when no contact is linked. */\n\t\temptyTitleLabel: { type: String, default: () => t('nextcloud-vue', 'No contacts linked') },\n\t\t/** Body of the empty state shown when no contact is linked. */\n\t\temptyDescriptionLabel: { type: String, default: () => t('nextcloud-vue', 'Link a contact from your address book.') },\n\t\t/** Message shown when the contacts could not be loaded. */\n\t\terrorLabel: { type: String, default: () => t('nextcloud-vue', 'Could not load contacts') },\n\t\t/** Stand-in name for a contact whose name is missing. */\n\t\tunknownLabel: { type: String, default: () => t('nextcloud-vue', 'Unknown contact') },\n\t\t/** Label for the action that unlinks a contact from the object. */\n\t\tunlinkLabel: { type: String, default: () => t('nextcloud-vue', 'Unlink contact') },\n\t\t/** Heading above the contacts grouped as applicants. */\n\t\tapplicantsLabel: { type: String, default: () => t('nextcloud-vue', 'Applicants') },\n\t\t/** Heading above the contacts grouped as handlers. */\n\t\thandlersLabel: { type: String, default: () => t('nextcloud-vue', 'Handlers') },\n\t\t/** Heading above the contacts grouped as advisors. */\n\t\tadvisorsLabel: { type: String, default: () => t('nextcloud-vue', 'Advisors') },\n\t\t/** Heading above the contacts with no recognised role. */\n\t\totherLabel: { type: String, default: () => t('nextcloud-vue', 'Other') },\n\t\t/** Badge on a link whose validity window has passed or not yet begun. */\n\t\tinactiveLabel: { type: String, default: () => t('nextcloud-vue', 'Not active') },\n\t\t/** Prefix for a validity window with an open end. */\n\t\tfromLabel: { type: String, default: () => t('nextcloud-vue', 'From') },\n\t\t/** Prefix for a validity window with an open start. */\n\t\tuntilLabel: { type: String, default: () => t('nextcloud-vue', 'Until') },\n\t\t/** Role badge shown on an applicant. */\n\t\tapplicantRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Applicant') },\n\t\t/** Role badge shown on a handler. */\n\t\thandlerRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Handler') },\n\t\t/** Role badge shown on an advisor. */\n\t\tadvisorRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Advisor') },\n\t},\n\n\temits: ['open-reverse-lookup'],\n\n\tdata() {\n\t\treturn {\n\t\t\t// The server's grouping and role vocabulary, when it sends them.\n\t\t\tserverGroups: null,\n\t\t\troleVocabulary: [],\n\t\t\tcontacts: [],\n\t\t\tloading: false,\n\t\t\terror: null,\n\t\t\tshowLinkDialog: false,\n\t\t\tshowCreateDialog: false,\n\t\t\tcreateLoading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The two header buttons in the order and weight of the look: the\n\t\t * board look draws Link contact then New contact, both outlined; the\n\t\t * Nextcloud look keeps Add new contact first and Link as the primary.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, variant: string}>} The buttons.\n\t\t */\n\t\theaderButtons() {\n\t\t\tconst board = normalizeLook(this.cnLook) === 'board'\n\t\t\tconst newLabel = this.addNewLabel !== ''\n\t\t\t\t? this.addNewLabel\n\t\t\t\t: (board ? t('nextcloud-vue', 'New contact') : t('nextcloud-vue', 'Add new contact'))\n\t\t\tconst link = { key: 'link', label: this.linkExistingLabel, variant: board ? 'secondary' : 'primary' }\n\t\t\tconst create = { key: 'new', label: newLabel, variant: 'secondary' }\n\t\t\treturn board ? [link, create] : [create, link]\n\t\t},\n\n\t\t/**\n\t\t * The widget's body actions, unwrapped.\n\t\t *\n\t\t * @return {Array<object>} The resolved buttons, or none.\n\t\t */\n\t\tbodyActions() {\n\t\t\tconst provided = this.cnWidgetBodyActions\n\t\t\tconst list = provided && typeof provided === 'object' && 'value' in provided ? provided.value : provided\n\t\t\treturn Array.isArray(list) ? list : []\n\t\t},\n\n\t\t/**\n\t\t * Defensive view onto `this.contacts` — always an array.\n\t\t *\n\t\t * Guards against the well-known \"this.contacts is not iterable\"\n\t\t * TypeError observed in Phase A / D-1 when the provider returns\n\t\t * an object-shaped payload (or `null`) and the iteration site\n\t\t * blows up before the watch has even fired.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tcontactsArray() {\n\t\t\treturn Array.isArray(this.contacts) ? this.contacts : []\n\t\t},\n\n\t\t/**\n\t\t * Group contacts by normalised role bucket.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupedContacts() {\n\t\t\tif (this.serverGroups !== null) {\n\t\t\t\treturn this.groupsFromServer()\n\t\t\t}\n\t\t\treturn this.groupsFromBuckets\n\t\t},\n\n\t\t/**\n\t\t * The role vocabulary as CnContactPicker options, or nothing when\n\t\t * the schema declares none (the picker keeps its own defaults).\n\t\t *\n\t\t * @return {object} Props to bind onto the picker.\n\t\t */\n\t\tpickerRoleOptions() {\n\t\t\tif (this.roleVocabulary.length === 0) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\troleOptions: this.roleVocabulary.map((role) => ({ label: role.label || role.key, value: role.key })),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Group by the tab's own role buckets, for a server that does not group.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupsFromBuckets() {\n\t\t\tconst labelMap = {\n\t\t\t\tapplicant: this.applicantsLabel,\n\t\t\t\thandler: this.handlersLabel,\n\t\t\t\tadvisor: this.advisorsLabel,\n\t\t\t\tother: this.otherLabel,\n\t\t\t}\n\t\t\tconst buckets = { applicant: [], handler: [], advisor: [], other: [] }\n\t\t\tfor (const c of this.contactsArray) {\n\t\t\t\tconst matched = ROLE_BUCKETS.find((b) => b.match(c.role))\n\t\t\t\tif (matched) {\n\t\t\t\t\tbuckets[matched.key].push(c)\n\t\t\t\t} else {\n\t\t\t\t\tbuckets.other.push(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst out = []\n\t\t\tfor (const key of ['applicant', 'handler', 'advisor', 'other']) {\n\t\t\t\tif (buckets[key].length > 0) {\n\t\t\t\t\tout.push({ key, label: labelMap[key], items: buckets[key] })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/contacts`\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler(id) {\n\t\t\t\tif (id) {\n\t\t\t\t\tthis.fetchContacts()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Initials from displayName (\"Jan de Vries\" → \"JV\"); falls back to `?`.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tinitialsFor(contact) {\n\t\t\tconst name = (contact?.displayName || '').trim()\n\t\t\tif (name === '') {\n\t\t\t\treturn '?'\n\t\t\t}\n\t\t\tconst parts = name.split(/\\s+/).filter(Boolean)\n\t\t\tif (parts.length === 1) {\n\t\t\t\treturn parts[0].charAt(0).toUpperCase()\n\t\t\t}\n\t\t\treturn (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * Stable seed for NcAvatar's deterministic colour + initials.\n\t\t * Prefer the contact UID, then email, then display name so the\n\t\t * generated avatar colour stays consistent for a given contact.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tavatarSeed(contact) {\n\t\t\treturn contact.contactUid || contact.email || contact.displayName || '?'\n\t\t},\n\n\t\t/**\n\t\t * Resolve the normalised role bucket for a contact.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string} one of applicant|handler|advisor|other\n\t\t */\n\t\troleKey(contact) {\n\t\t\tconst matched = ROLE_BUCKETS.find((b) => b.match(contact.role))\n\t\t\treturn matched ? matched.key : 'other'\n\t\t},\n\n\t\t/**\n\t\t * Human-readable role chip label — the translated bucket name\n\t\t * when the role maps to a known bucket, else the raw vCard role.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\troleLabel(contact) {\n\t\t\tconst map = {\n\t\t\t\tapplicant: this.applicantRoleLabel,\n\t\t\t\thandler: this.handlerRoleLabel,\n\t\t\t\tadvisor: this.advisorRoleLabel,\n\t\t\t}\n\t\t\treturn map[this.roleKey(contact)] || contact.role\n\t\t},\n\n\t\t/**\n\t\t * CnStatusBadge variant per role bucket so each role reads as a\n\t\t * distinct accent (handlers = primary, advisors = info, etc.).\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\troleVariant(contact) {\n\t\t\tconst map = {\n\t\t\t\tapplicant: 'info',\n\t\t\t\thandler: 'primary',\n\t\t\t\tadvisor: 'success',\n\t\t\t}\n\t\t\treturn map[this.roleKey(contact)] || 'default'\n\t\t},\n\n\t\t/**\n\t\t * Open the reverse-lookup flyout for a contact (AD-3). Emits an\n\t\t * event for the consuming app to handle (since the flyout's\n\t\t * implementation depends on the host router).\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenReverseLookup(contact) {\n\t\t\tthis.$emit('open-reverse-lookup', {\n\t\t\t\tcontactUid: contact.contactUid,\n\t\t\t\tdisplayName: contact.displayName,\n\t\t\t})\n\t\t},\n\n\t\tasync fetchContacts() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = null\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl), { headers: buildHeaders() })\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthis.error = `${response.status} ${response.statusText}`\n\t\t\t\t\tthis.contacts = []\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst data = await response.json()\n\t\t\t\t// Canonical wrapper-key cascade (per ADR-022): provider\n\t\t\t\t// may return `{results: [...]}` (paginated), `{items:\n\t\t\t\t// [...]}` (some leaves), or a bare array. Anything else\n\t\t\t\t// falls back to `[]` rather than letting a non-iterable\n\t\t\t\t// object reach `groupedContacts` (Phase A / D-1 bug).\n\t\t\t\tthis.contacts = this.unwrapList(data)\n\t\t\t\t// people-on-objects: the server groups by role and names the\n\t\t\t\t// schema's vocabulary. Both optional, so a server that sends\n\t\t\t\t// neither keeps the tab's own buckets.\n\t\t\t\tthis.serverGroups = (data && typeof data === 'object' && data.byRole) ? data.byRole : null\n\t\t\t\tthis.roleVocabulary = (data && Array.isArray(data.roles)) ? data.roles : []\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to fetch contacts', err)\n\t\t\t\tthis.error = String(err?.message || err)\n\t\t\t\tthis.contacts = []\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 * Normalise a list-shaped response body to an array. Accepts\n\t\t * `{results:[...]}`, `{items:[...]}`, or a bare array; any\n\t\t * other shape (object, null, undefined) becomes `[]`.\n\t\t *\n\t\t * @param {object|Array<object>|null} data parsed JSON response body\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tunwrapList(data) {\n\t\t\tif (Array.isArray(data)) {\n\t\t\t\treturn data\n\t\t\t}\n\t\t\tif (data && typeof data === 'object') {\n\t\t\t\tif (Array.isArray(data.results)) {\n\t\t\t\t\treturn data.results\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(data.items)) {\n\t\t\t\t\treturn data.items\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * The server's own grouping, labelled by the schema's vocabulary.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupsFromServer() {\n\t\t\tconst labels = new Map(this.roleVocabulary.map((role) => [role.key, role.label || role.key]))\n\t\t\treturn Object.entries(this.serverGroups)\n\t\t\t\t.filter(([, items]) => Array.isArray(items) && items.length > 0)\n\t\t\t\t.map(([key, items]) => ({\n\t\t\t\t\tkey,\n\t\t\t\t\tlabel: labels.get(key) || (key === 'other' ? this.otherLabel : key),\n\t\t\t\t\titems,\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The validity window of a link as a line, or '' when it is open.\n\t\t *\n\t\t * @param {object} contact The link.\n\t\t *\n\t\t * @return {string} The window.\n\t\t */\n\t\tperiodOf(contact) {\n\t\t\tconst from = contact?.validFrom || ''\n\t\t\tconst until = contact?.validUntil || ''\n\t\t\tif (from === '' && until === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (until === '') {\n\t\t\t\treturn `${this.fromLabel} ${from}`\n\t\t\t}\n\t\t\tif (from === '') {\n\t\t\t\treturn `${this.untilLabel} ${until}`\n\t\t\t}\n\t\t\treturn `${from} – ${until}`\n\t\t},\n\n\t\tasync unlink(contact) {\n\t\t\tif (!contact?.contactUid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// people-on-objects: a person can hold several roles on one\n\t\t\t\t// object, so removing a row removes that role, not the person.\n\t\t\t\tconst role = contact.role ? `?role=${encodeURIComponent(contact.role)}` : ''\n\t\t\t\tconst url = prefixUrl(`${this.baseUrl}/${encodeURIComponent(contact.contactUid)}${role}`)\n\t\t\t\tawait fetch(url, { method: 'DELETE', headers: buildHeaders() })\n\t\t\t\tthis.contacts = this.contactsArray.filter((c) => c.id !== contact.id)\n\t\t\t\tthis.serverGroups = null\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to unlink contact', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a `link` event from CnContactPicker — POST the picked\n\t\t * contact to the bare `/contacts` endpoint, then refresh.\n\t\t *\n\t\t * @param {object} payload picker payload\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onPickerLink(payload) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t...buildHeaders(),\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\t\tconsole.error('CnContactsTab: Failed to link picked contact', response.status)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.showLinkDialog = false\n\t\t\t\tawait this.fetchContacts()\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to link picked contact', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a `create` event from CnContactCreate — POST the form\n\t\t * payload to the dedicated `/contacts/new` endpoint.\n\t\t *\n\t\t * @param {object} payload form payload\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreateSubmit(payload) {\n\t\t\tthis.createLoading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t...buildHeaders(),\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\t\tconsole.error('CnContactsTab: Failed to create+link contact', response.status)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.showCreateDialog = false\n\t\t\t\tawait this.fetchContacts()\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to create+link contact', err)\n\t\t\t} finally {\n\t\t\t\tthis.createLoading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-contacts-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-contacts-tab__header {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tmargin-bottom: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-contacts-tab__group {\n\tmargin-bottom: 16px;\n}\n\n.cn-contacts-tab__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 4px;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.5px;\n}\n\n.cn-contacts-tab__group-count {\n\tfont-weight: 600;\n\tfont-size: 11px;\n\tmin-width: 18px;\n\tpadding: 0 6px;\n\theight: 18px;\n\tline-height: 18px;\n\ttext-align: center;\n\tborder-radius: 9px;\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-contacts-tab__list {\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-contacts-tab__item {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 10px 8px;\n\tborder-radius: var(--border-radius-large, 8px);\n\tcursor: pointer;\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-contacts-tab__item:hover,\n.cn-contacts-tab__item:focus-within {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-contacts-tab__avatar {\n\tflex-shrink: 0;\n\tdisplay: flex;\n\talign-items: center;\n}\n\n.cn-contacts-tab__details {\n\tflex: 1;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding-top: 2px;\n}\n\n.cn-contacts-tab__name-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n}\n\n.cn-contacts-tab__name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-contacts-tab__sub {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 5px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tmin-width: 0;\n}\n\n.cn-contacts-tab__sub-icon {\n\tflex-shrink: 0;\n\topacity: 0.7;\n}\n\n.cn-contacts-tab__sub-text {\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-contacts-tab__unlink {\n\tbackground: none;\n\tborder: none;\n\tpadding: 4px;\n\tcursor: pointer;\n\tcolor: var(--color-text-maxcontrast);\n\tborder-radius: 50%;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex-shrink: 0;\n\talign-self: center;\n}\n\n.cn-contacts-tab__unlink:hover,\n.cn-contacts-tab__unlink:focus {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-error);\n}\n</style>\n"],"names":["AccountMultipleOutline","OfficeBuildingOutline","CnStatusBadge","CnContactPicker","CnContactCreate","CnWidgetHeaderActionButtons","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnContactsTab.vue2.js","sources":["../../../../../src/integrations/builtin/contacts/CnContactsTab.vue"],"sourcesContent":["<!--\n - SPDX-License-Identifier: EUPL-1.2\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n -\n - CnContactsTab — bespoke sidebar tab for the `contacts` integration leaf.\n -\n - Renders the people linked to an OR object grouped by role. Since\n - people-on-objects a person is a Nextcloud user or a vCard contact, and\n - the grouping is the server's when it sends one: the listing carries\n - `byRole` and the schema's `roles`, and the tab falls back to its own\n - buckets (Applicants / Handlers / Advisors / Other, AD-1 of\n - `openspec/changes/integration-contacts/design.md`) for a server that\n - does not. Each card shows avatar, display name, email and role, and a\n - link whose validity has passed reads as inactive.\n -\n - Clicking a contact opens the reverse-lookup flyout (AD-3) listing every\n - OR object linked to that vCard.\n -\n - Tier-2 (this revision): the tab now hosts two header actions — \"Link\n - existing contact\" (opens `CnContactPicker`) and \"Add new contact\"\n - (opens `CnContactCreate`). Both dialogs are loaded lazily via\n - dynamic import to keep the bundle below the integration-tab\n - threshold, and both delegate the actual API call back to this tab\n - so the loading + refresh handshake stays here.\n -\n - Backed by:\n - GET /api/objects/{register}/{schema}/{id}/contacts\n - POST /api/objects/{register}/{schema}/{id}/contacts — link existing\n - POST /api/objects/{register}/{schema}/{id}/contacts/new — create + link\n - DELETE /api/objects/{register}/{schema}/{id}/contacts/{uid} — unlink\n -\n - The payload shape mirrors `ContactLink::jsonSerialize()` —\n - `{id, contactUid, displayName, email, phone, org, avatarUrl, role,\n - addressbookId, contactUri, schemaId, metadata, linkedAt}`.\n -\n - @spec openspec/changes/integration-contacts-tier2/specs/integrations/contacts/spec.md\n -->\n<template>\n\t<div class=\"cn-contacts-tab\">\n\t\t<!-- Header actions: link existing / create new. The board look draws\n\t\t Link contact first and both outlined (DqZaakBetrokkenen); the\n\t\t Nextcloud look keeps Add new contact first and Link as the primary. -->\n\t\t<div class=\"cn-contacts-tab__header\">\n\t\t\t<NcButton\n\t\t\t\tv-for=\"button in headerButtons\"\n\t\t\t\t:key=\"button.key\"\n\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t:aria-label=\"button.label\"\n\t\t\t\t@click=\"button.key === 'link' ? (showLinkDialog = true) : (showCreateDialog = true)\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<LinkVariant v-if=\"button.key === 'link'\" :size=\"20\" />\n\t\t\t\t\t<AccountPlus v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ button.label }}\n\t\t\t</NcButton>\n\t\t\t<CnWidgetHeaderActionButtons v-if=\"bodyActions.length > 0\" :actions=\"bodyActions\" />\n\t\t</div>\n\n\t\t<!-- Loading -->\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- Error -->\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"error\"\n\t\t\t:name=\"errorLabel\"\n\t\t\t:description=\"error\">\n\t\t\t<template #icon>\n\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<!-- Empty -->\n\t\t<NcEmptyContent\n\t\t\tv-else-if=\"contactsArray.length === 0\"\n\t\t\t:name=\"emptyTitleLabel\"\n\t\t\t:description=\"emptyDescriptionLabel\">\n\t\t\t<template #icon>\n\t\t\t\t<AccountMultipleOutline :size=\"48\" />\n\t\t\t</template>\n\t\t</NcEmptyContent>\n\n\t\t<!-- Grouped list -->\n\t\t<div v-else class=\"cn-contacts-tab__groups\">\n\t\t\t<section\n\t\t\t\tv-for=\"group in groupedContacts\"\n\t\t\t\t:key=\"group.key\"\n\t\t\t\tclass=\"cn-contacts-tab__group\">\n\t\t\t\t<h3 class=\"cn-contacts-tab__group-title\">\n\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t<span v-if=\"!isBoard\" class=\"cn-contacts-tab__group-count\">{{ group.items.length }}</span>\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-contacts-tab__list\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"contact in group.items\"\n\t\t\t\t\t\t:key=\"contact.id\"\n\t\t\t\t\t\tclass=\"cn-contacts-tab__item\"\n\t\t\t\t\t\t@click=\"openReverseLookup(contact)\">\n\t\t\t\t\t\t<div class=\"cn-contacts-tab__avatar\">\n\t\t\t\t\t\t\t<NcAvatar\n\t\t\t\t\t\t\t\t:size=\"isBoard ? 32 : 40\"\n\t\t\t\t\t\t\t\t:displayName=\"contact.displayName || unknownLabel\"\n\t\t\t\t\t\t\t\t:user=\"avatarSeed(contact)\"\n\t\t\t\t\t\t\t\t:url=\"contact.avatarUrl || undefined\"\n\t\t\t\t\t\t\t\t:isNoUser=\"true\"\n\t\t\t\t\t\t\t\t:disableMenu=\"true\"\n\t\t\t\t\t\t\t\t:disableTooltip=\"true\"\n\t\t\t\t\t\t\t\thideStatus />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"cn-contacts-tab__details\">\n\t\t\t\t\t\t\t<div class=\"cn-contacts-tab__name-row\">\n\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__name\">\n\t\t\t\t\t\t\t\t\t{{ contact.displayName || unknownLabel }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\t\tv-if=\"contact.role && !isBoard\"\n\t\t\t\t\t\t\t\t\t:label=\"roleLabel(contact)\"\n\t\t\t\t\t\t\t\t\t:variant=\"roleVariant(contact)\"\n\t\t\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\t\tv-if=\"contact.active === false\"\n\t\t\t\t\t\t\t\t\t:label=\"inactiveLabel\"\n\t\t\t\t\t\t\t\t\tvariant=\"default\"\n\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-inactive\" />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<!-- The board look sets the details on one line (DqZaakBetrokkenen). -->\n\t\t\t\t\t\t\t<span v-if=\"isBoard && boardMetaOf(contact)\"\n\t\t\t\t\t\t\t\tclass=\"cn-contacts-tab__meta\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-meta\">{{ boardMetaOf(contact) }}</span>\n\t\t\t\t\t\t\t<template v-if=\"!isBoard\">\n\t\t\t\t\t\t\t\t<div v-if=\"contact.email\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t\t<Email :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.email }}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"contact.phone\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t\t<Phone :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.phone }}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"contact.org\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t\t<OfficeBuildingOutline :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.org }}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tv-if=\"periodOf(contact)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-contacts-tab__sub\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-period\">\n\t\t\t\t\t\t\t\t\t<CalendarRange :size=\"14\" class=\"cn-contacts-tab__sub-icon\" />\n\t\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ periodOf(contact) }}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div v-if=\"contact.note\" class=\"cn-contacts-tab__sub\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-contacts-tab__sub-text\">{{ contact.note }}</span>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<template v-if=\"isBoard\">\n\t\t\t\t\t\t\t<span v-if=\"boardPeriodOf(contact)\"\n\t\t\t\t\t\t\t\tclass=\"cn-contacts-tab__period\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-board-period\">{{ boardPeriodOf(contact) }}</span>\n\t\t\t\t\t\t\t<NcActions class=\"cn-contacts-tab__menu\"\n\t\t\t\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t\t\t\t:aria-label=\"moreActionsLabel(contact)\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-contacts-tab-menu\"\n\t\t\t\t\t\t\t\t@click.stop>\n\t\t\t\t\t\t\t\t<NcActionButton :closeAfterClick=\"true\" @click=\"unlink(contact)\">\n\t\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t\t<Close :size=\"20\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t{{ unlinkLabel }}\n\t\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclass=\"cn-contacts-tab__unlink\"\n\t\t\t\t\t\t\t:aria-label=\"unlinkLabel\"\n\t\t\t\t\t\t\t:title=\"unlinkLabel\"\n\t\t\t\t\t\t\t@click.stop=\"unlink(contact)\">\n\t\t\t\t\t\t\t<Close :size=\"18\" />\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</div>\n\n\t\t<!-- Link existing contact dialog -->\n\t\t<CnContactPicker\n\t\t\tv-if=\"showLinkDialog\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\tv-bind=\"pickerRoleOptions\"\n\t\t\t@link=\"onPickerLink\"\n\t\t\t@close=\"showLinkDialog = false\" />\n\n\t\t<!-- Create new contact dialog -->\n\t\t<CnContactCreate\n\t\t\tv-if=\"showCreateDialog\"\n\t\t\t:loading=\"createLoading\"\n\t\t\t@create=\"onCreateSubmit\"\n\t\t\t@close=\"showCreateDialog = false\" />\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActions, NcAvatar, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'\nimport AccountMultipleOutline from 'vue-material-design-icons/AccountMultipleOutline.vue'\nimport AccountPlus from 'vue-material-design-icons/AccountPlus.vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport CalendarRange from 'vue-material-design-icons/CalendarRange.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\nimport Email from 'vue-material-design-icons/Email.vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport OfficeBuildingOutline from 'vue-material-design-icons/OfficeBuildingOutline.vue'\nimport Phone from 'vue-material-design-icons/Phone.vue'\nimport CnContactCreate from '../../../components/CnContactCreate/CnContactCreate.vue'\nimport CnContactPicker from '../../../components/CnContactPicker/CnContactPicker.vue'\nimport CnStatusBadge from '../../../components/CnStatusBadge/CnStatusBadge.vue'\nimport CnWidgetHeaderActionButtons from '../../../components/CnWidgetWrapper/CnWidgetHeaderActionButtons.vue'\nimport { normalizeLook } from '../../../composables/useLook.js'\nimport { formatBoardDate } from '../../../utils/boardDate.js'\nimport { buildHeaders, prefixUrl } from '../../../utils/index.js'\nimport { WIDGET_BODY_ACTIONS } from '../../../utils/widgetHeaderActions.js'\n\n/**\n * Normalised role keys for grouping. Free-text vCard roles are\n * normalised by lowercasing + trimming; unknown roles fall into `other`.\n *\n * @type {Array<{key: string, match: (r: ?string) => boolean, labelKey: string}>}\n */\nconst ROLE_BUCKETS = [\n\t{ key: 'applicant', match: (r) => /^(applicant|aanvrager|requestor)$/i.test(r ?? ''), labelKey: 'Applicants' },\n\t{ key: 'handler', match: (r) => /^(handler|behandelaar|owner|assignee)$/i.test(r ?? ''), labelKey: 'Handlers' },\n\t{ key: 'advisor', match: (r) => /^(advisor|advisor|adviseur|reviewer)$/i.test(r ?? ''), labelKey: 'Advisors' },\n]\n\n/**\n * CnContactsTab — vCard contacts linked to an OR object, grouped by role.\n *\n * Basic usage\n * ```vue\n * <CnContactsTab\n * register=\"my-register\"\n * schema=\"case\"\n * object-id=\"abc-123\" />\n * ```\n */\nexport default {\n\tname: 'CnContactsTab',\n\n\tcomponents: {\n\t\tNcActionButton,\n\t\tNcActions,\n\t\tNcAvatar,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tAccountPlus,\n\t\tAccountMultipleOutline,\n\t\tAlertCircleOutline,\n\t\tCalendarRange,\n\t\tClose,\n\t\tEmail,\n\t\tLinkVariant,\n\t\tOfficeBuildingOutline,\n\t\tPhone,\n\t\tCnStatusBadge,\n\t\tCnContactPicker,\n\t\tCnContactCreate,\n\t\tCnWidgetHeaderActionButtons,\n\t},\n\n\tinject: {\n\t\t/** The look the app is drawn in (`board` or `nextcloud`), from CnAppRoot or the page. */\n\t\tcnLook: { default: 'nextcloud' },\n\t\t/** The widget's manifest `bodyActions`, drawn after this tab's own buttons. */\n\t\tcnWidgetBodyActions: { from: WIDGET_BODY_ACTIONS, default: null },\n\t},\n\n\tprops: {\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, default: '' },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, default: '' },\n\t\t/** Base API URL for OpenRegister. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\n\t\t// --- Pre-translated labels (consumer-overridable for i18n flexibility) ---\n\t\t/** Label for the button that links an existing contact. */\n\t\tlinkExistingLabel: { type: String, default: () => t('nextcloud-vue', 'Link contact') },\n\t\t/** Label for the button that creates a contact; empty takes the look's own (New contact, or Add new contact). */\n\t\taddNewLabel: { type: String, default: '' },\n\t\t/** Title of the empty state shown when no contact is linked. */\n\t\temptyTitleLabel: { type: String, default: () => t('nextcloud-vue', 'No contacts linked') },\n\t\t/** Body of the empty state shown when no contact is linked. */\n\t\temptyDescriptionLabel: { type: String, default: () => t('nextcloud-vue', 'Link a contact from your address book.') },\n\t\t/** Message shown when the contacts could not be loaded. */\n\t\terrorLabel: { type: String, default: () => t('nextcloud-vue', 'Could not load contacts') },\n\t\t/** Stand-in name for a contact whose name is missing. */\n\t\tunknownLabel: { type: String, default: () => t('nextcloud-vue', 'Unknown contact') },\n\t\t/** Label for the action that unlinks a contact from the object. */\n\t\tunlinkLabel: { type: String, default: () => t('nextcloud-vue', 'Unlink contact') },\n\t\t/** Heading above the contacts grouped as applicants. */\n\t\tapplicantsLabel: { type: String, default: () => t('nextcloud-vue', 'Applicants') },\n\t\t/** Heading above the contacts grouped as handlers. */\n\t\thandlersLabel: { type: String, default: () => t('nextcloud-vue', 'Handlers') },\n\t\t/** Heading above the contacts grouped as advisors. */\n\t\tadvisorsLabel: { type: String, default: () => t('nextcloud-vue', 'Advisors') },\n\t\t/** Heading above the contacts with no recognised role. */\n\t\totherLabel: { type: String, default: () => t('nextcloud-vue', 'Other') },\n\t\t/** Badge on a link whose validity window has passed or not yet begun. */\n\t\tinactiveLabel: { type: String, default: () => t('nextcloud-vue', 'Not active') },\n\t\t/** Prefix for a validity window with an open end. */\n\t\tfromLabel: { type: String, default: () => t('nextcloud-vue', 'From') },\n\t\t/** Prefix for a validity window with an open start. */\n\t\tuntilLabel: { type: String, default: () => t('nextcloud-vue', 'Until') },\n\t\t/** Role badge shown on an applicant. */\n\t\tapplicantRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Applicant') },\n\t\t/** Role badge shown on a handler. */\n\t\thandlerRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Handler') },\n\t\t/** Role badge shown on an advisor. */\n\t\tadvisorRoleLabel: { type: String, default: () => t('nextcloud-vue', 'Advisor') },\n\t},\n\n\temits: ['open-reverse-lookup'],\n\n\tdata() {\n\t\treturn {\n\t\t\t// The server's grouping and role vocabulary, when it sends them.\n\t\t\tserverGroups: null,\n\t\t\troleVocabulary: [],\n\t\t\tcontacts: [],\n\t\t\tloading: false,\n\t\t\terror: null,\n\t\t\tshowLinkDialog: false,\n\t\t\tshowCreateDialog: false,\n\t\t\tcreateLoading: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} The board look draws the rows as DqZaakBetrokkenen does. */\n\t\tisBoard() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * The two header buttons in the order and weight of the look: the\n\t\t * board look draws Link contact then New contact, both outlined; the\n\t\t * Nextcloud look keeps Add new contact first and Link as the primary.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, variant: string}>} The buttons.\n\t\t */\n\t\theaderButtons() {\n\t\t\tconst board = normalizeLook(this.cnLook) === 'board'\n\t\t\tconst newLabel = this.addNewLabel !== ''\n\t\t\t\t? this.addNewLabel\n\t\t\t\t: (board ? t('nextcloud-vue', 'New contact') : t('nextcloud-vue', 'Add new contact'))\n\t\t\tconst link = { key: 'link', label: this.linkExistingLabel, variant: board ? 'secondary' : 'primary' }\n\t\t\tconst create = { key: 'new', label: newLabel, variant: 'secondary' }\n\t\t\treturn board ? [link, create] : [create, link]\n\t\t},\n\n\t\t/**\n\t\t * The widget's body actions, unwrapped.\n\t\t *\n\t\t * @return {Array<object>} The resolved buttons, or none.\n\t\t */\n\t\tbodyActions() {\n\t\t\tconst provided = this.cnWidgetBodyActions\n\t\t\tconst list = provided && typeof provided === 'object' && 'value' in provided ? provided.value : provided\n\t\t\treturn Array.isArray(list) ? list : []\n\t\t},\n\n\t\t/**\n\t\t * Defensive view onto `this.contacts` — always an array.\n\t\t *\n\t\t * Guards against the well-known \"this.contacts is not iterable\"\n\t\t * TypeError observed in Phase A / D-1 when the provider returns\n\t\t * an object-shaped payload (or `null`) and the iteration site\n\t\t * blows up before the watch has even fired.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tcontactsArray() {\n\t\t\treturn Array.isArray(this.contacts) ? this.contacts : []\n\t\t},\n\n\t\t/**\n\t\t * Group contacts by normalised role bucket.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupedContacts() {\n\t\t\tif (this.serverGroups !== null) {\n\t\t\t\treturn this.groupsFromServer()\n\t\t\t}\n\t\t\treturn this.groupsFromBuckets\n\t\t},\n\n\t\t/**\n\t\t * The role vocabulary as CnContactPicker options, or nothing when\n\t\t * the schema declares none (the picker keeps its own defaults).\n\t\t *\n\t\t * @return {object} Props to bind onto the picker.\n\t\t */\n\t\tpickerRoleOptions() {\n\t\t\tif (this.roleVocabulary.length === 0) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\troleOptions: this.roleVocabulary.map((role) => ({ label: role.label || role.key, value: role.key })),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Group by the tab's own role buckets, for a server that does not group.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupsFromBuckets() {\n\t\t\tconst labelMap = {\n\t\t\t\tapplicant: this.applicantsLabel,\n\t\t\t\thandler: this.handlersLabel,\n\t\t\t\tadvisor: this.advisorsLabel,\n\t\t\t\tother: this.otherLabel,\n\t\t\t}\n\t\t\tconst buckets = { applicant: [], handler: [], advisor: [], other: [] }\n\t\t\tfor (const c of this.contactsArray) {\n\t\t\t\tconst matched = ROLE_BUCKETS.find((b) => b.match(c.role))\n\t\t\t\tif (matched) {\n\t\t\t\t\tbuckets[matched.key].push(c)\n\t\t\t\t} else {\n\t\t\t\t\tbuckets.other.push(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst out = []\n\t\t\tfor (const key of ['applicant', 'handler', 'advisor', 'other']) {\n\t\t\t\tif (buckets[key].length > 0) {\n\t\t\t\t\tout.push({ key, label: labelMap[key], items: buckets[key] })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/contacts`\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler(id) {\n\t\t\t\tif (id) {\n\t\t\t\t\tthis.fetchContacts()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Initials from displayName (\"Jan de Vries\" → \"JV\"); falls back to `?`.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tinitialsFor(contact) {\n\t\t\tconst name = (contact?.displayName || '').trim()\n\t\t\tif (name === '') {\n\t\t\t\treturn '?'\n\t\t\t}\n\t\t\tconst parts = name.split(/\\s+/).filter(Boolean)\n\t\t\tif (parts.length === 1) {\n\t\t\t\treturn parts[0].charAt(0).toUpperCase()\n\t\t\t}\n\t\t\treturn (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * Stable seed for NcAvatar's deterministic colour + initials.\n\t\t * Prefer the contact UID, then email, then display name so the\n\t\t * generated avatar colour stays consistent for a given contact.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tavatarSeed(contact) {\n\t\t\treturn contact.contactUid || contact.email || contact.displayName || '?'\n\t\t},\n\n\t\t/**\n\t\t * Resolve the normalised role bucket for a contact.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string} one of applicant|handler|advisor|other\n\t\t */\n\t\troleKey(contact) {\n\t\t\tconst matched = ROLE_BUCKETS.find((b) => b.match(contact.role))\n\t\t\treturn matched ? matched.key : 'other'\n\t\t},\n\n\t\t/**\n\t\t * Human-readable role chip label — the translated bucket name\n\t\t * when the role maps to a known bucket, else the raw vCard role.\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\troleLabel(contact) {\n\t\t\tconst map = {\n\t\t\t\tapplicant: this.applicantRoleLabel,\n\t\t\t\thandler: this.handlerRoleLabel,\n\t\t\t\tadvisor: this.advisorRoleLabel,\n\t\t\t}\n\t\t\treturn map[this.roleKey(contact)] || contact.role\n\t\t},\n\n\t\t/**\n\t\t * CnStatusBadge variant per role bucket so each role reads as a\n\t\t * distinct accent (handlers = primary, advisors = info, etc.).\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\troleVariant(contact) {\n\t\t\tconst map = {\n\t\t\t\tapplicant: 'info',\n\t\t\t\thandler: 'primary',\n\t\t\t\tadvisor: 'success',\n\t\t\t}\n\t\t\treturn map[this.roleKey(contact)] || 'default'\n\t\t},\n\n\t\t/**\n\t\t * Open the reverse-lookup flyout for a contact (AD-3). Emits an\n\t\t * event for the consuming app to handle (since the flyout's\n\t\t * implementation depends on the host router).\n\t\t *\n\t\t * @param {object} contact ContactLink JSON.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenReverseLookup(contact) {\n\t\t\tthis.$emit('open-reverse-lookup', {\n\t\t\t\tcontactUid: contact.contactUid,\n\t\t\t\tdisplayName: contact.displayName,\n\t\t\t})\n\t\t},\n\n\t\tasync fetchContacts() {\n\t\t\tif (!this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = null\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl), { headers: buildHeaders() })\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthis.error = `${response.status} ${response.statusText}`\n\t\t\t\t\tthis.contacts = []\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst data = await response.json()\n\t\t\t\t// Canonical wrapper-key cascade (per ADR-022): provider\n\t\t\t\t// may return `{results: [...]}` (paginated), `{items:\n\t\t\t\t// [...]}` (some leaves), or a bare array. Anything else\n\t\t\t\t// falls back to `[]` rather than letting a non-iterable\n\t\t\t\t// object reach `groupedContacts` (Phase A / D-1 bug).\n\t\t\t\tthis.contacts = this.unwrapList(data)\n\t\t\t\t// people-on-objects: the server groups by role and names the\n\t\t\t\t// schema's vocabulary. Both optional, so a server that sends\n\t\t\t\t// neither keeps the tab's own buckets.\n\t\t\t\tthis.serverGroups = (data && typeof data === 'object' && data.byRole) ? data.byRole : null\n\t\t\t\tthis.roleVocabulary = (data && Array.isArray(data.roles)) ? data.roles : []\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to fetch contacts', err)\n\t\t\t\tthis.error = String(err?.message || err)\n\t\t\t\tthis.contacts = []\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 * Normalise a list-shaped response body to an array. Accepts\n\t\t * `{results:[...]}`, `{items:[...]}`, or a bare array; any\n\t\t * other shape (object, null, undefined) becomes `[]`.\n\t\t *\n\t\t * @param {object|Array<object>|null} data parsed JSON response body\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tunwrapList(data) {\n\t\t\tif (Array.isArray(data)) {\n\t\t\t\treturn data\n\t\t\t}\n\t\t\tif (data && typeof data === 'object') {\n\t\t\t\tif (Array.isArray(data.results)) {\n\t\t\t\t\treturn data.results\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(data.items)) {\n\t\t\t\t\treturn data.items\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * The server's own grouping, labelled by the schema's vocabulary.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, items: Array}>}\n\t\t */\n\t\tgroupsFromServer() {\n\t\t\tconst labels = new Map(this.roleVocabulary.map((role) => [role.key, role.label || role.key]))\n\t\t\treturn Object.entries(this.serverGroups)\n\t\t\t\t.filter(([, items]) => Array.isArray(items) && items.length > 0)\n\t\t\t\t.map(([key, items]) => ({\n\t\t\t\t\tkey,\n\t\t\t\t\tlabel: labels.get(key) || (key === 'other' ? this.otherLabel : key),\n\t\t\t\t\titems,\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The validity window of a link as a line, or '' when it is open.\n\t\t *\n\t\t * @param {object} contact The link.\n\t\t *\n\t\t * @return {string} The window.\n\t\t */\n\t\t/**\n\t\t * A person's details on one line for the board look: e-mail, phone,\n\t\t * organisation and note, separated by middots.\n\t\t *\n\t\t * @param {object} contact The contact link.\n\t\t * @return {string}\n\t\t */\n\t\tboardMetaOf(contact) {\n\t\t\treturn [contact?.email, contact?.phone, contact?.org, contact?.note].filter((part) => typeof part === 'string' && part !== '').join(' · ')\n\t\t},\n\n\t\t/**\n\t\t * The validity window in the board's short dates: \"Vanaf 3 okt\",\n\t\t * \"Tot 31 okt\" or \"3 okt – 31 okt\".\n\t\t *\n\t\t * @param {object} contact The contact link.\n\t\t * @return {string}\n\t\t */\n\t\tboardPeriodOf(contact) {\n\t\t\tconst from = formatBoardDate(contact?.validFrom || '')\n\t\t\tconst until = formatBoardDate(contact?.validUntil || '')\n\t\t\tif (from === '' && until === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (until === '') {\n\t\t\t\treturn t('nextcloud-vue', 'From {date}', { date: from })\n\t\t\t}\n\t\t\tif (from === '') {\n\t\t\t\treturn t('nextcloud-vue', 'Until {date}', { date: until })\n\t\t\t}\n\t\t\treturn `${from} – ${until}`\n\t\t},\n\n\t\t/**\n\t\t * The label of a person's actions menu.\n\t\t *\n\t\t * @param {object} contact The contact link.\n\t\t * @return {string}\n\t\t */\n\t\tmoreActionsLabel(contact) {\n\t\t\treturn t('nextcloud-vue', 'More actions for {name}', { name: contact?.displayName || this.unknownLabel })\n\t\t},\n\n\t\tperiodOf(contact) {\n\t\t\tconst from = contact?.validFrom || ''\n\t\t\tconst until = contact?.validUntil || ''\n\t\t\tif (from === '' && until === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (until === '') {\n\t\t\t\treturn `${this.fromLabel} ${from}`\n\t\t\t}\n\t\t\tif (from === '') {\n\t\t\t\treturn `${this.untilLabel} ${until}`\n\t\t\t}\n\t\t\treturn `${from} – ${until}`\n\t\t},\n\n\t\tasync unlink(contact) {\n\t\t\tif (!contact?.contactUid) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\t// people-on-objects: a person can hold several roles on one\n\t\t\t\t// object, so removing a row removes that role, not the person.\n\t\t\t\tconst role = contact.role ? `?role=${encodeURIComponent(contact.role)}` : ''\n\t\t\t\tconst url = prefixUrl(`${this.baseUrl}/${encodeURIComponent(contact.contactUid)}${role}`)\n\t\t\t\tawait fetch(url, { method: 'DELETE', headers: buildHeaders() })\n\t\t\t\tthis.contacts = this.contactsArray.filter((c) => c.id !== contact.id)\n\t\t\t\tthis.serverGroups = null\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to unlink contact', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a `link` event from CnContactPicker — POST the picked\n\t\t * contact to the bare `/contacts` endpoint, then refresh.\n\t\t *\n\t\t * @param {object} payload picker payload\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onPickerLink(payload) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.baseUrl), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t...buildHeaders(),\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\t\tconsole.error('CnContactsTab: Failed to link picked contact', response.status)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.showLinkDialog = false\n\t\t\t\tawait this.fetchContacts()\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to link picked contact', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a `create` event from CnContactCreate — POST the form\n\t\t * payload to the dedicated `/contacts/new` endpoint.\n\t\t *\n\t\t * @param {object} payload form payload\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreateSubmit(payload) {\n\t\t\tthis.createLoading = true\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl}/new`), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t...buildHeaders(),\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t},\n\t\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t\t})\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\t\tconsole.error('CnContactsTab: Failed to create+link contact', response.status)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.showCreateDialog = false\n\t\t\t\tawait this.fetchContacts()\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('CnContactsTab: Failed to create+link contact', err)\n\t\t\t} finally {\n\t\t\t\tthis.createLoading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-contacts-tab {\n\tpadding: 12px;\n\toverflow-x: hidden;\n}\n\n.cn-contacts-tab__header {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tmargin-bottom: 12px;\n\tflex-wrap: wrap;\n}\n\n.cn-contacts-tab__group {\n\tmargin-bottom: 16px;\n}\n\n.cn-contacts-tab__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0 0 4px;\n\ttext-transform: uppercase;\n\tletter-spacing: 0.5px;\n}\n\n.cn-contacts-tab__group-count {\n\tfont-weight: 600;\n\tfont-size: 11px;\n\tmin-width: 18px;\n\tpadding: 0 6px;\n\theight: 18px;\n\tline-height: 18px;\n\ttext-align: center;\n\tborder-radius: 9px;\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-contacts-tab__list {\n\tlist-style: none;\n\tpadding: 0;\n\tmargin: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-contacts-tab__item {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 10px 8px;\n\tborder-radius: var(--border-radius-large, 8px);\n\tcursor: pointer;\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-contacts-tab__item:hover,\n.cn-contacts-tab__item:focus-within {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-contacts-tab__avatar {\n\tflex-shrink: 0;\n\tdisplay: flex;\n\talign-items: center;\n}\n\n.cn-contacts-tab__details {\n\tflex: 1;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tpadding-top: 2px;\n}\n\n.cn-contacts-tab__name-row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n}\n\n.cn-contacts-tab__name {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-contacts-tab__sub {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 5px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tmin-width: 0;\n}\n\n.cn-contacts-tab__sub-icon {\n\tflex-shrink: 0;\n\topacity: 0.7;\n}\n\n.cn-contacts-tab__sub-text {\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-contacts-tab__unlink {\n\tbackground: none;\n\tborder: none;\n\tpadding: 4px;\n\tcursor: pointer;\n\tcolor: var(--color-text-maxcontrast);\n\tborder-radius: 50%;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex-shrink: 0;\n\talign-self: center;\n}\n\n.cn-contacts-tab__unlink:hover,\n.cn-contacts-tab__unlink:focus {\n\tbackground-color: var(--color-background-dark);\n\tcolor: var(--color-error);\n}\n</style>\n"],"names":["AccountMultipleOutline","OfficeBuildingOutline","CnStatusBadge","CnContactPicker","CnContactCreate","CnWidgetHeaderActionButtons","t"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+NA;;;AAGC;AACA;;AAED,MAAM,eAAe;CACpB,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,oCAAoC,CAAC,IAAI,CAAC,CAAE,IAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAW,EAAG;CAC9G,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,yCAAyC,CAAC,IAAI,CAAC,CAAE,IAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,UAAS,EAAG;CAC/G,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,wCAAwC,CAAC,IAAI,CAAC,CAAE,IAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,UAAS,EAAG;AAC/G,EAAA;;AAEA;;AAEC;;AAEA;AACA;;;AAGA;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;CAErB,UAAU,EAAE;AACX,EAAA,cAAc;AACd,EAAA,SAAS;AACT,EAAA,QAAQ;AACR,EAAA,QAAQ;AACR,EAAA,cAAc;EACd,aAAa;EACb,WAAW;0BACXA,0BAAsB;AACtB,EAAA,kBAAkB;EAClB,aAAa;EACb,KAAK;EACL,KAAK;EACL,WAAW;AACX,yBAAAC,yBAAqB;EACrB,KAAK;iBACLC,UAAa;AACb,mBAAAC,YAAe;AACf,mBAAAC,YAAe;+BACfC,YAA2B;EAC3B;;AAED,CAAA,MAAM,EAAE;AACP;EACA,MAAM,EAAE,EAAE,OAAO,EAAE,WAAU,EAAG;AAChC;EACA,mBAAmB,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAG,EAAG;EACjE;;AAED,CAAA,KAAK,EAAE;AACN;AACA,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;AAC1C;AACA,EAAA,QAAQ,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACvC;AACA,EAAA,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;AACrC;EACA,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,wBAAuB,EAAG;;AAE5D;;AAEA,EAAA,iBAAiB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMC,SAAC,CAAC,eAAe,EAAE,cAAc,GAAG;AACtF;AACA,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI;;AAE1C,EAAA,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,oBAAoB,GAAG;;EAE1F,qBAAqB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,wCAAwC,CAAA,EAAG;;AAEpH,EAAA,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,yBAAyB,GAAG;;AAE1F,EAAA,YAAY,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,iBAAiB,GAAG;;AAEpF,EAAA,WAAW,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,gBAAgB,GAAG;;AAElF,EAAA,eAAe,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,YAAY,GAAG;AAClF;AACA,EAAA,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,UAAU,GAAG;AAC9E;AACA,EAAA,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,UAAU,GAAG;;AAE9E,EAAA,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,OAAO,GAAG;;AAExE,EAAA,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,YAAY,GAAG;AAChF;EACA,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,MAAM,CAAA,EAAG;AACtE;AACA,EAAA,UAAU,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,OAAO,GAAG;AACxE;AACA,EAAA,kBAAkB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,WAAW,GAAG;;AAEpF,EAAA,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,SAAS,GAAG;AAChF;AACA,EAAA,gBAAgB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAMA,SAAC,CAAC,eAAe,EAAE,SAAS,GAAG;EAChF;;CAED,KAAK,EAAE,CAAC,qBAAqB,CAAC;;AAE9B,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;;AAEN,GAAA,YAAY,EAAE,IAAI;AAClB,GAAA,cAAc,EAAE,EAAE;GAClB,QAAQ,EAAE,EAAE;GACZ,OAAO,EAAE,KAAK;GACd,KAAK,EAAE,IAAI;AACX,GAAA,cAAc,EAAE,KAAK;GACrB,gBAAgB,EAAE,KAAK;AACvB,GAAA,aAAa,EAAE,KAAK;AACrB,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;AACA,EAAA,OAAO,GAAG;AACT,GAAA,OAAO,aAAa,CAAC,IAAI,CAAC,MAAM,CAAE,KAAI,OAAM;GAC5C;;AAED;;;;AAIC;AACA;;AAED,EAAA,aAAa,GAAG;GACf,MAAM,KAAM,GAAE,aAAa,CAAC,IAAI,CAAC,MAAM,CAAE,KAAI,QAAM;AACnD,GAAA,MAAM,QAAS,GAAE,IAAI,CAAC,WAAU,KAAM,EAAC;AACtC,MAAE,IAAI,CAAC,WAAU;AACjB,OAAG,KAAM,GAAEA,SAAC,CAAC,eAAe,EAAE,aAAa,CAAA,GAAIA,SAAC,CAAC,eAAe,EAAE,iBAAiB,CAAC,EAAA;AACrF,GAAA,MAAM,IAAK,GAAE,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,iBAAiB,EAAE,OAAO,EAAE,KAAM,GAAE,WAAU,GAAI,SAAU,GAAA;GACpG,MAAM,MAAK,GAAI,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAY,GAAA;AACnE,GAAA,OAAO,KAAI,GAAI,CAAC,IAAI,EAAE,MAAM,CAAE,GAAE,CAAC,MAAM,EAAE,IAAI,CAAA;GAC7C;;AAED;AACC;AACA;;;EAGD,WAAW,GAAG;AACb,GAAA,MAAM,QAAO,GAAI,IAAI,CAAC,oBAAkB;AACxC,GAAA,MAAM,IAAK,GAAE,QAAO,IAAK,OAAO,QAAO,KAAM,QAAO,IAAK,OAAM,IAAK,QAAS,GAAE,QAAQ,CAAC,KAAI,GAAI,SAAO;AACvG,GAAA,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAE,GAAE,IAAG,GAAI,EAAC;GACrC;;AAED;;AAEC;;;;AAIA;AACA;AACA;;AAED,EAAA,aAAa,GAAG;GACf,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAA,GAAI,IAAI,CAAC,QAAS,GAAE,EAAC;GACvD;;AAED;AACC;AACA;;;AAGD,EAAA,eAAe,GAAG;GACjB,IAAI,IAAI,CAAC,YAAa,KAAI,IAAI,EAAE;IAC/B,OAAO,IAAI,CAAC,gBAAgB,EAAC;AAC9B,IAAA;GACA,OAAO,IAAI,CAAC,iBAAgB;GAC5B;;AAED;;;AAGC;AACA;;AAED,EAAA,iBAAiB,GAAG;AACnB,GAAA,IAAI,IAAI,CAAC,cAAc,CAAC,MAAO,KAAI,CAAC,EAAE;AACrC,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,OAAO;AACN,IAAA,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAI,IAAK,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAI,EAAC,CAAC,CAAC;AACrG,IAAA;GACA;;AAED;;AAEC;;;AAGD,EAAA,iBAAiB,GAAG;AACnB,GAAA,MAAM,QAAO,GAAI;AAChB,IAAA,SAAS,EAAE,IAAI,CAAC,eAAe;IAC/B,OAAO,EAAE,IAAI,CAAC,aAAa;IAC3B,OAAO,EAAE,IAAI,CAAC,aAAa;AAC3B,IAAA,KAAK,EAAE,IAAI,CAAC,UAAU;AACvB,KAAA;GACA,MAAM,OAAQ,GAAE,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAG,GAAA;AACrE,GAAA,KAAK,MAAM,CAAE,IAAG,IAAI,CAAC,aAAa,EAAE;IACnC,MAAM,OAAM,GAAI,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAA;IACxD,IAAI,OAAO,EAAE;KACZ,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,EAAA;KAC1B,MAAK;AACN,KAAA,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,EAAA;AACrB,KAAA;AACD,IAAA;GACA,MAAM,GAAE,GAAI,GAAC;GACb,KAAK,MAAM,GAAI,IAAG,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE;IAC/D,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,MAAK,GAAI,CAAC,EAAE;KAC5B,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,GAAG,CAAA,EAAG,EAAA;AAC5D,KAAA;AACD,IAAA;AACA,GAAA,OAAO,GAAE;GACT;;AAED,EAAA,OAAO,GAAG;AACT,GAAA,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAA;GACzF;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,QAAQ,EAAE;AACT,GAAA,SAAS,EAAE,IAAI;GACf,OAAO,CAAC,EAAE,EAAE;AACX,IAAA,IAAI,EAAE,EAAE;AACP,KAAA,IAAI,CAAC,aAAa,GAAC;AACpB,KAAA;IACA;GACD;EACD;;AAED,CAAA,OAAO,EAAE;AACR;;AAEC;AACA;AACA;AACA;;AAED,EAAA,WAAW,CAAC,OAAO,EAAE;AACpB,GAAA,MAAM,IAAK,GAAE,CAAC,OAAO,EAAE,WAAY,IAAG,EAAE,EAAE,IAAI,GAAC;GAC/C,IAAI,IAAK,KAAI,EAAE,EAAE;AAChB,IAAA,OAAO,GAAE;AACV,IAAA;AACA,GAAA,MAAM,KAAI,GAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,EAAA;AAC9C,GAAA,IAAI,KAAK,CAAC,MAAK,KAAM,CAAC,EAAE;AACvB,IAAA,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAC;AACvC,IAAA;AACA,GAAA,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,GAAE,KAAK,CAAC,KAAK,CAAC,MAAK,GAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,WAAW,EAAC;GAC5E;;AAED;;;;AAIC;AACA;AACA;AACA;;AAED,EAAA,UAAU,CAAC,OAAO,EAAE;AACnB,GAAA,OAAO,OAAO,CAAC,UAAW,IAAG,OAAO,CAAC,KAAM,IAAG,OAAO,CAAC,WAAU,IAAK,GAAE;GACvE;;AAED;AACC;AACA;AACA;AACA;;;AAGD,EAAA,OAAO,CAAC,OAAO,EAAE;GAChB,MAAM,OAAQ,GAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAA;AAC9D,GAAA,OAAO,OAAM,GAAI,OAAO,CAAC,GAAI,GAAE,OAAM;GACrC;;AAED;;;AAGC;AACA;AACA;AACA;;AAED,EAAA,SAAS,CAAC,OAAO,EAAE;AAClB,GAAA,MAAM,MAAM;AACX,IAAA,SAAS,EAAE,IAAI,CAAC,kBAAkB;IAClC,OAAO,EAAE,IAAI,CAAC,gBAAgB;IAC9B,OAAO,EAAE,IAAI,CAAC,gBAAgB;AAC/B,KAAA;AACA,GAAA,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,IAAG;GAChD;;AAED;;;AAGC;AACA;AACA;AACA;;AAED,EAAA,WAAW,CAAC,OAAO,EAAE;AACpB,GAAA,MAAM,MAAM;AACX,IAAA,SAAS,EAAE,MAAM;AACjB,IAAA,OAAO,EAAE,SAAS;AAClB,IAAA,OAAO,EAAE,SAAS;AACnB,KAAA;AACA,GAAA,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA,IAAK,SAAQ;GAC7C;;AAED;;;AAGC;AACA;AACA;AACA;;;EAGD,iBAAiB,CAAC,OAAO,EAAE;AAC1B,GAAA,IAAI,CAAC,KAAK,CAAC,qBAAqB,EAAE;IACjC,UAAU,EAAE,OAAO,CAAC,UAAU;AAC9B,IAAA,WAAW,EAAE,OAAO,CAAC,WAAW;IAChC,EAAA;GACD;;AAED,EAAA,MAAM,aAAa,GAAG;GACrB,IAAI,CAAC,IAAI,CAAC,QAAO,IAAK,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,QAAQ,EAAE;IACrD,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;AAClB,GAAA,IAAI,CAAC,KAAM,GAAE,KAAG;AAChB,GAAA,IAAI;AACH,IAAA,MAAM,QAAS,GAAE,MAAM,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,OAAO,EAAE,YAAY,IAAI,EAAA;AACjF,IAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;KACjB,IAAI,CAAC,KAAI,GAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,QAAQ,CAAC,UAAU,CAAC,EAAA;AACvD,KAAA,IAAI,CAAC,QAAO,GAAI,GAAC;KACjB,MAAK;AACN,KAAA;IACA,MAAM,IAAK,GAAE,MAAM,QAAQ,CAAC,IAAI,GAAC;;;;;AAKjC;AACA,IAAA,IAAI,CAAC,QAAS,GAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAA;;;AAGpC;AACA,IAAA,IAAI,CAAC,YAAa,GAAE,CAAC,IAAG,IAAK,OAAO,IAAG,KAAM,QAAS,IAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAK,GAAI,KAAG;AACzF,IAAA,IAAI,CAAC,cAAe,GAAE,CAAC,IAAK,IAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,KAAI,GAAI,GAAC;IACzE,CAAA,OAAO,GAAG,EAAE;;IAEb,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,EAAA;AAC5D,IAAA,IAAI,CAAC,KAAI,GAAI,MAAM,CAAC,GAAG,EAAE,OAAQ,IAAG,GAAG,EAAA;AACvC,IAAA,IAAI,CAAC,QAAO,GAAI,GAAC;AAClB,IAAA,SAAU;AACT,IAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;AACpB,IAAA;GACA;;AAED;;;AAGC;AACA;;AAEA;AACA;;AAED,EAAA,UAAU,CAAC,IAAI,EAAE;GAChB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;AACxB,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,IAAI,IAAG,IAAK,OAAO,IAAG,KAAM,QAAQ,EAAE;AACrC,IAAA,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AAChC,KAAA,OAAO,IAAI,CAAC,OAAM;AACnB,KAAA;IACA,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;AAC9B,KAAA,OAAO,IAAI,CAAC,KAAI;AACjB,KAAA;AACD,IAAA;AACA,GAAA,OAAO,EAAC;GACR;;AAED;;AAEC;;;AAGD,EAAA,gBAAgB,GAAG;AAClB,GAAA,MAAM,MAAK,GAAI,IAAI,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,CAAC,EAAA;AAC5F,GAAA,OAAO,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAA;KACrC,MAAM,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,KAAK,CAAC,OAAO,CAAC,KAAK,CAAA,IAAK,KAAK,CAAC,MAAO,GAAE,CAAC,CAAA;AAC/D,KAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM;AACvB,KAAA,GAAG;KACH,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAA,KAAM,GAAE,KAAM,OAAQ,GAAE,IAAI,CAAC,UAAS,GAAI,GAAG,CAAC;KACnE,KAAK;AACN,KAAC,CAAC,CAAA;GACH;;AAED;;AAEC;;AAEA;;;AAGD;;AAEC;AACA;AACA;AACA;;AAED,EAAA,WAAW,CAAC,OAAO,EAAE;GACpB,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,OAAO,IAAG,KAAM,QAAO,IAAK,IAAG,KAAM,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAA;GACzI;;AAED;;;AAGC;AACA;AACA;;EAED,aAAa,CAAC,OAAO,EAAE;GACtB,MAAM,OAAO,eAAe,CAAC,OAAO,EAAE,SAAU,IAAG,EAAE,EAAA;GACrD,MAAM,KAAI,GAAI,eAAe,CAAC,OAAO,EAAE,UAAW,IAAG,EAAE,EAAA;GACvD,IAAI,IAAK,KAAI,EAAC,IAAK,KAAI,KAAM,EAAE,EAAE;AAChC,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,IAAI,KAAM,KAAI,EAAE,EAAE;IACjB,OAAOA,SAAC,CAAC,eAAe,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,IAAG,EAAG,CAAA;AACxD,IAAA;GACA,IAAI,IAAK,KAAI,EAAE,EAAE;IAChB,OAAOA,SAAC,CAAC,eAAe,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,KAAI,EAAG,CAAA;AAC1D,IAAA;GACA,OAAO,CAAC,EAAE,IAAI,CAAA,GAAI,EAAE,KAAK,CAAC,CAAA;GAC1B;;AAED;AACC;AACA;AACA;AACA;;EAED,gBAAgB,CAAC,OAAO,EAAE;AACzB,GAAA,OAAOA,SAAC,CAAC,eAAe,EAAE,yBAAyB,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,WAAU,IAAK,IAAI,CAAC,cAAc,CAAA;GACxG;;AAED,EAAA,QAAQ,CAAC,OAAO,EAAE;AACjB,GAAA,MAAM,IAAG,GAAI,OAAO,EAAE,SAAU,IAAG,GAAC;AACpC,GAAA,MAAM,KAAM,GAAE,OAAO,EAAE,UAAW,IAAG,GAAC;GACtC,IAAI,IAAK,KAAI,EAAC,IAAK,KAAI,KAAM,EAAE,EAAE;AAChC,IAAA,OAAO,EAAC;AACT,IAAA;AACA,GAAA,IAAI,KAAM,KAAI,EAAE,EAAE;AACjB,IAAA,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,CAAE,CAAA,EAAE,IAAI,CAAC,CAAA;AAClC,IAAA;GACA,IAAI,IAAK,KAAI,EAAE,EAAE;AAChB,IAAA,OAAO,CAAC,EAAE,IAAI,CAAC,UAAU,CAAA,CAAE,EAAE,KAAK,CAAC,CAAA;AACpC,IAAA;GACA,OAAO,CAAC,EAAE,IAAI,CAAA,GAAI,EAAE,KAAK,CAAC,CAAA;GAC1B;;AAED,EAAA,MAAM,MAAM,CAAC,OAAO,EAAE;GACrB,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE;IACzB,MAAK;AACN,IAAA;AACA,GAAA,IAAI;;;AAGH,IAAA,MAAM,IAAG,GAAI,OAAO,CAAC,IAAG,GAAI,CAAC,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAA,GAAI,GAAC;AAC3E,IAAA,MAAM,GAAI,GAAE,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAAE,kBAAkB,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAA;IACxF,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAC,EAAG,EAAA;AAC9D,IAAA,IAAI,CAAC,QAAO,GAAI,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAC,KAAM,OAAO,CAAC,EAAE,EAAA;AACpE,IAAA,IAAI,CAAC,eAAe,KAAG;IACtB,CAAA,OAAO,GAAG,EAAE;;IAEb,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,EAAA;AAC7D,IAAA;GACA;;AAED;;;AAGC;AACA;AACA;;;EAGD,MAAM,YAAY,CAAC,OAAO,EAAE;AAC3B,GAAA,IAAI;IACH,MAAM,QAAS,GAAE,MAAM,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;KACrD,MAAM,EAAE,MAAM;AACd,KAAA,OAAO,EAAE;AACR,MAAA,GAAG,YAAY,EAAE;AACjB,MAAA,cAAc,EAAE,kBAAkB;MAClC;KACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAC7B,EAAA;AACD,IAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;;AAEjB,KAAA,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,QAAQ,CAAC,MAAM,EAAA;KAC7E,MAAK;AACN,KAAA;IACA,IAAI,CAAC,iBAAiB,MAAI;IAC1B,MAAM,IAAI,CAAC,aAAa,GAAC;IACxB,CAAA,OAAO,GAAG,EAAE;;IAEb,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,GAAG,EAAA;AAClE,IAAA;GACA;;AAED;;AAEC;AACA;AACA;AACA;;;EAGD,MAAM,cAAc,CAAC,OAAO,EAAE;GAC7B,IAAI,CAAC,gBAAgB,KAAG;AACxB,GAAA,IAAI;IACH,MAAM,QAAS,GAAE,MAAM,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE;KAC9D,MAAM,EAAE,MAAM;AACd,KAAA,OAAO,EAAE;AACR,MAAA,GAAG,YAAY,EAAE;AACjB,MAAA,cAAc,EAAE,kBAAkB;MAClC;KACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAC7B,EAAA;AACD,IAAA,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE;;AAEjB,KAAA,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,QAAQ,CAAC,MAAM,EAAA;KAC7E,MAAK;AACN,KAAA;IACA,IAAI,CAAC,mBAAmB,MAAI;IAC5B,MAAM,IAAI,CAAC,aAAa,GAAC;IACxB,CAAA,OAAO,GAAG,EAAE;;IAEb,OAAO,CAAC,KAAK,CAAC,8CAA8C,EAAE,GAAG,EAAA;AAClE,IAAA,SAAU;IACT,IAAI,CAAC,gBAAgB,MAAI;AAC1B,IAAA;GACA;EACD;AACF;;;;"}