@conduction/nextcloud-vue 2.140.0 → 2.142.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 (123) hide show
  1. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
  2. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +10 -2
  3. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +24 -3
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  9. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +57 -40
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -3
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +43 -3
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  30. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +1 -1
  31. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +21 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  37. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  38. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +65 -1
  39. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  40. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +120 -69
  41. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  42. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  43. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +47 -9
  44. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +3 -3
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  51. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  52. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  54. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  55. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  56. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  57. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  58. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +2 -0
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +1 -1
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +10 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +34 -26
  68. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -1
  69. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  70. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  71. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  72. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  73. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  74. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  75. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  76. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  77. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  78. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  79. package/dist/esm/l10n/en.json.js +11 -1
  80. package/dist/esm/l10n/en.json.js.map +1 -1
  81. package/dist/esm/l10n/nl.json.js +12 -2
  82. package/dist/esm/l10n/nl.json.js.map +1 -1
  83. package/dist/esm/library-style.js +1 -1
  84. package/dist/esm/nextcloud-vue.css +1116 -686
  85. package/dist/esm/utils/buildViewTree.js +23 -8
  86. package/dist/esm/utils/buildViewTree.js.map +1 -1
  87. package/dist/esm/utils/groupSublines.js +120 -0
  88. package/dist/esm/utils/groupSublines.js.map +1 -0
  89. package/dist/nextcloud-vue.cjs.js +2023 -1239
  90. package/dist/nextcloud-vue.cjs.js.map +2 -2
  91. package/dist/nextcloud-vue.css +1116 -686
  92. package/l10n/en.json +11 -1
  93. package/l10n/nl.json +12 -2
  94. package/package.json +1 -1
  95. package/src/components/CnAssignDialog/CnAssignDialog.vue +10 -2
  96. package/src/components/CnCellRenderer/CnCellRenderer.vue +26 -4
  97. package/src/components/CnChartWidget/CnChartWidget.md +1 -0
  98. package/src/components/CnChartWidget/CnChartWidget.vue +49 -4
  99. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  100. package/src/components/CnDataTable/CnDataTable.md +1 -0
  101. package/src/components/CnDataTable/CnDataTable.vue +28 -4
  102. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  103. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +44 -4
  104. package/src/components/CnIndexPage/CnIndexPage.vue +22 -1
  105. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  106. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +101 -3
  107. package/src/components/CnMapWidget/CnMapWidget.vue +47 -9
  108. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +3 -3
  109. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  110. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  111. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +20 -1
  112. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +33 -9
  113. package/src/css/dialog.css +13 -1
  114. package/src/css/look-board-detail.css +107 -9
  115. package/src/css/look-board-index.css +143 -7
  116. package/src/css/look-board.css +109 -0
  117. package/src/css/table.css +12 -0
  118. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  119. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  120. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  121. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  122. package/src/utils/buildViewTree.js +23 -8
  123. package/src/utils/groupSublines.js +125 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CnContactsTab.vue3.js","sources":["../../../../../src/integrations/builtin/contacts/CnContactsTab.vue?vue&type=template&id=ec6b0008&lang.js"],"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":["_openBlock","_createElementBlock","_createElementVNode","_Fragment","_renderList","_createBlock","_toDisplayString","_createVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnContactsTab.vue3.js","sources":["../../../../../src/integrations/builtin/contacts/CnContactsTab.vue?vue&type=template&id=a0d90184&lang.js"],"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":["_openBlock","_createElementBlock","_createElementVNode","_Fragment","_renderList","_createBlock","_toDisplayString","_createVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnDeckCard.vue2.js';
2
2
  import { render } from './CnDeckCard.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-13f7eff5";
5
+ script.__scopeId = "data-v-d8cff1c2";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,9 +1,13 @@
1
1
  import { translate, translatePlural } from '@nextcloud/l10n';
2
- import { NcLoadingIcon } from '@nextcloud/vue';
2
+ import { NcButton, NcLoadingIcon } from '@nextcloud/vue';
3
+ import LinkVariant from 'vue-material-design-icons/LinkVariant.vue';
4
+ import Plus from 'vue-material-design-icons/Plus.vue';
3
5
  import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue';
4
- import __sfc_main from '../../../components/CnDetailCard/CnDetailCard.vue.js';
5
- import { safeHref } from '../../../utils/safeHref.js';
6
+ import __sfc_main from '../../../components/CnDeckCardCreate/CnDeckCardCreate.vue.js';
7
+ import __sfc_main$1 from '../../../components/CnDeckCardPicker/CnDeckCardPicker.vue.js';
8
+ import __sfc_main$2 from '../../../components/CnDetailCard/CnDetailCard.vue.js';
6
9
  import { prefixUrl, buildHeaders } from '../../../utils/headers.js';
10
+ import { safeHref } from '../../../utils/safeHref.js';
7
11
 
8
12
  const VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity'];
9
13
 
@@ -16,7 +20,7 @@ const VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'singl
16
20
  var script = {
17
21
  name: 'CnDeckCard',
18
22
 
19
- components: { CnDetailCard: __sfc_main, NcLoadingIcon, ViewColumnOutline },
23
+ components: { CnDeckCardCreate: __sfc_main, CnDeckCardPicker: __sfc_main$1, CnDetailCard: __sfc_main$2, LinkVariant, NcButton, NcLoadingIcon, Plus, ViewColumnOutline },
20
24
 
21
25
  props: {
22
26
  /** Stable integration id (forwarded from the registry — always `'deck'`). */
@@ -50,6 +54,16 @@ var script = {
50
54
  unavailableLabel: { type: String, default: () => translate('nextcloud-vue', 'NC Deck is currently unavailable.') },
51
55
  /** URL of the NC Deck app entry. */
52
56
  deckAppUrl: { type: String, default: '/index.php/apps/deck' },
57
+ /**
58
+ * Buttons in the card's title row on a detail page: `link` links an
59
+ * existing Deck card, `create` makes a new one linked to the object.
60
+ * In the order given; none by default.
61
+ */
62
+ headerActions: { type: Array, default: () => [] },
63
+ /** Label of the `link` button. */
64
+ linkLabel: { type: String, default: () => translate('nextcloud-vue', 'Link an existing card') },
65
+ /** Label of the `create` button. */
66
+ createLabel: { type: String, default: () => translate('nextcloud-vue', 'New card') },
53
67
  },
54
68
 
55
69
  data() {
@@ -58,10 +72,26 @@ var script = {
58
72
  entity: null,
59
73
  loading: false,
60
74
  degraded: '',
75
+ pickerOpen: false,
76
+ createOpen: false,
77
+ actionError: '',
61
78
  }
62
79
  },
63
80
 
64
81
  computed: {
82
+ /**
83
+ * The title-row buttons to draw: only on a detail page with an
84
+ * object, only the known ones, in the order given.
85
+ *
86
+ * @return {Array<string>}
87
+ */
88
+ shownHeaderActions() {
89
+ if (this.surface !== 'detail-page' || !this.objectId || !Array.isArray(this.headerActions)) {
90
+ return []
91
+ }
92
+ return this.headerActions.filter((action, index, all) => ['create', 'link'].includes(action) && all.indexOf(action) === index)
93
+ },
94
+
65
95
  cardHeaderTitle() {
66
96
  return this.title || this.integrationId
67
97
  },
@@ -156,6 +186,58 @@ var script = {
156
186
  },
157
187
 
158
188
  methods: {
189
+ /** @return {string} The object's Deck endpoint the link and create flows post to. */
190
+ deckEndpoint() {
191
+ return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/deck`
192
+ },
193
+
194
+ /**
195
+ * Link the picked card, then read the list again.
196
+ *
197
+ * @param {object} payload `{ cardId }` from the picker.
198
+ */
199
+ async onLink(payload) {
200
+ this.pickerOpen = false;
201
+ await this.post(this.deckEndpoint(), payload, translate('nextcloud-vue', 'Could not link card.'));
202
+ },
203
+
204
+ /**
205
+ * Create the card linked to the object, then read the list again.
206
+ *
207
+ * @param {object} payload `{ boardId, stackId, title, ... }` from the form.
208
+ */
209
+ async onCreate(payload) {
210
+ this.createOpen = false;
211
+ await this.post(`${this.deckEndpoint()}/new`, payload, translate('nextcloud-vue', 'Could not create card.'));
212
+ },
213
+
214
+ /**
215
+ * POST one flow's payload and reload on success.
216
+ *
217
+ * @param {string} url The endpoint.
218
+ * @param {object} payload The body.
219
+ * @param {string} failure The sentence shown when it fails.
220
+ */
221
+ async post(url, payload, failure) {
222
+ this.actionError = '';
223
+ try {
224
+ const response = await fetch(prefixUrl(url), {
225
+ method: 'POST',
226
+ headers: { ...buildHeaders(), 'Content-Type': 'application/json' },
227
+ body: JSON.stringify(payload),
228
+ });
229
+ if (response.ok) {
230
+ await this.fetchList();
231
+ } else if (response.status === 409) {
232
+ this.actionError = translate('nextcloud-vue', 'This card is already linked.');
233
+ } else {
234
+ this.actionError = failure;
235
+ }
236
+ } catch {
237
+ this.actionError = failure;
238
+ }
239
+ },
240
+
159
241
  baseUrl() {
160
242
  return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`
161
243
  },
@@ -1 +1 @@
1
- {"version":3,"file":"CnDeckCard.vue2.js","sources":["../../../../../src/integrations/builtin/deck/CnDeckCard.vue"],"sourcesContent":["<!--\n CnDeckCard — bespoke surface-aware widget for the `deck` integration.\n\n Replaces the generic CnIntegrationCard for the `deck` leaf. Branches on\n `surface` per AD-19:\n - user-dashboard / app-dashboard : headline \"N cards across M stacks\";\n secondary line names the most recent card.\n - detail-page : mini three-column kanban view of\n the linked card's board with the linked card highlighted in its\n current stack (per design.md AD-2).\n - single-entity : chip with card title + stack name\n (referenceType: 'deck').\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnDeckTab; for `single-entity` the optional `value` prop addresses a\n single card by id (matching CnIntegrationCard's fetchSingle contract).\n\n See `openregister/openspec/changes/integration-deck/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback),\n AD-2 (mini-kanban on detail-page).\n-->\n<template>\n\t<CnDetailCard :title=\"cardHeaderTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-deck-card__chip\" :title=\"chipSubtitle(entity)\">\n\t\t\t\t<ViewColumnOutline :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"cardUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener\">{{ cardTitle(entity) }}</a>\n\t\t\t\t<span v-if=\"stackTitle(entity)\" class=\"cn-deck-card__chip-stack\">\n\t\t\t\t\t· {{ stackTitle(entity) }}\n\t\t\t\t</span>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-deck-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + most-recent -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"cards.length === 0\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-deck-card__headline\">\n\t\t\t\t<div class=\"cn-deck-card__headline-line\">\n\t\t\t\t\t<strong>{{ countHeadline }}</strong>\n\t\t\t\t</div>\n\t\t\t\t<ul v-if=\"stackDistribution.length > 0\" class=\"cn-deck-card__distribution\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"bucket in stackDistribution\"\n\t\t\t\t\t\t:key=\"bucket.key\"\n\t\t\t\t\t\tclass=\"cn-deck-card__distribution-row\">\n\t\t\t\t\t\t<span class=\"cn-deck-card__distribution-label\">{{ bucket.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-deck-card__distribution-count\">{{ bucket.count }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<div v-if=\"mostRecent\" class=\"cn-deck-card__headline-recent\">\n\t\t\t\t\t<ViewColumnOutline :size=\"14\" />\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"cardUrl(mostRecent)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener\">{{ cardTitle(mostRecent) }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: mini-kanban -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"cards.length === 0\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-deck-card__kanban\">\n\t\t\t\t<section\n\t\t\t\t\tv-for=\"stack in miniColumns\"\n\t\t\t\t\t:key=\"stack.key\"\n\t\t\t\t\tclass=\"cn-deck-card__column\">\n\t\t\t\t\t<header class=\"cn-deck-card__column-header\">\n\t\t\t\t\t\t<span class=\"cn-deck-card__column-title\">{{ stack.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-deck-card__column-count\">{{ stack.cards.length }}</span>\n\t\t\t\t\t</header>\n\t\t\t\t\t<ul class=\"cn-deck-card__column-list\">\n\t\t\t\t\t\t<li\n\t\t\t\t\t\t\tv-for=\"card in stack.cards\"\n\t\t\t\t\t\t\t:key=\"cardKey(card)\"\n\t\t\t\t\t\t\tclass=\"cn-deck-card__row\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-deck-card__row--highlight': isLinked(card) }\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t:href=\"cardUrl(card)\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener\"\n\t\t\t\t\t\t\t\tclass=\"cn-deck-card__title\">{{ cardTitle(card) }}</a>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\t\t\t</div>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnDeckCard — bespoke surface-aware widget for the `deck` integration.\n *\n * Renders Deck-aware metadata across all four surfaces. See the\n * file-level docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnDeckCard',\n\n\tcomponents: { CnDetailCard, NcLoadingIcon, ViewColumnOutline },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'deck'`). */\n\t\tintegrationId: { type: String, default: 'deck' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (card id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Cards') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ViewColumnOutline },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No cards linked yet') },\n\t\t/** Pre-translated unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Deck is currently unavailable.') },\n\t\t/** URL of the NC Deck app entry. */\n\t\tdeckAppUrl: { type: String, default: '/index.php/apps/deck' },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcards: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\tcardHeaderTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Cards grouped by stack for both the mini-kanban (detail-page)\n\t\t * and the stack-distribution list (dashboard surfaces).\n\t\t *\n\t\t * @return {Array<{key: string, label: string, cards: Array}>}\n\t\t */\n\t\tcolumns() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const card of this.cards) {\n\t\t\t\tconst stackId = card.stackId ?? card.stack?.id ?? 'unstacked'\n\t\t\t\tconst stackLabel = card.stackTitle\n\t\t\t\t\t?? card.stack?.title\n\t\t\t\t\t?? (stackId === 'unstacked'\n\t\t\t\t\t\t? t('nextcloud-vue', 'No stack')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Stack {n}', { n: stackId }))\n\t\t\t\tconst key = String(stackId)\n\t\t\t\tif (!groups.has(key)) {\n\t\t\t\t\tgroups.set(key, { key, label: stackLabel, cards: [] })\n\t\t\t\t}\n\t\t\t\tgroups.get(key).cards.push(card)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\n\t\t/**\n\t\t * Up to three columns for the mini-kanban view on `detail-page`.\n\t\t *\n\t\t * Per AD-2 (design.md): when the linked card has its own stack\n\t\t * we always include that stack; otherwise the first three stacks\n\t\t * by first-seen order are surfaced.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, cards: Array}>}\n\t\t */\n\t\tminiColumns() {\n\t\t\treturn this.columns.slice(0, 3)\n\t\t},\n\n\t\tstackDistribution() {\n\t\t\treturn this.columns.map((col) => ({\n\t\t\t\tkey: col.key,\n\t\t\t\tlabel: col.label,\n\t\t\t\tcount: col.cards.length,\n\t\t\t}))\n\t\t},\n\n\t\tcountHeadline() {\n\t\t\tconst total = this.cards.length\n\t\t\tconst stackCount = this.columns.length\n\t\t\tif (stackCount <= 1) {\n\t\t\t\treturn n('nextcloud-vue', '{count} card', '{count} cards', total, { count: total })\n\t\t\t}\n\t\t\tconst cardFragment = n('nextcloud-vue', '{count} card', '{count} cards', total, { count: total })\n\t\t\tconst stacksFragment = n('nextcloud-vue', 'across {count} stack', 'across {count} stacks', stackCount, { count: stackCount })\n\t\t\treturn `${cardFragment} ${stacksFragment}`\n\t\t},\n\n\t\tmostRecent() {\n\t\t\tif (this.cards.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst sorted = [...this.cards].sort((a, b) => {\n\t\t\t\tconst ta = Date.parse(a.linkedAt ?? a.lastActivity ?? '') || 0\n\t\t\t\tconst tb = Date.parse(b.linkedAt ?? b.lastActivity ?? '') || 0\n\t\t\t\treturn tb - ta\n\t\t\t})\n\t\t\treturn sorted[0]\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\tcardKey(card) {\n\t\t\treturn card.cardId ?? card.id ?? ''\n\t\t},\n\n\t\tcardTitle(card) {\n\t\t\treturn card.cardTitle ?? card.title ?? this.cardKey(card)\n\t\t},\n\n\t\tcardUrl(card) {\n\t\t\tif (card.url && safeHref(card.url) !== '#') {\n\t\t\t\treturn card.url\n\t\t\t}\n\t\t\tconst boardId = card.boardId ?? card.board?.id ?? ''\n\t\t\tconst cardId = card.cardId ?? card.id ?? ''\n\t\t\tif (boardId && cardId) {\n\t\t\t\treturn `/index.php/apps/deck/board/${boardId}/card/${cardId}`\n\t\t\t}\n\t\t\treturn this.deckAppUrl\n\t\t},\n\n\t\tstackTitle(card) {\n\t\t\treturn card.stackTitle ?? card.stack?.title ?? ''\n\t\t},\n\n\t\tisLinked(card) {\n\t\t\tif (!this.value) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn String(this.cardKey(card)) === String(this.value)\n\t\t},\n\n\t\tchipSubtitle(card) {\n\t\t\treturn this.stackTitle(card) || this.cardTitle(card)\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\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.degraded = ''\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\tconst data = await response.json()\n\t\t\t\t\tthis.cards = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.cards = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.cards = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnDeckCard] failed to fetch cards', err)\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (!this.value || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnDeckCard] failed to fetch single card', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-deck-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-deck-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-deck-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-deck-card__distribution {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-deck-card__distribution-row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-deck-card__distribution-count {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-deck-card__headline-recent {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__headline-recent a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-deck-card__headline-recent a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-deck-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 8px;\n\tborder-radius: 12px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-deck-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-deck-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-deck-card__chip-stack {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-deck-card__kanban {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: flex-start;\n}\n\n.cn-deck-card__column {\n\tflex: 1;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tpadding: 6px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-deck-card__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 4px;\n}\n\n.cn-deck-card__column-title {\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__column-count {\n\tfont-size: 0.7em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-deck-card__column-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n}\n\n.cn-deck-card__row {\n\tpadding: 4px 6px;\n\tborder-radius: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tfont-size: 0.8em;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__row--highlight {\n\tborder-color: var(--color-primary-element, #21468B);\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tfont-weight: 600;\n}\n\n.cn-deck-card__title {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\na.cn-deck-card__title:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDetailCard","t","n"],"mappings":"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"}
1
+ {"version":3,"file":"CnDeckCard.vue2.js","sources":["../../../../../src/integrations/builtin/deck/CnDeckCard.vue"],"sourcesContent":["<!--\n CnDeckCard — bespoke surface-aware widget for the `deck` integration.\n\n Replaces the generic CnIntegrationCard for the `deck` leaf. Branches on\n `surface` per AD-19:\n - user-dashboard / app-dashboard : headline \"N cards across M stacks\";\n secondary line names the most recent card.\n - detail-page : mini three-column kanban view of\n the linked card's board with the linked card highlighted in its\n current stack (per design.md AD-2).\n - single-entity : chip with card title + stack name\n (referenceType: 'deck').\n\n Pulls rows from the same OR pluggable-integration sub-resource as\n CnDeckTab; for `single-entity` the optional `value` prop addresses a\n single card by id (matching CnIntegrationCard's fetchSingle contract).\n\n See `openregister/openspec/changes/integration-deck/` for the spec\n delta and ADR-019 (registry mechanism), AD-19 (surface fallback),\n AD-2 (mini-kanban on detail-page).\n-->\n<template>\n\t<CnDetailCard :title=\"cardHeaderTitle\" :icon=\"cardIcon\" :collapsible=\"collapsible\">\n\t\t<!-- `headerActions`: the Deck tab's link and create flows in the title row (detail page). -->\n\t\t<template v-if=\"shownHeaderActions.length > 0\" #actions>\n\t\t\t<NcButton\n\t\t\t\tv-for=\"action in shownHeaderActions\"\n\t\t\t\t:key=\"action\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:data-testid=\"`cn-deck-card-${action}`\"\n\t\t\t\t@click.stop=\"action === 'create' ? (createOpen = true) : (pickerOpen = true)\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus v-if=\"action === 'create'\" :size=\"20\" />\n\t\t\t\t\t<LinkVariant v-else :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action === 'create' ? createLabel : linkLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\n\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t<!-- single-entity surface: chip -->\n\t\t<template v-else-if=\"surface === 'single-entity'\">\n\t\t\t<span v-if=\"entity\" class=\"cn-deck-card__chip\" :title=\"chipSubtitle(entity)\">\n\t\t\t\t<ViewColumnOutline :size=\"14\" />\n\t\t\t\t<a\n\t\t\t\t\t:href=\"cardUrl(entity)\"\n\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\trel=\"noopener\">{{ cardTitle(entity) }}</a>\n\t\t\t\t<span v-if=\"stackTitle(entity)\" class=\"cn-deck-card__chip-stack\">\n\t\t\t\t\t· {{ stackTitle(entity) }}\n\t\t\t\t</span>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-deck-card__empty\">{{ emptyLabel }}</span>\n\t\t</template>\n\n\t\t<!-- dashboard surfaces: headline + most-recent -->\n\t\t<template v-else-if=\"surface === 'user-dashboard' || surface === 'app-dashboard'\">\n\t\t\t<div v-if=\"degraded\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"cards.length === 0\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-deck-card__headline\">\n\t\t\t\t<div class=\"cn-deck-card__headline-line\">\n\t\t\t\t\t<strong>{{ countHeadline }}</strong>\n\t\t\t\t</div>\n\t\t\t\t<ul v-if=\"stackDistribution.length > 0\" class=\"cn-deck-card__distribution\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"bucket in stackDistribution\"\n\t\t\t\t\t\t:key=\"bucket.key\"\n\t\t\t\t\t\tclass=\"cn-deck-card__distribution-row\">\n\t\t\t\t\t\t<span class=\"cn-deck-card__distribution-label\">{{ bucket.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-deck-card__distribution-count\">{{ bucket.count }}</span>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t\t<div v-if=\"mostRecent\" class=\"cn-deck-card__headline-recent\">\n\t\t\t\t\t<ViewColumnOutline :size=\"14\" />\n\t\t\t\t\t<a\n\t\t\t\t\t\t:href=\"cardUrl(mostRecent)\"\n\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\trel=\"noopener\">{{ cardTitle(mostRecent) }}</a>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<!-- detail-page surface: mini-kanban -->\n\t\t<template v-else>\n\t\t\t<div v-if=\"degraded\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ degraded }}\n\t\t\t</div>\n\t\t\t<div v-else-if=\"cards.length === 0\" class=\"cn-deck-card__empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-deck-card__kanban\">\n\t\t\t\t<section\n\t\t\t\t\tv-for=\"stack in miniColumns\"\n\t\t\t\t\t:key=\"stack.key\"\n\t\t\t\t\tclass=\"cn-deck-card__column\">\n\t\t\t\t\t<header class=\"cn-deck-card__column-header\">\n\t\t\t\t\t\t<span class=\"cn-deck-card__column-title\">{{ stack.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-deck-card__column-count\">{{ stack.cards.length }}</span>\n\t\t\t\t\t</header>\n\t\t\t\t\t<ul class=\"cn-deck-card__column-list\">\n\t\t\t\t\t\t<li\n\t\t\t\t\t\t\tv-for=\"card in stack.cards\"\n\t\t\t\t\t\t\t:key=\"cardKey(card)\"\n\t\t\t\t\t\t\tclass=\"cn-deck-card__row\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-deck-card__row--highlight': isLinked(card) }\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t:href=\"cardUrl(card)\"\n\t\t\t\t\t\t\t\ttarget=\"_blank\"\n\t\t\t\t\t\t\t\trel=\"noopener\"\n\t\t\t\t\t\t\t\tclass=\"cn-deck-card__title\">{{ cardTitle(card) }}</a>\n\t\t\t\t\t\t</li>\n\t\t\t\t\t</ul>\n\t\t\t\t</section>\n\t\t\t</div>\n\t\t</template>\n\n\t\t<p v-if=\"actionError\" class=\"cn-deck-card__action-error\" role=\"alert\">\n\t\t\t{{ actionError }}\n\t\t</p>\n\n\t\t<CnDeckCardPicker\n\t\t\tv-if=\"pickerOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"pickerOpen = false\"\n\t\t\t@link=\"onLink\" />\n\n\t\t<CnDeckCardCreate\n\t\t\tv-if=\"createOpen\"\n\t\t\t:apiBase=\"apiBase\"\n\t\t\t@close=\"createOpen = false\"\n\t\t\t@create=\"onCreate\" />\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon } from '@nextcloud/vue'\nimport LinkVariant from 'vue-material-design-icons/LinkVariant.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'\nimport CnDeckCardCreate from '../../../components/CnDeckCardCreate/CnDeckCardCreate.vue'\nimport CnDeckCardPicker from '../../../components/CnDeckCardPicker/CnDeckCardPicker.vue'\nimport CnDetailCard from '../../../components/CnDetailCard/CnDetailCard.vue'\nimport { buildHeaders, prefixUrl, safeHref } from '../../../utils/index.js'\n\nconst VALID_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/**\n * CnDeckCard — bespoke surface-aware widget for the `deck` integration.\n *\n * Renders Deck-aware metadata across all four surfaces. See the\n * file-level docblock for surface-by-surface behaviour.\n */\nexport default {\n\tname: 'CnDeckCard',\n\n\tcomponents: { CnDeckCardCreate, CnDeckCardPicker, CnDetailCard, LinkVariant, NcButton, NcLoadingIcon, Plus, ViewColumnOutline },\n\n\tprops: {\n\t\t/** Stable integration id (forwarded from the registry — always `'deck'`). */\n\t\tintegrationId: { type: String, default: 'deck' },\n\t\t/** OpenRegister register id (slug or uuid). */\n\t\tregister: { type: String, required: true },\n\t\t/** OpenRegister schema id (slug or uuid). */\n\t\tschema: { type: String, required: true },\n\t\t/** Parent object id. */\n\t\tobjectId: { type: String, required: true },\n\t\t/** Rendering surface (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (s) => VALID_SURFACES.includes(s),\n\t\t},\n\n\t\t/** Optional single-entity reference (card id). */\n\t\tvalue: { type: [String, Number], default: '' },\n\t\t/** Pre-translated card title. */\n\t\ttitle: { type: String, default: () => t('nextcloud-vue', 'Cards') },\n\t\t/** Optional Material Design Icon component. */\n\t\ticon: { type: Object, default: () => ViewColumnOutline },\n\t\t/** Base API URL. */\n\t\tapiBase: { type: String, default: '/apps/openregister/api' },\n\t\t/** Whether the card body is collapsible. */\n\t\tcollapsible: { type: Boolean, default: true },\n\t\t/** Pre-translated empty-state label. */\n\t\temptyLabel: { type: String, default: () => t('nextcloud-vue', 'No cards linked yet') },\n\t\t/** Pre-translated unavailable label. */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'NC Deck is currently unavailable.') },\n\t\t/** URL of the NC Deck app entry. */\n\t\tdeckAppUrl: { type: String, default: '/index.php/apps/deck' },\n\t\t/**\n\t\t * Buttons in the card's title row on a detail page: `link` links an\n\t\t * existing Deck card, `create` makes a new one linked to the object.\n\t\t * In the order given; none by default.\n\t\t */\n\t\theaderActions: { type: Array, default: () => [] },\n\t\t/** Label of the `link` button. */\n\t\tlinkLabel: { type: String, default: () => t('nextcloud-vue', 'Link an existing card') },\n\t\t/** Label of the `create` button. */\n\t\tcreateLabel: { type: String, default: () => t('nextcloud-vue', 'New card') },\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tcards: [],\n\t\t\tentity: null,\n\t\t\tloading: false,\n\t\t\tdegraded: '',\n\t\t\tpickerOpen: false,\n\t\t\tcreateOpen: false,\n\t\t\tactionError: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The title-row buttons to draw: only on a detail page with an\n\t\t * object, only the known ones, in the order given.\n\t\t *\n\t\t * @return {Array<string>}\n\t\t */\n\t\tshownHeaderActions() {\n\t\t\tif (this.surface !== 'detail-page' || !this.objectId || !Array.isArray(this.headerActions)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn this.headerActions.filter((action, index, all) => ['create', 'link'].includes(action) && all.indexOf(action) === index)\n\t\t},\n\n\t\tcardHeaderTitle() {\n\t\t\treturn this.title || this.integrationId\n\t\t},\n\n\t\tcardIcon() {\n\t\t\treturn this.icon\n\t\t},\n\n\t\t/**\n\t\t * Cards grouped by stack for both the mini-kanban (detail-page)\n\t\t * and the stack-distribution list (dashboard surfaces).\n\t\t *\n\t\t * @return {Array<{key: string, label: string, cards: Array}>}\n\t\t */\n\t\tcolumns() {\n\t\t\tconst groups = new Map()\n\t\t\tfor (const card of this.cards) {\n\t\t\t\tconst stackId = card.stackId ?? card.stack?.id ?? 'unstacked'\n\t\t\t\tconst stackLabel = card.stackTitle\n\t\t\t\t\t?? card.stack?.title\n\t\t\t\t\t?? (stackId === 'unstacked'\n\t\t\t\t\t\t? t('nextcloud-vue', 'No stack')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Stack {n}', { n: stackId }))\n\t\t\t\tconst key = String(stackId)\n\t\t\t\tif (!groups.has(key)) {\n\t\t\t\t\tgroups.set(key, { key, label: stackLabel, cards: [] })\n\t\t\t\t}\n\t\t\t\tgroups.get(key).cards.push(card)\n\t\t\t}\n\t\t\treturn Array.from(groups.values())\n\t\t},\n\n\t\t/**\n\t\t * Up to three columns for the mini-kanban view on `detail-page`.\n\t\t *\n\t\t * Per AD-2 (design.md): when the linked card has its own stack\n\t\t * we always include that stack; otherwise the first three stacks\n\t\t * by first-seen order are surfaced.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, cards: Array}>}\n\t\t */\n\t\tminiColumns() {\n\t\t\treturn this.columns.slice(0, 3)\n\t\t},\n\n\t\tstackDistribution() {\n\t\t\treturn this.columns.map((col) => ({\n\t\t\t\tkey: col.key,\n\t\t\t\tlabel: col.label,\n\t\t\t\tcount: col.cards.length,\n\t\t\t}))\n\t\t},\n\n\t\tcountHeadline() {\n\t\t\tconst total = this.cards.length\n\t\t\tconst stackCount = this.columns.length\n\t\t\tif (stackCount <= 1) {\n\t\t\t\treturn n('nextcloud-vue', '{count} card', '{count} cards', total, { count: total })\n\t\t\t}\n\t\t\tconst cardFragment = n('nextcloud-vue', '{count} card', '{count} cards', total, { count: total })\n\t\t\tconst stacksFragment = n('nextcloud-vue', 'across {count} stack', 'across {count} stacks', stackCount, { count: stackCount })\n\t\t\treturn `${cardFragment} ${stacksFragment}`\n\t\t},\n\n\t\tmostRecent() {\n\t\t\tif (this.cards.length === 0) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst sorted = [...this.cards].sort((a, b) => {\n\t\t\t\tconst ta = Date.parse(a.linkedAt ?? a.lastActivity ?? '') || 0\n\t\t\t\tconst tb = Date.parse(b.linkedAt ?? b.lastActivity ?? '') || 0\n\t\t\t\treturn tb - ta\n\t\t\t})\n\t\t\treturn sorted[0]\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectId: { immediate: true, handler() {\n\t\t\tthis.fetch()\n\t\t} },\n\n\t\tsurface() {\n\t\t\tthis.fetch()\n\t\t},\n\n\t\tvalue() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/** @return {string} The object's Deck endpoint the link and create flows post to. */\n\t\tdeckEndpoint() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/deck`\n\t\t},\n\n\t\t/**\n\t\t * Link the picked card, then read the list again.\n\t\t *\n\t\t * @param {object} payload `{ cardId }` from the picker.\n\t\t */\n\t\tasync onLink(payload) {\n\t\t\tthis.pickerOpen = false\n\t\t\tawait this.post(this.deckEndpoint(), payload, t('nextcloud-vue', 'Could not link card.'))\n\t\t},\n\n\t\t/**\n\t\t * Create the card linked to the object, then read the list again.\n\t\t *\n\t\t * @param {object} payload `{ boardId, stackId, title, ... }` from the form.\n\t\t */\n\t\tasync onCreate(payload) {\n\t\t\tthis.createOpen = false\n\t\t\tawait this.post(`${this.deckEndpoint()}/new`, payload, t('nextcloud-vue', 'Could not create card.'))\n\t\t},\n\n\t\t/**\n\t\t * POST one flow's payload and reload on success.\n\t\t *\n\t\t * @param {string} url The endpoint.\n\t\t * @param {object} payload The body.\n\t\t * @param {string} failure The sentence shown when it fails.\n\t\t */\n\t\tasync post(url, payload, failure) {\n\t\t\tthis.actionError = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\theaders: { ...buildHeaders(), 'Content-Type': 'application/json' },\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\tawait this.fetchList()\n\t\t\t\t} else if (response.status === 409) {\n\t\t\t\t\tthis.actionError = t('nextcloud-vue', 'This card is already linked.')\n\t\t\t\t} else {\n\t\t\t\t\tthis.actionError = failure\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.actionError = failure\n\t\t\t}\n\t\t},\n\n\t\tbaseUrl() {\n\t\t\treturn `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/integrations/${this.integrationId}`\n\t\t},\n\n\t\tcardKey(card) {\n\t\t\treturn card.cardId ?? card.id ?? ''\n\t\t},\n\n\t\tcardTitle(card) {\n\t\t\treturn card.cardTitle ?? card.title ?? this.cardKey(card)\n\t\t},\n\n\t\tcardUrl(card) {\n\t\t\tif (card.url && safeHref(card.url) !== '#') {\n\t\t\t\treturn card.url\n\t\t\t}\n\t\t\tconst boardId = card.boardId ?? card.board?.id ?? ''\n\t\t\tconst cardId = card.cardId ?? card.id ?? ''\n\t\t\tif (boardId && cardId) {\n\t\t\t\treturn `/index.php/apps/deck/board/${boardId}/card/${cardId}`\n\t\t\t}\n\t\t\treturn this.deckAppUrl\n\t\t},\n\n\t\tstackTitle(card) {\n\t\t\treturn card.stackTitle ?? card.stack?.title ?? ''\n\t\t},\n\n\t\tisLinked(card) {\n\t\t\tif (!this.value) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn String(this.cardKey(card)) === String(this.value)\n\t\t},\n\n\t\tchipSubtitle(card) {\n\t\t\treturn this.stackTitle(card) || this.cardTitle(card)\n\t\t},\n\n\t\tfetch() {\n\t\t\tif (this.surface === 'single-entity') {\n\t\t\t\tthis.fetchSingle()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.fetchList()\n\t\t},\n\n\t\tasync fetchList() {\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.degraded = ''\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\tconst data = await response.json()\n\t\t\t\t\tthis.cards = data.results || data.items || (Array.isArray(data) ? data : []) || []\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.cards = []\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.cards = []\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnDeckCard] failed to fetch cards', err)\n\t\t\t\tthis.cards = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync fetchSingle() {\n\t\t\tif (!this.value || !this.register || !this.schema || !this.objectId) {\n\t\t\t\tthis.entity = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.degraded = ''\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(`${this.baseUrl()}/${encodeURIComponent(this.value)}`), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.entity = await response.json()\n\t\t\t\t} else if (response.status === 503) {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t\tthis.degraded = this.unavailableLabel\n\t\t\t\t} else {\n\t\t\t\t\tthis.entity = null\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.error('[CnDeckCard] failed to fetch single card', err)\n\t\t\t\tthis.entity = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-deck-card__action-error {\n\tmargin: 8px 0 0;\n\tcolor: var(--color-error-text, var(--color-error));\n}\n\n.cn-deck-card__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tpadding: 8px 0;\n}\n\n.cn-deck-card__headline {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-deck-card__headline-line {\n\tfont-size: 1.1em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-deck-card__distribution {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-deck-card__distribution-row {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px;\n\tfont-size: 0.85em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-deck-card__distribution-count {\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-deck-card__headline-recent {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 0.9em;\n\tcolor: var(--color-text-maxcontrast);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__headline-recent a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-deck-card__headline-recent a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-deck-card__chip {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 8px;\n\tborder-radius: 12px;\n\tbackground: var(--color-background-hover);\n\tfont-size: 0.9em;\n\tmax-width: 100%;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n/* stylelint-disable-next-line no-descending-specificity */\n.cn-deck-card__chip a {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n.cn-deck-card__chip a:hover {\n\ttext-decoration: underline;\n}\n\n.cn-deck-card__chip-stack {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-deck-card__kanban {\n\tdisplay: flex;\n\tgap: 8px;\n\talign-items: flex-start;\n}\n\n.cn-deck-card__column {\n\tflex: 1;\n\tmin-width: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tpadding: 6px;\n\tborder-radius: var(--border-radius);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-deck-card__column-header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 4px;\n}\n\n.cn-deck-card__column-title {\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__column-count {\n\tfont-size: 0.7em;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-deck-card__column-list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n}\n\n.cn-deck-card__row {\n\tpadding: 4px 6px;\n\tborder-radius: 4px;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tfont-size: 0.8em;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n.cn-deck-card__row--highlight {\n\tborder-color: var(--color-primary-element, #21468B);\n\tbackground: var(--color-primary-element-light, var(--color-background-darker));\n\tfont-weight: 600;\n}\n\n.cn-deck-card__title {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\na.cn-deck-card__title:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnDeckCardCreate","CnDeckCardPicker","CnDetailCard","t","n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createBlock, withCtx, createElementBlock, Fragment, createVNode, createElementVNode, toDisplayString, createCommentVNode, renderList, normalizeClass } from 'vue';
1
+ import { resolveComponent, openBlock, createBlock, createSlots, withCtx, createElementBlock, Fragment, createVNode, createElementVNode, toDisplayString, createCommentVNode, renderList, normalizeClass, withModifiers, createTextVNode } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["title"];
4
4
  const _hoisted_2 = ["href"];
@@ -51,17 +51,27 @@ const _hoisted_18 = { class: "cn-deck-card__column-title" };
51
51
  const _hoisted_19 = { class: "cn-deck-card__column-count" };
52
52
  const _hoisted_20 = { class: "cn-deck-card__column-list" };
53
53
  const _hoisted_21 = ["href"];
54
+ const _hoisted_22 = {
55
+ key: 4,
56
+ class: "cn-deck-card__action-error",
57
+ role: "alert"
58
+ };
54
59
 
55
60
  function render(_ctx, _cache, $props, $setup, $data, $options) {
61
+ const _component_Plus = resolveComponent("Plus");
62
+ const _component_LinkVariant = resolveComponent("LinkVariant");
63
+ const _component_NcButton = resolveComponent("NcButton");
56
64
  const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
57
65
  const _component_ViewColumnOutline = resolveComponent("ViewColumnOutline");
66
+ const _component_CnDeckCardPicker = resolveComponent("CnDeckCardPicker");
67
+ const _component_CnDeckCardCreate = resolveComponent("CnDeckCardCreate");
58
68
  const _component_CnDetailCard = resolveComponent("CnDetailCard");
59
69
 
60
70
  return (openBlock(), createBlock(_component_CnDetailCard, {
61
71
  title: $options.cardHeaderTitle,
62
72
  icon: $options.cardIcon,
63
73
  collapsible: $props.collapsible
64
- }, {
74
+ }, createSlots({
65
75
  default: withCtx(() => [
66
76
  ($data.loading)
67
77
  ? (openBlock(), createBlock(_component_NcLoadingIcon, { key: 0 }))
@@ -153,10 +163,62 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
153
163
  ]))
154
164
  }), 128 /* KEYED_FRAGMENT */))
155
165
  ]))
156
- ], 64 /* STABLE_FRAGMENT */))
166
+ ], 64 /* STABLE_FRAGMENT */)),
167
+ ($data.actionError)
168
+ ? (openBlock(), createElementBlock("p", _hoisted_22, toDisplayString($data.actionError), 1 /* TEXT */))
169
+ : createCommentVNode("v-if", true),
170
+ ($data.pickerOpen)
171
+ ? (openBlock(), createBlock(_component_CnDeckCardPicker, {
172
+ key: 5,
173
+ apiBase: $props.apiBase,
174
+ onClose: _cache[0] || (_cache[0] = $event => ($data.pickerOpen = false)),
175
+ onLink: $options.onLink
176
+ }, null, 8 /* PROPS */, ["apiBase", "onLink"]))
177
+ : createCommentVNode("v-if", true),
178
+ ($data.createOpen)
179
+ ? (openBlock(), createBlock(_component_CnDeckCardCreate, {
180
+ key: 6,
181
+ apiBase: $props.apiBase,
182
+ onClose: _cache[1] || (_cache[1] = $event => ($data.createOpen = false)),
183
+ onCreate: $options.onCreate
184
+ }, null, 8 /* PROPS */, ["apiBase", "onCreate"]))
185
+ : createCommentVNode("v-if", true)
157
186
  ]),
158
- _: 1 /* STABLE */
159
- }, 8 /* PROPS */, ["title", "icon", "collapsible"]))
187
+ _: 2 /* DYNAMIC */
188
+ }, [
189
+ ($options.shownHeaderActions.length > 0)
190
+ ? {
191
+ name: "actions",
192
+ fn: withCtx(() => [
193
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.shownHeaderActions, (action) => {
194
+ return (openBlock(), createBlock(_component_NcButton, {
195
+ key: action,
196
+ variant: "secondary",
197
+ "data-testid": `cn-deck-card-${action}`,
198
+ onClick: withModifiers($event => (action === 'create' ? ($data.createOpen = true) : ($data.pickerOpen = true)), ["stop"])
199
+ }, {
200
+ icon: withCtx(() => [
201
+ (action === 'create')
202
+ ? (openBlock(), createBlock(_component_Plus, {
203
+ key: 0,
204
+ size: 20
205
+ }))
206
+ : (openBlock(), createBlock(_component_LinkVariant, {
207
+ key: 1,
208
+ size: 20
209
+ }))
210
+ ]),
211
+ default: withCtx(() => [
212
+ createTextVNode(" " + toDisplayString(action === 'create' ? $props.createLabel : $props.linkLabel), 1 /* TEXT */)
213
+ ]),
214
+ _: 2 /* DYNAMIC */
215
+ }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["data-testid", "onClick"]))
216
+ }), 128 /* KEYED_FRAGMENT */))
217
+ ]),
218
+ key: "0"
219
+ }
220
+ : undefined
221
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "icon", "collapsible"]))
160
222
  }
161
223
 
162
224
  export { render };