@conduction/nextcloud-vue 2.56.0-beta.34 → 2.56.0-beta.36

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 (155) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  6. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  7. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  8. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  10. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  11. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  17. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  23. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  24. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  35. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  36. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  39. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  45. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  51. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  52. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  54. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  56. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  57. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  59. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  61. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
  62. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  65. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  66. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  68. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  74. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  75. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  78. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  79. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  81. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  82. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  83. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  84. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  85. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  86. package/dist/esm/index.js +1 -0
  87. package/dist/esm/index.js.map +1 -1
  88. package/dist/esm/l10n/en.json.js +16 -1
  89. package/dist/esm/l10n/en.json.js.map +1 -1
  90. package/dist/esm/l10n/nl.json.js +38 -1
  91. package/dist/esm/l10n/nl.json.js.map +1 -1
  92. package/dist/esm/library-style.js +1 -1
  93. package/dist/esm/nextcloud-vue.css +525 -318
  94. package/dist/esm/utils/buildManifest.js +37 -3
  95. package/dist/esm/utils/buildManifest.js.map +1 -1
  96. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  97. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  98. package/dist/esm/utils/formatMetric.js +39 -3
  99. package/dist/esm/utils/formatMetric.js.map +1 -1
  100. package/dist/esm/utils/gridStack.js +40 -1
  101. package/dist/esm/utils/gridStack.js.map +1 -1
  102. package/dist/esm/utils/humaniseKey.js +41 -0
  103. package/dist/esm/utils/humaniseKey.js.map +1 -0
  104. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  105. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  106. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  107. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  108. package/dist/esm/utils/sentinelTokens.js +9 -0
  109. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  110. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  111. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  112. package/dist/nextcloud-vue.cjs.js +2250 -708
  113. package/dist/nextcloud-vue.cjs.js.map +2 -2
  114. package/dist/nextcloud-vue.css +525 -318
  115. package/l10n/en.json +16 -1
  116. package/l10n/nl.json +38 -1
  117. package/package.json +1 -1
  118. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  119. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  120. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  121. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  122. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
  123. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  124. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  125. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  126. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  127. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  128. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  129. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  130. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  131. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  132. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  133. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  134. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  135. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  136. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  137. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  138. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  139. package/src/css/form-field.css +14 -0
  140. package/src/css/form-widget.css +11 -0
  141. package/src/css/kpi-card.css +13 -0
  142. package/src/css/look-board-reports.css +58 -0
  143. package/src/css/look-board.css +31 -0
  144. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  145. package/src/index.js +2 -0
  146. package/src/schemas/app-manifest-v2.schema.json +23 -2
  147. package/src/utils/buildManifest.js +36 -2
  148. package/src/utils/detailWidgetVisibility.js +145 -0
  149. package/src/utils/formatMetric.js +38 -2
  150. package/src/utils/gridStack.js +39 -0
  151. package/src/utils/humaniseKey.js +38 -0
  152. package/src/utils/notificationRuleLabel.js +5 -23
  153. package/src/utils/resolveManifestTokens.js +1 -0
  154. package/src/utils/sentinelTokens.js +9 -0
  155. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CnRecordListWidget.vue3.js","sources":["../../../../src/components/CnRecordListWidget/CnRecordListWidget.vue?vue&type=template&id=519e7f1c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-record-list\"\n\t\t:class=\"`cn-record-list--${presentation}`\"\n\t\tdata-testid=\"cn-record-list\">\n\t\t<p v-if=\"error\" class=\"cn-record-list__error\" data-testid=\"cn-record-list-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && rows.length === 0\" class=\"cn-record-list__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"rows.length === 0\" class=\"cn-record-list__empty\" data-testid=\"cn-record-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-record-list__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"item in items\"\n\t\t\t\t:key=\"item.key\"\n\t\t\t\tclass=\"cn-record-list__item\"\n\t\t\t\tdata-testid=\"cn-record-list-item\">\n\t\t\t\t<!-- rows with `rowAction`: the title is the link, and one action\n\t\t\t\t ends the row instead of the chevron (\"Beoordelen\"). -->\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"presentation === 'rows' && item.action\"\n\t\t\t\t\tclass=\"cn-record-list__row cn-record-list__row--action\">\n\t\t\t\t\t<span v-if=\"item.initials\"\n\t\t\t\t\t\tclass=\"cn-record-list__avatar\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-avatar\">{{ item.initials }}</span>\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\t\t\tv-if=\"item.title\"\n\t\t\t\t\t\t\tclass=\"cn-record-list__title\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-record-list__title--link': item.tag !== 'div' }\"\n\t\t\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t\t</component>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.badges.length > 0\" class=\"cn-record-list__pills\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-for=\"badge in item.badges\"\n\t\t\t\t\t\t\t:key=\"badge.key\"\n\t\t\t\t\t\t\t:label=\"badge.label\"\n\t\t\t\t\t\t\t:variant=\"badge.variant\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-record-list-badge-${badge.kind}`\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"item.due\"\n\t\t\t\t\t\tclass=\"cn-record-list__due cn-record-list__due--column\"\n\t\t\t\t\t\t:class=\"`cn-record-list__due--${item.due.tone}`\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-due\">{{ item.due.text }}</span>\n\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"item.action.tag\"\n\t\t\t\t\t\tclass=\"cn-record-list__action\"\n\t\t\t\t\t\tv-bind=\"item.action.attrs\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-action\"\n\t\t\t\t\t\tv-on=\"item.action.tag === 'button' ? { click: () => runRowAction(item.row) } : {}\">\n\t\t\t\t\t\t{{ item.action.label }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t\t<!-- rows: title and meta, then the pill, the date and a chevron. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\tv-else-if=\"presentation === 'rows'\"\n\t\t\t\t\tclass=\"cn-record-list__row\"\n\t\t\t\t\t:class=\"{ 'cn-record-list__row--link': item.tag !== 'div' }\"\n\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t<span v-if=\"item.initials\"\n\t\t\t\t\t\tclass=\"cn-record-list__avatar\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-avatar\">{{ item.initials }}</span>\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<span v-if=\"item.title\" class=\"cn-record-list__title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.badges.length > 0 || item.due || item.trailing\" class=\"cn-record-list__aside\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-for=\"badge in item.badges\"\n\t\t\t\t\t\t\t:key=\"badge.key\"\n\t\t\t\t\t\t\t:label=\"badge.label\"\n\t\t\t\t\t\t\t:variant=\"badge.variant\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-record-list-badge-${badge.kind}`\" />\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"item.due\"\n\t\t\t\t\t\t\tclass=\"cn-record-list__due\"\n\t\t\t\t\t\t\t:class=\"`cn-record-list__due--${item.due.tone}`\"\n\t\t\t\t\t\t\tdata-testid=\"cn-record-list-due\">{{ item.due.text }}</span>\n\t\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<ChevronRight\n\t\t\t\t\t\tv-if=\"item.tag !== 'div'\"\n\t\t\t\t\t\tclass=\"cn-record-list__chevron\"\n\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t</component>\n\n\t\t\t\t<!-- feed: a round icon, a meta line and the text. -->\n\t\t\t\t<div v-else-if=\"presentation === 'feed'\" class=\"cn-record-list__feed-row\">\n\t\t\t\t\t<span class=\"cn-record-list__feed-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon :name=\"item.icon\" :size=\"18\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__main\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t\t<span v-if=\"item.text\" class=\"cn-record-list__text\">{{ item.text }}</span>\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- inline: a label and the title, the title a link. -->\n\t\t\t\t<div v-else class=\"cn-record-list__inline-row\">\n\t\t\t\t\t<span v-if=\"item.label\" class=\"cn-record-list__label\">{{ item.label }}</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__inline-title\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { actionLink, dispatchAction, interpolateActionTarget } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { ageInDays } from '../../utils/boardDate.js'\nimport { resolveDue } from '../../utils/dueText.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { initialsOf } from '../../utils/initials.js'\nimport { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'\nimport { pageFilter, pageSort } from '../../utils/pageControls.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\nconst PRESENTATIONS = ['rows', 'feed', 'inline']\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * CnRecordListWidget — the objects of another schema that point at this\n * record, drawn as the boards draw them (screens-relation-pages).\n *\n * A person page lists its open cases (\"Verlichting fietspad Lindelaan,\n * 2026-0082 · Woo-verzoek, In behandeling, Termijn 1 nov\"), its latest contact\n * moments (an icon by channel, a meta line, the summary) and its relations\n * (\"Secretaris van Buurtvereniging Oost\"). Each is a list of another schema's\n * objects filtered on a reference to this record. The source keys are those of\n * `object-list` (`register`, `schema`, `filter` with the `@objectId` /\n * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,\n * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name\n * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`\n * or `inline`, and each line is a template (`\"{identifier} · {caseTypeName}\"`,\n * `\"Termijn {deadline|date}\"`).\n *\n * A content-only catalog type: on a card surface the detail host or the\n * dashboard page draws the titled card. The widget emits `header` with `{ link, note }` (the\n * `headerLink` and `headerNote` with `{total}` filled in) for that card's\n * header.\n */\nexport default {\n\tname: 'CnRecordListWidget',\n\n\tcomponents: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },\n\n\tinject: {\n\t\t/** Detail-page object context (`{ objectId, object, register, schema }`). */\n\t\tcnObjectContext: { default: null },\n\t\t/** Page workspace bag, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's app config map; its `currency` is the reporting currency of a money token. */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/** The page's pre-bound action dispatcher (CnPageRenderer), for `rowAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The list config: `{ register, schema, filter?, sort?, limit?, extend?,\n\t\t * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,\n\t\t * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText? }`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\trows: [],\n\t\t\ttotal: 0,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Personal-lens reports (`@self.lenses`) of the latest response. */\n\t\t\tlenses: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} `rows`, `feed` or `inline`. */\n\t\tpresentation() {\n\t\t\tconst value = this.content?.presentation\n\t\t\treturn PRESENTATIONS.includes(value) ? value : 'rows'\n\t\t},\n\n\t\t/** @return {object} The token context: record, id and workspace. */\n\t\ttokenCtx() {\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The filter with its tokens resolved. */\n\t\tresolvedFilter() {\n\t\t\tconst filter = pageFilter(this.content?.pageControls, this.tokenCtx.workspace, this.content?.filter)\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))\n\t\t},\n\n\t\t/** @return {boolean} A required token (the record id, say) has no value yet. */\n\t\twaiting() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/** @return {object|null} The sort: the page's sort control when the widget listens to one, else its own. */\n\t\teffectiveSort() {\n\t\t\treturn pageSort(this.content?.pageControls, this.tokenCtx.workspace, this.content?.sort)\n\t\t},\n\n\t\t/**\n\t\t * What a template may read beside the row: the app config, whose\n\t\t * `currency` is the reporting currency of a `{path|currency}` token.\n\t\t *\n\t\t * @return {{config: object}} The template context.\n\t\t * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n\t\t */\n\t\ttemplateContext() {\n\t\t\treturn { config: unwrapAppConfig(this.cnAppConfig) }\n\t\t},\n\n\t\t/** @return {number} The fetch cap. */\n\t\tlimit() {\n\t\t\tconst value = Number(this.content?.limit)\n\t\t\treturn Number.isFinite(value) && value > 0 ? Math.floor(value) : 5\n\t\t},\n\n\t\t/** @return {string} What a fetch depends on; a change fetches again. */\n\t\trequestKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])\n\t\t},\n\n\t\t/** @return {Array<object>} The rows ready to draw. */\n\t\titems() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn this.rows.slice(0, this.limit).map((row, index) => {\n\t\t\t\tconst link = this.rowLink(row)\n\t\t\t\treturn {\n\t\t\t\t\tkey: (row && (row.id || row['@self']?.id)) || `row-${index}`,\n\t\t\t\t\trow,\n\t\t\t\t\taction: this.rowActionOf(row),\n\t\t\t\t\ttag: link ? 'router-link' : 'div',\n\t\t\t\t\tattrs: link ? { to: link } : {},\n\t\t\t\t\tinitials: this.initialsFor(row),\n\t\t\t\t\ttitle: this.fill(c.title, row),\n\t\t\t\t\tmeta: this.metaOf(row),\n\t\t\t\t\ttext: this.fill(c.text, row),\n\t\t\t\t\tlabel: this.fill(c.label, row),\n\t\t\t\t\ttrailing: c.trailing ? firstFilled(c.trailing, row, this.translate, new Date(), this.templateContext) : '',\n\t\t\t\t\tpill: this.pillOf(row),\n\t\t\t\t\tbadges: this.badgesOf(row),\n\t\t\t\t\tdue: this.dueOf(row),\n\t\t\t\t\ticon: this.iconOf(row),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** @return {object|null} The header link with `{total}` filled and tokens resolved. */\n\t\theaderLink() {\n\t\t\tconst link = this.content?.headerLink\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = { label: this.withTotal(link.label) }\n\t\t\tif (link.route) {\n\t\t\t\tresolved.route = link.route\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tresolved.params = resolveFilterTokens(link.params, this.tokenCtx)\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tresolved.query = resolveFilterTokens(link.query, this.tokenCtx)\n\t\t\t\t}\n\t\t\t} else if (typeof link.href === 'string') {\n\t\t\t\tresolved.href = link.href\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/** @return {string} The header note with `{total}` filled. */\n\t\theaderNote() {\n\t\t\tconst note = this.content?.headerNote\n\t\t\treturn typeof note === 'string' && note !== '' ? this.withTotal(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * Why the list is empty when a personal lens could not answer\n\t\t * (`@self.lenses.<lens>.available: false`, as for `filter: { _recent: true }`),\n\t\t * or '' to keep `emptyText`. `lensReasonTexts` holds the app's wording,\n\t\t * keyed `<lens>.<reason>` or `<reason>`.\n\t\t *\n\t\t * @return {string} The explanation, or ''.\n\t\t * @spec openspec/changes/screens-record-list-lens-reason/specs/record-list-lens-reason/spec.md#requirement-a-record-list-says-why-a-lens-is-empty\n\t\t */\n\t\tlensReasonText() {\n\t\t\tif (this.rows.length > 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst text = lensUnavailableText(this.lenses, this.content?.lensReasonTexts)\n\t\t\treturn text ? this.translate(text) : ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyLabel() {\n\t\t\tif (this.lensReasonText !== '') {\n\t\t\t\treturn this.lensReasonText\n\t\t\t}\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error line. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The spinner's accessible name. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\t},\n\n\twatch: {\n\t\trequestKey() {\n\t\t\tthis.fetchRows()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.fetchRows()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t\tthis.fetchRows()\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The initials of the row's avatar (`avatar`: a name template such as\n\t\t * `\"{client.name}\"`, or `{ text }`), or '' without one.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} One or two letters.\n\t\t * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar\n\t\t */\n\t\tinitialsFor(row) {\n\t\t\tconst avatar = this.content?.avatar\n\t\t\tconst template = typeof avatar === 'string' ? avatar : (avatar && typeof avatar === 'object' ? avatar.text : '')\n\t\t\treturn initialsOf(this.fill(template, row))\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\treturn typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), row, new Date(), this.templateContext) : ''\n\t\t},\n\n\t\t/**\n\t\t * Replace `{total}` in a translated label.\n\t\t *\n\t\t * @param {string} label The label.\n\t\t * @return {string} The text.\n\t\t */\n\t\twithTotal(label) {\n\t\t\treturn this.translate(label).replace(/\\{total\\}/g, String(this.total))\n\t\t},\n\n\t\t/**\n\t\t * The route a row opens: `rowRoute` with the id at `rowIdField`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The router location, or null.\n\t\t */\n\t\t/**\n\t\t * The one action that ends a row (`rowAction`): its label (from\n\t\t * `labels.map` by the row's `labels.field`, else `label`), and how it\n\t\t * renders: a link when it only navigates, else a button that\n\t\t * dispatches. Null without the key or a label.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{label: string, tag: string, attrs: object}|null}\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trowActionOf(row) {\n\t\t\tconst action = this.content?.rowAction\n\t\t\tif (!action || typeof action !== 'object' || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet label = typeof action.label === 'string' ? action.label : ''\n\t\t\tconst by = action.labels\n\t\t\tif (by && typeof by === 'object' && typeof by.field === 'string' && by.map && typeof by.map === 'object') {\n\t\t\t\tconst raw = readPath(row, by.field)\n\t\t\t\tif (raw !== null && raw !== undefined && typeof by.map[String(raw)] === 'string') {\n\t\t\t\t\tlabel = by.map[String(raw)]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlabel = this.translate(label)\n\t\t\tconst link = this.rowActionLink(action, row)\n\t\t\tif (link) {\n\t\t\t\treturn link.external\n\t\t\t\t\t? { label, tag: 'a', attrs: { href: link.href, target: '_blank', rel: 'noopener' } }\n\t\t\t\t\t: { label, tag: 'router-link', attrs: { to: link.to } }\n\t\t\t}\n\t\t\treturn { label, tag: 'button', attrs: { type: 'button' } }\n\t\t},\n\n\t\t/**\n\t\t * The link a `rowAction` that only navigates renders as: `open-page`\n\t\t * opens that page for the row's id, `navigate` reads its\n\t\t * `@object.<field>` / `@objectId` tokens off the row. Null for any\n\t\t * other action, which is dispatched on click.\n\t\t *\n\t\t * @param {object} action The declared row action.\n\t\t * @param {object} row The row.\n\t\t * @return {{href: string, to: (string|object|null), external: boolean}|null}\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trowActionLink(action, row) {\n\t\t\tif (action.confirm) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (action.type === 'open-page') {\n\t\t\t\tconst id = this.rowIdOf(row)\n\t\t\t\tif (!this.$router || id === null || typeof action.target !== 'string' || action.target === '') {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst to = { name: action.target, params: { id } }\n\t\t\t\ttry {\n\t\t\t\t\tconst href = this.$router.resolve(to).href\n\t\t\t\t\treturn href ? { href, to, external: false } : null\n\t\t\t\t} catch {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (action.type !== 'navigate') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst target = interpolateActionTarget(action.target, this.rowTokenCtx(row))\n\t\t\t// An external target needs no router; an in-app one does.\n\t\t\treturn actionLink({ ...action, target }, { router: this.$router || null })\n\t\t},\n\n\t\t/**\n\t\t * The token context of a row: the row is `@object`, its id `@objectId`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object} The token context.\n\t\t */\n\t\trowTokenCtx(row) {\n\t\t\treturn { ...this.tokenCtx, objectId: this.rowIdOf(row), object: row }\n\t\t},\n\n\t\t/**\n\t\t * The id of a row (`rowIdField`, else `id`, else `@self.id`), or null.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string|null} The id.\n\t\t */\n\t\trowIdOf(row) {\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row && (row.id || row['@self']?.id))\n\t\t\treturn (id === null || id === undefined || id === '' || typeof id === 'object') ? null : String(id)\n\t\t},\n\n\t\t/**\n\t\t * Run a `rowAction` that is not a plain link through the page's\n\t\t * dispatcher: a `handler` gets the row as its last argument, an\n\t\t * `open-modal` gets it as the `row` prop, an `object-op` acts on it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {unknown} What the dispatcher returns.\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trunRowAction(row) {\n\t\t\tconst action = this.content?.rowAction\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst type = action.type || 'handler'\n\t\t\tlet wrapped = action\n\t\t\tif (type === 'handler') {\n\t\t\t\twrapped = { ...action, args: [...(action.args || []), row] }\n\t\t\t} else if (type === 'open-modal') {\n\t\t\t\twrapped = { ...action, props: { ...(action.props || {}), row } }\n\t\t\t}\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(wrapped)\n\t\t\t}\n\t\t\treturn dispatchAction(wrapped, { router: this.$router || null, row, tokenCtx: this.rowTokenCtx(row), source: { register: this.content?.register, schema: this.content?.schema } })\n\t\t},\n\n\t\trowLink(row) {\n\t\t\tconst route = this.content?.rowRoute\n\t\t\tif (!route || !this.$router || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row.id || row['@self']?.id)\n\t\t\tif (id === null || id === undefined || id === '' || typeof id === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, params: { id: String(id) } }\n\t\t},\n\n\t\t/**\n\t\t * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, emptyText? }`):\n\t\t * \"3 dagen te laat\" in the danger tone, \"Vandaag\", the date otherwise,\n\t\t * `emptyText` when the field is empty.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{text: string, tone: string}|null} The text and tone, or null.\n\t\t * @spec openspec/changes/screens-due-text/specs/due-text/spec.md#requirement-a-due-date-reads-as-text-with-a-tone\n\t\t */\n\t\tdueOf(row) {\n\t\t\tconst due = this.content?.due\n\t\t\tif (!due || typeof due !== 'object' || !due.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveDue(readPath(row, due.field), due, (text) => this.translate(text))\n\t\t},\n\n\t\t/**\n\t\t * The row's extra badges (`badges`): a coloured badge from a field\n\t\t * (`{ field, labels?, colorMap?, variant?, hideValues? }`, the value\n\t\t * shown unless `hideValues` lists it) and an age badge\n\t\t * (`{ kind: \"stale\", field, afterDays, label?, variant? }`, shown when the\n\t\t * date field is older than `afterDays` whole days; `{days}` in the\n\t\t * label is the age).\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<{key: string, kind: string, label: string, variant: string}>} The badges that apply.\n\t\t * @spec openspec/changes/screens-record-list-badges/specs/record-list-badges/spec.md#requirement-a-record-list-row-can-carry-badges\n\t\t */\n\t\tbadgesOf(row) {\n\t\t\tconst declared = Array.isArray(this.content?.badges) ? this.content.badges : []\n\t\t\tconst out = []\n\t\t\tdeclared.forEach((badge, index) => {\n\t\t\t\tif (!badge || typeof badge !== 'object' || !badge.field) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (badge.kind === 'stale') {\n\t\t\t\t\tconst days = ageInDays(readPath(row, badge.field))\n\t\t\t\t\tconst after = Number(badge.afterDays)\n\t\t\t\t\tif (days === null || !Number.isFinite(after) || days <= after) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst template = this.translate(badge.label || 'Stale')\n\t\t\t\t\tout.push({\n\t\t\t\t\t\tkey: `stale-${index}`,\n\t\t\t\t\t\tkind: 'stale',\n\t\t\t\t\t\tlabel: template.replace(/\\{days\\}/g, String(days)),\n\t\t\t\t\t\tvariant: isBadgeVariant(badge.variant) ? badge.variant : 'warning',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst made = this.pillOf(row, badge)\n\t\t\t\tif (made) {\n\t\t\t\t\tout.push({ key: `field-${index}`, kind: 'field', ...made })\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The row's meta line. A string is one template, and one empty token\n\t\t * empties it. `{ parts: [\"{identifier}\", \"{caseTypeName}\", \"{handler}\"],\n\t\t * separator? }` fills each part on its own, drops the empty ones and\n\t\t * joins the rest (default \" · \"), so a row without a handler still\n\t\t * reads \"2026-0082 · Woo-verzoek\".\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The meta line, '' when nothing fills.\n\t\t * @spec openspec/changes/screens-record-list-meta-parts/specs/record-list-meta-parts/spec.md#requirement-a-meta-line-of-parts-skips-the-empty-ones\n\t\t */\n\t\tmetaOf(row) {\n\t\t\tconst meta = this.content?.meta\n\t\t\tif (!meta || typeof meta !== 'object' || !Array.isArray(meta.parts)) {\n\t\t\t\treturn this.fill(meta, row)\n\t\t\t}\n\t\t\tconst separator = typeof meta.separator === 'string' ? meta.separator : ' · '\n\t\t\treturn meta.parts\n\t\t\t\t.map((part) => this.fill(part, row))\n\t\t\t\t.filter((text) => text !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * The row's pill: `{ field, labels?, colorMap?, variant?, hideValues? }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {object} [config] A pill config; the widget's `pill` by default.\n\t\t * @return {{label: string, variant: string}|null} The pill, or null.\n\t\t */\n\t\tpillOf(row, config) {\n\t\t\tconst pill = config || this.content?.pill\n\t\t\tif (!pill || typeof pill !== 'object' || !pill.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(row, pill.field)\n\t\t\tif (raw === null || raw === undefined || raw === '' || typeof raw === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst key = String(raw)\n\t\t\tif (Array.isArray(pill.hideValues) && pill.hideValues.map(String).includes(key)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}\n\t\t\tconst map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}\n\t\t\tconst found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())\n\t\t\tconst variant = found ? map[found] : pill.variant\n\t\t\treturn {\n\t\t\t\tlabel: labels[key] ? this.translate(labels[key]) : key,\n\t\t\t\tvariant: isBadgeVariant(variant) ? variant : 'default',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The feed icon: an icon name, or `{ field, map, default }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The MDI icon name.\n\t\t */\n\t\ticonOf(row) {\n\t\t\tconst icon = this.content?.icon\n\t\t\tif (typeof icon === 'string' && icon !== '') {\n\t\t\t\treturn icon\n\t\t\t}\n\t\t\tif (icon && typeof icon === 'object') {\n\t\t\t\tconst raw = icon.field ? readPath(row, icon.field) : undefined\n\t\t\t\tconst map = icon.map && typeof icon.map === 'object' ? icon.map : {}\n\t\t\t\tif (raw !== null && raw !== undefined && map[String(raw)]) {\n\t\t\t\t\treturn map[String(raw)]\n\t\t\t\t}\n\t\t\t\tif (typeof icon.default === 'string' && icon.default !== '') {\n\t\t\t\t\treturn icon.default\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 'CircleSmall'\n\t\t},\n\n\t\t/** Tell the card around this list what its header shows. */\n\t\temitHeader() {\n\t\t\tconst header = { link: this.headerLink, note: this.headerNote }\n\t\t\t// `headerCount: true`: the card draws the total as a chip after its title.\n\t\t\tif (this.content?.headerCount === true && !this.error) {\n\t\t\t\theader.count = this.total\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event header Emitted after every read with the card header's parts.\n\t\t\t * @type {{link: (object|null), note: string, count: (number|undefined)}} The header link (resolved, with `{total}` filled), the note, and with `headerCount` the total.\n\t\t\t */\n\t\t\tthis.$emit('header', header)\n\t\t},\n\n\t\t/**\n\t\t * The rank list of a sort (`{ field, rank: [\"urgent\", \"high\", \"normal\", \"low\"], then? }`),\n\t\t * or null when the sort has none or the list's own filter already fixes the field.\n\t\t *\n\t\t * @param {object|null} sort The effective sort.\n\t\t * @return {Array<string>|null} The values in rank order.\n\t\t * @spec openspec/changes/screens-enum-rank-sort/specs/enum-rank-sort/spec.md#requirement-a-record-list-can-sort-by-the-rank-of-an-enum-value\n\t\t */\n\t\trankOf(sort) {\n\t\t\tif (!sort || typeof sort !== 'object' || !sort.field || !Array.isArray(sort.rank)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst rank = sort.rank.filter((value) => typeof value === 'string' && value !== '')\n\t\t\tif (rank.length === 0 || Object.hasOwn(this.resolvedFilter || {}, sort.field)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn rank\n\t\t},\n\n\t\t/**\n\t\t * Read a list in the rank of an enum field. The list endpoint orders a\n\t\t * text field alphabetically (\"high\", \"low\", \"normal\", \"urgent\"), so the\n\t\t * rows are read one rank at a time, best first, each with the rest of the\n\t\t * sort (`then`) inside it, until the page is full; the rows with a value\n\t\t * outside the rank (or none) follow. The result is exact, not a client-side\n\t\t * sort of a partial page. It costs one request for the total, one per rank\n\t\t * value read and one for the remainder.\n\t\t *\n\t\t * @param {(params: object) => Promise<object>} get Reads the list with the given params.\n\t\t * @param {object} base The params without any order.\n\t\t * @param {{field: string, then?: {field: string, dir?: string}}} sort The sort.\n\t\t * @param {Array<string>} rank The values in rank order.\n\t\t * @return {Promise<{rows: Array<object>, total: number}>} The page and the total.\n\t\t * @spec openspec/changes/screens-enum-rank-sort/specs/enum-rank-sort/spec.md#requirement-a-record-list-can-sort-by-the-rank-of-an-enum-value\n\t\t */\n\t\tasync fetchRanked(get, base, sort, rank) {\n\t\t\tconst order = {}\n\t\t\tif (sort.then && typeof sort.then === 'object' && sort.then.field) {\n\t\t\t\torder[`_order[${sort.then.field}]`] = sort.then.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tconst read = async (extra) => {\n\t\t\t\tconst response = await get({ ...base, ...order, ...extra })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\treturn { results: Array.isArray(data.results) ? data.results : [], total: data.total }\n\t\t\t}\n\t\t\tconst head = await read({ _limit: 1 })\n\t\t\tconst rows = []\n\t\t\tconst seen = new Set()\n\t\t\tconst idOf = (row) => (row && (row.id || row['@self']?.id)) || null\n\t\t\tconst take = (list) => {\n\t\t\t\tfor (const row of list) {\n\t\t\t\t\tconst id = idOf(row)\n\t\t\t\t\tif (id !== null && seen.has(id)) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif (id !== null) {\n\t\t\t\t\t\tseen.add(id)\n\t\t\t\t\t}\n\t\t\t\t\tif (rows.length < this.limit) {\n\t\t\t\t\t\trows.push(row)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const value of rank) {\n\t\t\t\tif (rows.length >= this.limit) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttake((await read({ [sort.field]: value, _limit: this.limit - rows.length })).results)\n\t\t\t}\n\t\t\tif (rows.length < this.limit) {\n\t\t\t\ttake((await read({ _limit: this.limit + rows.length })).results)\n\t\t\t}\n\t\t\treturn { rows, total: typeof head.total === 'number' ? head.total : rows.length }\n\t\t},\n\n\t\t/**\n\t\t * Fetch the list. Sends nothing while a required token is unresolved\n\t\t * or the source is incomplete.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (!c.register || !c.schema || this.waiting) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = false\n\t\t\t\tthis.emitHeader()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\tconst run = this.fetchRun = (this.fetchRun || 0) + 1\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst sort = this.effectiveSort\n\t\t\t\tconst params = { _limit: this.limit }\n\t\t\t\t// A rank sort (`sort.rank`) cannot be one `_order`: the list endpoint\n\t\t\t\t// orders a text field alphabetically. It is read group by group below.\n\t\t\t\tconst rank = this.rankOf(sort)\n\t\t\t\tif (!rank && sort && typeof sort === 'object' && sort.field) {\n\t\t\t\t\tparams[`_order[${sort.field}]`] = sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter)) {\n\t\t\t\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (value !== '' && value !== null && value !== undefined) {\n\t\t\t\t\t\tparams[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (rank) {\n\t\t\t\t\t// Several requests: a fetch started later (the sort changed\n\t\t\t\t\t// meanwhile) must not be overwritten by this one.\n\t\t\t\t\tconst ranked = await this.fetchRanked((query) => axios.get(url, { params: query }), params, sort, rank)\n\t\t\t\t\tif (run !== this.fetchRun) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.rows = ranked.rows\n\t\t\t\t\tthis.total = ranked.total\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(url, { params })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\tthis.rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\tthis.total = typeof data.total === 'number' ? data.total : this.rows.length\n\t\t\t\tthis.lenses = readLensReports(data)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnRecordListWidget] failed to load objects:', e)\n\t\t\t\tthis.lenses = {}\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.emitHeader()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-record-list__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-record-list__empty,\n.cn-record-list__error {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-record-list__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* rows */\n.cn-record-list--rows .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__row {\n\talign-items: center;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tgap: 16px;\n\tpadding: 12px 0;\n\ttext-decoration: none;\n}\n\n.cn-record-list__row--link:hover .cn-record-list__title,\n.cn-record-list__row--link:focus-visible .cn-record-list__title {\n\ttext-decoration: underline;\n}\n\n.cn-record-list__avatar {\n\talign-items: center;\n\tbackground: var(--color-background-hover);\n\tborder-radius: 50%;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 12px;\n\tfont-weight: 700;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__main {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-record-list__title {\n\tfont-size: 15px;\n\tfont-weight: 700;\n}\n\n.cn-record-list__meta {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-record-list__aside {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: none;\n\tgap: 16px;\n}\n\n.cn-record-list__trailing {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__due {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__due--danger {\n\tcolor: var(--color-text-error, var(--color-error-text));\n\tfont-weight: 600;\n}\n\n.cn-record-list__due--empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-record-list__row--action {\n\tflex-wrap: wrap;\n\tgap: 8px 16px;\n\tpadding: 14px 0;\n}\n\n.cn-record-list__row--action .cn-record-list__main {\n\tflex: 1 1 280px;\n\tgap: 3px;\n}\n\n.cn-record-list__title--link {\n\tcolor: var(--color-main-text);\n\ttext-decoration: underline;\n}\n\n.cn-record-list__pills {\n\tdisplay: flex;\n\tflex: 0 0 120px;\n\tgap: 6px;\n\tjustify-content: flex-end;\n}\n\n.cn-record-list__due--column {\n\tflex: 0 0 96px;\n\tfont-size: 13px;\n\ttext-align: end;\n}\n\n.cn-record-list__action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 17px;\n\tbox-sizing: border-box;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tflex: 0 0 150px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\theight: 34px;\n\tjustify-content: center;\n\tpadding: 0 14px;\n\ttext-decoration: none;\n}\n\n.cn-record-list__action:hover,\n.cn-record-list__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-record-list__chevron {\n\tcolor: var(--color-text-maxcontrast);\n\tflex: none;\n}\n\n/* feed */\n.cn-record-list--feed .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__feed-row {\n\talign-items: flex-start;\n\tdisplay: flex;\n\tgap: 14px;\n\tpadding: 12px 0;\n}\n\n.cn-record-list__feed-icon {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tdisplay: flex;\n\tflex: none;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__text {\n\tfont-size: 15px;\n}\n\n.cn-record-list--feed .cn-record-list__main {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n/* inline */\n.cn-record-list__inline-row {\n\tpadding: 4px 0;\n}\n\n.cn-record-list__label {\n\tmargin-inline-end: 4px;\n}\n\n.cn-record-list__inline-title {\n\tcolor: var(--color-main-text);\n}\n\na.cn-record-list__inline-title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n</style>\n\n<style>\n/* In a narrow side column a row stacks: title, meta, then pill and date. */\n.cn-detail-page__side .cn-record-list__row {\n\talign-items: flex-start;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-detail-page__side .cn-record-list__row .cn-record-list__title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-detail-page__side .cn-record-list__chevron {\n\tdisplay: none;\n}\n\n/* A row is a touch target of at least 44px on a phone. */\n@media (max-width: 640px) {\n\t.cn-record-list__row {\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_toDisplayString","_openBlock","_createVNode","_createBlock","_resolveDynamicComponent","_mergeProps","_createTextVNode","_Fragment","_renderList","_toHandlers","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnRecordListWidget.vue3.js","sources":["../../../../src/components/CnRecordListWidget/CnRecordListWidget.vue?vue&type=template&id=bc186ef8&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-record-list\"\n\t\t:class=\"`cn-record-list--${presentation}`\"\n\t\tdata-testid=\"cn-record-list\">\n\t\t<p v-if=\"error\" class=\"cn-record-list__error\" data-testid=\"cn-record-list-error\">\n\t\t\t{{ errorLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && rows.length === 0\" class=\"cn-record-list__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" :name=\"loadingLabel\" />\n\t\t</div>\n\t\t<p v-else-if=\"rows.length === 0\" class=\"cn-record-list__empty\" data-testid=\"cn-record-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<ul v-else class=\"cn-record-list__list\">\n\t\t\t<li\n\t\t\t\tv-for=\"item in items\"\n\t\t\t\t:key=\"item.key\"\n\t\t\t\tclass=\"cn-record-list__item\"\n\t\t\t\tdata-testid=\"cn-record-list-item\">\n\t\t\t\t<!-- rows with `rowAction`: the title is the link, and one action\n\t\t\t\t ends the row instead of the chevron (\"Beoordelen\"). -->\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"presentation === 'rows' && item.action\"\n\t\t\t\t\tclass=\"cn-record-list__row cn-record-list__row--action\">\n\t\t\t\t\t<span v-if=\"item.initials\"\n\t\t\t\t\t\tclass=\"cn-record-list__avatar\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-avatar\">{{ item.initials }}</span>\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\t\t\tv-if=\"item.title\"\n\t\t\t\t\t\t\tclass=\"cn-record-list__title\"\n\t\t\t\t\t\t\t:class=\"{ 'cn-record-list__title--link': item.tag !== 'div' }\"\n\t\t\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t\t</component>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.badges.length > 0\" class=\"cn-record-list__pills\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-for=\"badge in item.badges\"\n\t\t\t\t\t\t\t:key=\"badge.key\"\n\t\t\t\t\t\t\t:label=\"badge.label\"\n\t\t\t\t\t\t\t:variant=\"badge.variant\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-record-list-badge-${badge.kind}`\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"item.due\"\n\t\t\t\t\t\tclass=\"cn-record-list__due cn-record-list__due--column\"\n\t\t\t\t\t\t:class=\"`cn-record-list__due--${item.due.tone}`\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-due\">{{ item.due.text }}</span>\n\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"item.action.tag\"\n\t\t\t\t\t\tclass=\"cn-record-list__action\"\n\t\t\t\t\t\tv-bind=\"item.action.attrs\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-action\"\n\t\t\t\t\t\tv-on=\"item.action.tag === 'button' ? { click: () => runRowAction(item.row) } : {}\">\n\t\t\t\t\t\t{{ item.action.label }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t\t<!-- rows: title and meta, then the pill, the date and a chevron. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"item.tag\"\n\t\t\t\t\tv-else-if=\"presentation === 'rows'\"\n\t\t\t\t\tclass=\"cn-record-list__row\"\n\t\t\t\t\t:class=\"{ 'cn-record-list__row--link': item.tag !== 'div' }\"\n\t\t\t\t\tv-bind=\"item.attrs\">\n\t\t\t\t\t<span v-if=\"item.initials\"\n\t\t\t\t\t\tclass=\"cn-record-list__avatar\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\tdata-testid=\"cn-record-list-avatar\">{{ item.initials }}</span>\n\t\t\t\t\t<span class=\"cn-record-list__main\">\n\t\t\t\t\t\t<span v-if=\"item.title\" class=\"cn-record-list__title\">{{ item.title }}</span>\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"item.pill || item.badges.length > 0 || item.due || item.trailing\" class=\"cn-record-list__aside\">\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-if=\"item.pill\"\n\t\t\t\t\t\t\t:label=\"item.pill.label\"\n\t\t\t\t\t\t\t:variant=\"item.pill.variant\"\n\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\tv-for=\"badge in item.badges\"\n\t\t\t\t\t\t\t:key=\"badge.key\"\n\t\t\t\t\t\t\t:label=\"badge.label\"\n\t\t\t\t\t\t\t:variant=\"badge.variant\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-record-list-badge-${badge.kind}`\" />\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"item.due\"\n\t\t\t\t\t\t\tclass=\"cn-record-list__due\"\n\t\t\t\t\t\t\t:class=\"`cn-record-list__due--${item.due.tone}`\"\n\t\t\t\t\t\t\tdata-testid=\"cn-record-list-due\">{{ item.due.text }}</span>\n\t\t\t\t\t\t<span v-if=\"item.trailing\" class=\"cn-record-list__trailing\">{{ item.trailing }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<ChevronRight\n\t\t\t\t\t\tv-if=\"item.tag !== 'div'\"\n\t\t\t\t\t\tclass=\"cn-record-list__chevron\"\n\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t</component>\n\n\t\t\t\t<!-- feed: a round icon, a meta line and the text. -->\n\t\t\t\t<div v-else-if=\"presentation === 'feed'\" class=\"cn-record-list__feed-row\">\n\t\t\t\t\t<span class=\"cn-record-list__feed-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon :name=\"item.icon\" :size=\"18\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__main\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t<span v-if=\"item.meta\" class=\"cn-record-list__meta\">{{ item.meta }}</span>\n\t\t\t\t\t\t<span v-if=\"item.text\" class=\"cn-record-list__text\">{{ item.text }}</span>\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\n\t\t\t\t<!-- inline: a label and the title, the title a link. -->\n\t\t\t\t<div v-else class=\"cn-record-list__inline-row\">\n\t\t\t\t\t<span v-if=\"item.label\" class=\"cn-record-list__label\">{{ item.label }}</span>\n\t\t\t\t\t<component :is=\"item.tag\" class=\"cn-record-list__inline-title\" v-bind=\"item.attrs\">\n\t\t\t\t\t\t{{ item.title }}\n\t\t\t\t\t</component>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { actionLink, dispatchAction, interpolateActionTarget } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { ageInDays } from '../../utils/boardDate.js'\nimport { resolveDue } from '../../utils/dueText.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { initialsOf } from '../../utils/initials.js'\nimport { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'\nimport { pageFilter, pageSort } from '../../utils/pageControls.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\nconst PRESENTATIONS = ['rows', 'feed', 'inline']\n\n/**\n * Unwrap a value that may be a ref.\n *\n * @param {unknown} value The value.\n * @return {unknown} The plain value.\n */\nfunction unwrap(value) {\n\treturn value && typeof value === 'object' && 'value' in value ? value.value : value\n}\n\n/**\n * CnRecordListWidget — the objects of another schema that point at this\n * record, drawn as the boards draw them (screens-relation-pages).\n *\n * A person page lists its open cases (\"Verlichting fietspad Lindelaan,\n * 2026-0082 · Woo-verzoek, In behandeling, Termijn 1 nov\"), its latest contact\n * moments (an icon by channel, a meta line, the summary) and its relations\n * (\"Secretaris van Buurtvereniging Oost\"). Each is a list of another schema's\n * objects filtered on a reference to this record. The source keys are those of\n * `object-list` (`register`, `schema`, `filter` with the `@objectId` /\n * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,\n * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name\n * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`\n * or `inline`, and each line is a template (`\"{identifier} · {caseTypeName}\"`,\n * `\"Termijn {deadline|date}\"`).\n *\n * A content-only catalog type: on a card surface the detail host or the\n * dashboard page draws the titled card. The widget emits `header` with `{ link, note }` (the\n * `headerLink` and `headerNote` with `{total}` filled in) for that card's\n * header.\n */\nexport default {\n\tname: 'CnRecordListWidget',\n\n\tcomponents: { ChevronRight, CnIcon, CnStatusBadge, NcLoadingIcon },\n\n\tinject: {\n\t\t/** Detail-page object context (`{ objectId, object, register, schema }`). */\n\t\tcnObjectContext: { default: null },\n\t\t/** Page workspace bag, for `@workspace.<key>` tokens. */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The host app's translate function. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The page's app config map; its `currency` is the reporting currency of a money token. */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/** The page's pre-bound action dispatcher (CnPageRenderer), for `rowAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The list config: `{ register, schema, filter?, sort?, limit?, extend?,\n\t\t * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,\n\t\t * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,\n\t\t * valueLabels? }`. `valueLabels` maps a field's stored values to the labels\n\t\t * the templates show.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The current record's id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current record, for `@object.<field>` tokens. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header'],\n\n\tdata() {\n\t\treturn {\n\t\t\trows: [],\n\t\t\ttotal: 0,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Personal-lens reports (`@self.lenses`) of the latest response. */\n\t\t\tlenses: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {string} `rows`, `feed` or `inline`. */\n\t\tpresentation() {\n\t\t\tconst value = this.content?.presentation\n\t\t\treturn PRESENTATIONS.includes(value) ? value : 'rows'\n\t\t},\n\n\t\t/** @return {object} The token context: record, id and workspace. */\n\t\ttokenCtx() {\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/** @return {object} The filter with its tokens resolved. */\n\t\tresolvedFilter() {\n\t\t\tconst filter = pageFilter(this.content?.pageControls, this.tokenCtx.workspace, this.content?.filter)\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(filter, this.tokenCtx))\n\t\t},\n\n\t\t/** @return {boolean} A required token (the record id, say) has no value yet. */\n\t\twaiting() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/** @return {object|null} The sort: the page's sort control when the widget listens to one, else its own. */\n\t\teffectiveSort() {\n\t\t\treturn pageSort(this.content?.pageControls, this.tokenCtx.workspace, this.content?.sort)\n\t\t},\n\n\t\t/**\n\t\t * What a template may read beside the row: the app config, whose\n\t\t * `currency` is the reporting currency of a `{path|currency}` token.\n\t\t *\n\t\t * @return {{config: object}} The template context.\n\t\t * @spec openspec/changes/screens-currency-format/specs/currency-format/spec.md#requirement-a-money-value-reads-in-its-currency\n\t\t */\n\t\ttemplateContext() {\n\t\t\treturn { config: unwrapAppConfig(this.cnAppConfig) }\n\t\t},\n\n\t\t/** @return {number} The fetch cap. */\n\t\tlimit() {\n\t\t\tconst value = Number(this.content?.limit)\n\t\t\treturn Number.isFinite(value) && value > 0 ? Math.floor(value) : 5\n\t\t},\n\n\t\t/** @return {string} What a fetch depends on; a change fetches again. */\n\t\trequestKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify([c.register, c.schema, this.resolvedFilter, this.effectiveSort, this.limit, c.extend, this.waiting])\n\t\t},\n\n\t\t/** @return {Array<object>} The rows ready to draw. */\n\t\titems() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn this.rows.slice(0, this.limit).map((row, index) => {\n\t\t\t\tconst link = this.rowLink(row)\n\t\t\t\treturn {\n\t\t\t\t\tkey: (row && (row.id || row['@self']?.id)) || `row-${index}`,\n\t\t\t\t\trow,\n\t\t\t\t\taction: this.rowActionOf(row),\n\t\t\t\t\ttag: link ? 'router-link' : 'div',\n\t\t\t\t\tattrs: link ? { to: link } : {},\n\t\t\t\t\tinitials: this.initialsFor(row),\n\t\t\t\t\ttitle: this.fill(c.title, row),\n\t\t\t\t\tmeta: this.metaOf(row),\n\t\t\t\t\ttext: this.fill(c.text, row),\n\t\t\t\t\tlabel: this.fill(c.label, row),\n\t\t\t\t\ttrailing: c.trailing ? firstFilled(c.trailing, row, this.translate, new Date(), this.templateContext) : '',\n\t\t\t\t\tpill: this.pillOf(row),\n\t\t\t\t\tbadges: this.badgesOf(row),\n\t\t\t\t\tdue: this.dueOf(row),\n\t\t\t\t\ticon: this.iconOf(row),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** @return {object|null} The header link with `{total}` filled and tokens resolved. */\n\t\theaderLink() {\n\t\t\tconst link = this.content?.headerLink\n\t\t\tif (!link || typeof link !== 'object' || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = { label: this.withTotal(link.label) }\n\t\t\tif (link.route) {\n\t\t\t\tresolved.route = link.route\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tresolved.params = resolveFilterTokens(link.params, this.tokenCtx)\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tresolved.query = resolveFilterTokens(link.query, this.tokenCtx)\n\t\t\t\t}\n\t\t\t} else if (typeof link.href === 'string') {\n\t\t\t\tresolved.href = link.href\n\t\t\t}\n\t\t\treturn resolved\n\t\t},\n\n\t\t/** @return {string} The header note with `{total}` filled. */\n\t\theaderNote() {\n\t\t\tconst note = this.content?.headerNote\n\t\t\treturn typeof note === 'string' && note !== '' ? this.withTotal(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * Why the list is empty when a personal lens could not answer\n\t\t * (`@self.lenses.<lens>.available: false`, as for `filter: { _recent: true }`),\n\t\t * or '' to keep `emptyText`. `lensReasonTexts` holds the app's wording,\n\t\t * keyed `<lens>.<reason>` or `<reason>`.\n\t\t *\n\t\t * @return {string} The explanation, or ''.\n\t\t * @spec openspec/changes/screens-record-list-lens-reason/specs/record-list-lens-reason/spec.md#requirement-a-record-list-says-why-a-lens-is-empty\n\t\t */\n\t\tlensReasonText() {\n\t\t\tif (this.rows.length > 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst text = lensUnavailableText(this.lenses, this.content?.lensReasonTexts)\n\t\t\treturn text ? this.translate(text) : ''\n\t\t},\n\n\t\t/** @return {string} The empty line. */\n\t\temptyLabel() {\n\t\t\tif (this.lensReasonText !== '') {\n\t\t\t\treturn this.lensReasonText\n\t\t\t}\n\t\t\treturn this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')\n\t\t},\n\n\t\t/** @return {string} The error line. */\n\t\terrorLabel() {\n\t\t\treturn this.content?.errorText ? this.translate(this.content.errorText) : t('nextcloud-vue', 'This list could not be loaded.')\n\t\t},\n\n\t\t/** @return {string} The spinner's accessible name. */\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading …')\n\t\t},\n\t},\n\n\twatch: {\n\t\trequestKey() {\n\t\t\tthis.fetchRows()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.onRefresh = () => this.fetchRows()\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t\tthis.fetchRows()\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The translation.\n\t\t */\n\t\ttranslate(text) {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The initials of the row's avatar (`avatar`: a name template such as\n\t\t * `\"{client.name}\"`, or `{ text }`), or '' without one.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} One or two letters.\n\t\t * @spec openspec/changes/screens-record-list-avatar/specs/record-list-avatar/spec.md#requirement-a-record-list-row-can-carry-an-avatar\n\t\t */\n\t\tinitialsFor(row) {\n\t\t\tconst avatar = this.content?.avatar\n\t\t\tconst template = typeof avatar === 'string' ? avatar : (avatar && typeof avatar === 'object' ? avatar.text : '')\n\t\t\treturn initialsOf(this.fill(template, row))\n\t\t},\n\n\t\t/**\n\t\t * Fill a template from a row, after translating it.\n\t\t *\n\t\t * @param {string} template The template.\n\t\t * @param {object} row The row.\n\t\t * @return {string} The text, '' when a token is empty.\n\t\t */\n\t\tfill(template, row) {\n\t\t\treturn typeof template === 'string' && template !== '' ? fillTemplate(this.translate(template), this.labelledRow(row), new Date(), this.templateContext) : ''\n\t\t},\n\n\t\t/**\n\t\t * The row as the templates read it: a field named in `valueLabels`\n\t\t * (`{ channel: { phone: \"Phone\", telefoon: \"Phone\" } }`) shows the\n\t\t * translated label of its stored value, so \"{channel}\" reads \"Telefoon\"\n\t\t * for both spellings the data holds. A value without a label stays as\n\t\t * stored. The pill, the badges and the sort read the stored value.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object} The row, or a shallow copy with labelled fields.\n\t\t * @spec openspec/changes/screens-record-list-value-labels/specs/record-list-value-labels/spec.md#requirement-a-record-list-template-can-show-a-label-for-a-stored-value\n\t\t */\n\t\tlabelledRow(row) {\n\t\t\tconst map = this.content?.valueLabels\n\t\t\tif (!row || typeof row !== 'object' || !map || typeof map !== 'object') {\n\t\t\t\treturn row\n\t\t\t}\n\t\t\tlet out = row\n\t\t\tfor (const [field, labels] of Object.entries(map)) {\n\t\t\t\tconst raw = row[field]\n\t\t\t\tif (!labels || typeof labels !== 'object' || (typeof raw !== 'string' && typeof raw !== 'number')) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst label = labels[String(raw)]\n\t\t\t\tif (typeof label === 'string' && label !== '') {\n\t\t\t\t\tif (out === row) {\n\t\t\t\t\t\tout = { ...row }\n\t\t\t\t\t}\n\t\t\t\t\tout[field] = this.translate(label)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Replace `{total}` in a translated label.\n\t\t *\n\t\t * @param {string} label The label.\n\t\t * @return {string} The text.\n\t\t */\n\t\twithTotal(label) {\n\t\t\treturn this.translate(label).replace(/\\{total\\}/g, String(this.total))\n\t\t},\n\n\t\t/**\n\t\t * The route a row opens: `rowRoute` with the id at `rowIdField`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object|null} The router location, or null.\n\t\t */\n\t\t/**\n\t\t * The one action that ends a row (`rowAction`): its label (from\n\t\t * `labels.map` by the row's `labels.field`, else `label`), and how it\n\t\t * renders: a link when it only navigates, else a button that\n\t\t * dispatches. Null without the key or a label.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{label: string, tag: string, attrs: object}|null}\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trowActionOf(row) {\n\t\t\tconst action = this.content?.rowAction\n\t\t\tif (!action || typeof action !== 'object' || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet label = typeof action.label === 'string' ? action.label : ''\n\t\t\tconst by = action.labels\n\t\t\tif (by && typeof by === 'object' && typeof by.field === 'string' && by.map && typeof by.map === 'object') {\n\t\t\t\tconst raw = readPath(row, by.field)\n\t\t\t\tif (raw !== null && raw !== undefined && typeof by.map[String(raw)] === 'string') {\n\t\t\t\t\tlabel = by.map[String(raw)]\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlabel = this.translate(label)\n\t\t\tconst link = this.rowActionLink(action, row)\n\t\t\tif (link) {\n\t\t\t\treturn link.external\n\t\t\t\t\t? { label, tag: 'a', attrs: { href: link.href, target: '_blank', rel: 'noopener' } }\n\t\t\t\t\t: { label, tag: 'router-link', attrs: { to: link.to } }\n\t\t\t}\n\t\t\treturn { label, tag: 'button', attrs: { type: 'button' } }\n\t\t},\n\n\t\t/**\n\t\t * The link a `rowAction` that only navigates renders as: `open-page`\n\t\t * opens that page for the row's id, `navigate` reads its\n\t\t * `@object.<field>` / `@objectId` tokens off the row. Null for any\n\t\t * other action, which is dispatched on click.\n\t\t *\n\t\t * @param {object} action The declared row action.\n\t\t * @param {object} row The row.\n\t\t * @return {{href: string, to: (string|object|null), external: boolean}|null}\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trowActionLink(action, row) {\n\t\t\tif (action.confirm) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (action.type === 'open-page') {\n\t\t\t\tconst id = this.rowIdOf(row)\n\t\t\t\tif (!this.$router || id === null || typeof action.target !== 'string' || action.target === '') {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst to = { name: action.target, params: { id } }\n\t\t\t\ttry {\n\t\t\t\t\tconst href = this.$router.resolve(to).href\n\t\t\t\t\treturn href ? { href, to, external: false } : null\n\t\t\t\t} catch {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (action.type !== 'navigate') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst target = interpolateActionTarget(action.target, this.rowTokenCtx(row))\n\t\t\t// An external target needs no router; an in-app one does.\n\t\t\treturn actionLink({ ...action, target }, { router: this.$router || null })\n\t\t},\n\n\t\t/**\n\t\t * The token context of a row: the row is `@object`, its id `@objectId`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {object} The token context.\n\t\t */\n\t\trowTokenCtx(row) {\n\t\t\treturn { ...this.tokenCtx, objectId: this.rowIdOf(row), object: row }\n\t\t},\n\n\t\t/**\n\t\t * The id of a row (`rowIdField`, else `id`, else `@self.id`), or null.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string|null} The id.\n\t\t */\n\t\trowIdOf(row) {\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row && (row.id || row['@self']?.id))\n\t\t\treturn (id === null || id === undefined || id === '' || typeof id === 'object') ? null : String(id)\n\t\t},\n\n\t\t/**\n\t\t * Run a `rowAction` that is not a plain link through the page's\n\t\t * dispatcher: a `handler` gets the row as its last argument, an\n\t\t * `open-modal` gets it as the `row` prop, an `object-op` acts on it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {unknown} What the dispatcher returns.\n\t\t * @spec openspec/changes/screens-record-list-row-action/specs/record-list/spec.md\n\t\t */\n\t\trunRowAction(row) {\n\t\t\tconst action = this.content?.rowAction\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst type = action.type || 'handler'\n\t\t\tlet wrapped = action\n\t\t\tif (type === 'handler') {\n\t\t\t\twrapped = { ...action, args: [...(action.args || []), row] }\n\t\t\t} else if (type === 'open-modal') {\n\t\t\t\twrapped = { ...action, props: { ...(action.props || {}), row } }\n\t\t\t}\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(wrapped)\n\t\t\t}\n\t\t\treturn dispatchAction(wrapped, { router: this.$router || null, row, tokenCtx: this.rowTokenCtx(row), source: { register: this.content?.register, schema: this.content?.schema } })\n\t\t},\n\n\t\trowLink(row) {\n\t\t\tconst route = this.content?.rowRoute\n\t\t\tif (!route || !this.$router || !row) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = this.content?.rowIdField\n\t\t\tconst id = field ? readPath(row, field) : (row.id || row['@self']?.id)\n\t\t\tif (id === null || id === undefined || id === '' || typeof id === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, params: { id: String(id) } }\n\t\t},\n\n\t\t/**\n\t\t * The row's due text (`due`: `{ field, todayLabel?, overdueLabel?, emptyText? }`):\n\t\t * \"3 dagen te laat\" in the danger tone, \"Vandaag\", the date otherwise,\n\t\t * `emptyText` when the field is empty.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {{text: string, tone: string}|null} The text and tone, or null.\n\t\t * @spec openspec/changes/screens-due-text/specs/due-text/spec.md#requirement-a-due-date-reads-as-text-with-a-tone\n\t\t */\n\t\tdueOf(row) {\n\t\t\tconst due = this.content?.due\n\t\t\tif (!due || typeof due !== 'object' || !due.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveDue(readPath(row, due.field), due, (text) => this.translate(text))\n\t\t},\n\n\t\t/**\n\t\t * The row's extra badges (`badges`): a coloured badge from a field\n\t\t * (`{ field, labels?, colorMap?, variant?, hideValues? }`, the value\n\t\t * shown unless `hideValues` lists it) and an age badge\n\t\t * (`{ kind: \"stale\", field, afterDays, label?, variant? }`, shown when the\n\t\t * date field is older than `afterDays` whole days; `{days}` in the\n\t\t * label is the age).\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<{key: string, kind: string, label: string, variant: string}>} The badges that apply.\n\t\t * @spec openspec/changes/screens-record-list-badges/specs/record-list-badges/spec.md#requirement-a-record-list-row-can-carry-badges\n\t\t */\n\t\tbadgesOf(row) {\n\t\t\tconst declared = Array.isArray(this.content?.badges) ? this.content.badges : []\n\t\t\tconst out = []\n\t\t\tdeclared.forEach((badge, index) => {\n\t\t\t\tif (!badge || typeof badge !== 'object' || !badge.field) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (badge.kind === 'stale') {\n\t\t\t\t\tconst days = ageInDays(readPath(row, badge.field))\n\t\t\t\t\tconst after = Number(badge.afterDays)\n\t\t\t\t\tif (days === null || !Number.isFinite(after) || days <= after) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst template = this.translate(badge.label || 'Stale')\n\t\t\t\t\tout.push({\n\t\t\t\t\t\tkey: `stale-${index}`,\n\t\t\t\t\t\tkind: 'stale',\n\t\t\t\t\t\tlabel: template.replace(/\\{days\\}/g, String(days)),\n\t\t\t\t\t\tvariant: isBadgeVariant(badge.variant) ? badge.variant : 'warning',\n\t\t\t\t\t})\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst made = this.pillOf(row, badge)\n\t\t\t\tif (made) {\n\t\t\t\t\tout.push({ key: `field-${index}`, kind: 'field', ...made })\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The row's meta line. A string is one template, and one empty token\n\t\t * empties it. `{ parts: [\"{identifier}\", \"{caseTypeName}\", \"{handler}\"],\n\t\t * separator? }` fills each part on its own, drops the empty ones and\n\t\t * joins the rest (default \" · \"), so a row without a handler still\n\t\t * reads \"2026-0082 · Woo-verzoek\".\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The meta line, '' when nothing fills.\n\t\t * @spec openspec/changes/screens-record-list-meta-parts/specs/record-list-meta-parts/spec.md#requirement-a-meta-line-of-parts-skips-the-empty-ones\n\t\t */\n\t\tmetaOf(row) {\n\t\t\tconst meta = this.content?.meta\n\t\t\tif (!meta || typeof meta !== 'object' || !Array.isArray(meta.parts)) {\n\t\t\t\treturn this.fill(meta, row)\n\t\t\t}\n\t\t\tconst separator = typeof meta.separator === 'string' ? meta.separator : ' · '\n\t\t\treturn meta.parts\n\t\t\t\t.map((part) => this.fill(part, row))\n\t\t\t\t.filter((text) => text !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * The row's pill: `{ field, labels?, colorMap?, variant?, hideValues? }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {object} [config] A pill config; the widget's `pill` by default.\n\t\t * @return {{label: string, variant: string}|null} The pill, or null.\n\t\t */\n\t\tpillOf(row, config) {\n\t\t\tconst pill = config || this.content?.pill\n\t\t\tif (!pill || typeof pill !== 'object' || !pill.field) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(row, pill.field)\n\t\t\tif (raw === null || raw === undefined || raw === '' || typeof raw === 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst key = String(raw)\n\t\t\tif (Array.isArray(pill.hideValues) && pill.hideValues.map(String).includes(key)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst labels = pill.labels && typeof pill.labels === 'object' ? pill.labels : {}\n\t\t\tconst map = pill.colorMap && typeof pill.colorMap === 'object' ? pill.colorMap : {}\n\t\t\tconst found = Object.keys(map).find((k) => k.toLowerCase() === key.toLowerCase())\n\t\t\tconst variant = found ? map[found] : pill.variant\n\t\t\treturn {\n\t\t\t\tlabel: labels[key] ? this.translate(labels[key]) : key,\n\t\t\t\tvariant: isBadgeVariant(variant) ? variant : 'default',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The feed icon: an icon name, or `{ field, map, default }`.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string} The MDI icon name.\n\t\t */\n\t\ticonOf(row) {\n\t\t\tconst icon = this.content?.icon\n\t\t\tif (typeof icon === 'string' && icon !== '') {\n\t\t\t\treturn icon\n\t\t\t}\n\t\t\tif (icon && typeof icon === 'object') {\n\t\t\t\tconst raw = icon.field ? readPath(row, icon.field) : undefined\n\t\t\t\tconst map = icon.map && typeof icon.map === 'object' ? icon.map : {}\n\t\t\t\tif (raw !== null && raw !== undefined && map[String(raw)]) {\n\t\t\t\t\treturn map[String(raw)]\n\t\t\t\t}\n\t\t\t\tif (typeof icon.default === 'string' && icon.default !== '') {\n\t\t\t\t\treturn icon.default\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn 'CircleSmall'\n\t\t},\n\n\t\t/** Tell the card around this list what its header shows. */\n\t\temitHeader() {\n\t\t\tconst header = { link: this.headerLink, note: this.headerNote }\n\t\t\t// `headerCount: true`: the card draws the total as a chip after its title.\n\t\t\tif (this.content?.headerCount === true && !this.error) {\n\t\t\t\theader.count = this.total\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event header Emitted after every read with the card header's parts.\n\t\t\t * @type {{link: (object|null), note: string, count: (number|undefined)}} The header link (resolved, with `{total}` filled), the note, and with `headerCount` the total.\n\t\t\t */\n\t\t\tthis.$emit('header', header)\n\t\t},\n\n\t\t/**\n\t\t * The rank list of a sort (`{ field, rank: [\"urgent\", \"high\", \"normal\", \"low\"], then? }`),\n\t\t * or null when the sort has none or the list's own filter already fixes the field.\n\t\t *\n\t\t * @param {object|null} sort The effective sort.\n\t\t * @return {Array<string>|null} The values in rank order.\n\t\t * @spec openspec/changes/screens-enum-rank-sort/specs/enum-rank-sort/spec.md#requirement-a-record-list-can-sort-by-the-rank-of-an-enum-value\n\t\t */\n\t\trankOf(sort) {\n\t\t\tif (!sort || typeof sort !== 'object' || !sort.field || !Array.isArray(sort.rank)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst rank = sort.rank.filter((value) => typeof value === 'string' && value !== '')\n\t\t\tif (rank.length === 0 || Object.hasOwn(this.resolvedFilter || {}, sort.field)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn rank\n\t\t},\n\n\t\t/**\n\t\t * Read a list in the rank of an enum field. The list endpoint orders a\n\t\t * text field alphabetically (\"high\", \"low\", \"normal\", \"urgent\"), so the\n\t\t * rows are read one rank at a time, best first, each with the rest of the\n\t\t * sort (`then`) inside it, until the page is full; the rows with a value\n\t\t * outside the rank (or none) follow. The result is exact, not a client-side\n\t\t * sort of a partial page. It costs one request for the total, one per rank\n\t\t * value read and one for the remainder.\n\t\t *\n\t\t * @param {(params: object) => Promise<object>} get Reads the list with the given params.\n\t\t * @param {object} base The params without any order.\n\t\t * @param {{field: string, then?: {field: string, dir?: string}}} sort The sort.\n\t\t * @param {Array<string>} rank The values in rank order.\n\t\t * @return {Promise<{rows: Array<object>, total: number}>} The page and the total.\n\t\t * @spec openspec/changes/screens-enum-rank-sort/specs/enum-rank-sort/spec.md#requirement-a-record-list-can-sort-by-the-rank-of-an-enum-value\n\t\t */\n\t\tasync fetchRanked(get, base, sort, rank) {\n\t\t\tconst order = {}\n\t\t\tif (sort.then && typeof sort.then === 'object' && sort.then.field) {\n\t\t\t\torder[`_order[${sort.then.field}]`] = sort.then.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tconst read = async (extra) => {\n\t\t\t\tconst response = await get({ ...base, ...order, ...extra })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\treturn { results: Array.isArray(data.results) ? data.results : [], total: data.total }\n\t\t\t}\n\t\t\tconst head = await read({ _limit: 1 })\n\t\t\tconst rows = []\n\t\t\tconst seen = new Set()\n\t\t\tconst idOf = (row) => (row && (row.id || row['@self']?.id)) || null\n\t\t\tconst take = (list) => {\n\t\t\t\tfor (const row of list) {\n\t\t\t\t\tconst id = idOf(row)\n\t\t\t\t\tif (id !== null && seen.has(id)) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif (id !== null) {\n\t\t\t\t\t\tseen.add(id)\n\t\t\t\t\t}\n\t\t\t\t\tif (rows.length < this.limit) {\n\t\t\t\t\t\trows.push(row)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const value of rank) {\n\t\t\t\tif (rows.length >= this.limit) {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttake((await read({ [sort.field]: value, _limit: this.limit - rows.length })).results)\n\t\t\t}\n\t\t\tif (rows.length < this.limit) {\n\t\t\t\ttake((await read({ _limit: this.limit + rows.length })).results)\n\t\t\t}\n\t\t\treturn { rows, total: typeof head.total === 'number' ? head.total : rows.length }\n\t\t},\n\n\t\t/**\n\t\t * Fetch the list. Sends nothing while a required token is unresolved\n\t\t * or the source is incomplete.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (!c.register || !c.schema || this.waiting) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = false\n\t\t\t\tthis.emitHeader()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = false\n\t\t\tconst run = this.fetchRun = (this.fetchRun || 0) + 1\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst sort = this.effectiveSort\n\t\t\t\tconst params = { _limit: this.limit }\n\t\t\t\t// A rank sort (`sort.rank`) cannot be one `_order`: the list endpoint\n\t\t\t\t// orders a text field alphabetically. It is read group by group below.\n\t\t\t\tconst rank = this.rankOf(sort)\n\t\t\t\tif (!rank && sort && typeof sort === 'object' && sort.field) {\n\t\t\t\t\tparams[`_order[${sort.field}]`] = sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t\t}\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter)) {\n\t\t\t\t\tif (value && typeof value === 'object' && !Array.isArray(value)) {\n\t\t\t\t\t\tfor (const [op, operand] of Object.entries(value)) {\n\t\t\t\t\t\t\tparams[`${key}[${op}]`] = operand\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (value !== '' && value !== null && value !== undefined) {\n\t\t\t\t\t\tparams[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (rank) {\n\t\t\t\t\t// Several requests: a fetch started later (the sort changed\n\t\t\t\t\t// meanwhile) must not be overwritten by this one.\n\t\t\t\t\tconst ranked = await this.fetchRanked((query) => axios.get(url, { params: query }), params, sort, rank)\n\t\t\t\t\tif (run !== this.fetchRun) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tthis.rows = ranked.rows\n\t\t\t\t\tthis.total = ranked.total\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst response = await axios.get(url, { params })\n\t\t\t\tconst data = response && response.data ? response.data : {}\n\t\t\t\tthis.rows = Array.isArray(data.results) ? data.results : []\n\t\t\t\tthis.total = typeof data.total === 'number' ? data.total : this.rows.length\n\t\t\t\tthis.lenses = readLensReports(data)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnRecordListWidget] failed to load objects:', e)\n\t\t\t\tthis.lenses = {}\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t\tthis.emitHeader()\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-record-list__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-record-list__empty,\n.cn-record-list__error {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-record-list__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 8px 0;\n}\n\n/* rows */\n.cn-record-list--rows .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__row {\n\talign-items: center;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tgap: 16px;\n\tpadding: 12px 0;\n\ttext-decoration: none;\n}\n\n.cn-record-list__row--link:hover .cn-record-list__title,\n.cn-record-list__row--link:focus-visible .cn-record-list__title {\n\ttext-decoration: underline;\n}\n\n.cn-record-list__avatar {\n\talign-items: center;\n\tbackground: var(--color-background-hover);\n\tborder-radius: 50%;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 12px;\n\tfont-weight: 700;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__main {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-record-list__title {\n\tfont-size: 15px;\n\tfont-weight: 700;\n}\n\n.cn-record-list__meta {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-record-list__aside {\n\talign-items: center;\n\tdisplay: flex;\n\tflex: none;\n\tgap: 16px;\n}\n\n.cn-record-list__trailing {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__due {\n\tfont-size: 14px;\n\twhite-space: nowrap;\n}\n\n.cn-record-list__due--danger {\n\tcolor: var(--color-text-error, var(--color-error-text));\n\tfont-weight: 600;\n}\n\n.cn-record-list__due--empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-record-list__row--action {\n\tflex-wrap: wrap;\n\tgap: 8px 16px;\n\tpadding: 14px 0;\n}\n\n.cn-record-list__row--action .cn-record-list__main {\n\tflex: 1 1 280px;\n\tgap: 3px;\n}\n\n.cn-record-list__title--link {\n\tcolor: var(--color-main-text);\n\ttext-decoration: underline;\n}\n\n.cn-record-list__pills {\n\tdisplay: flex;\n\tflex: 0 0 120px;\n\tgap: 6px;\n\tjustify-content: flex-end;\n}\n\n.cn-record-list__due--column {\n\tflex: 0 0 96px;\n\tfont-size: 13px;\n\ttext-align: end;\n}\n\n.cn-record-list__action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 17px;\n\tbox-sizing: border-box;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tflex: 0 0 150px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\theight: 34px;\n\tjustify-content: center;\n\tpadding: 0 14px;\n\ttext-decoration: none;\n}\n\n.cn-record-list__action:hover,\n.cn-record-list__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-record-list__chevron {\n\tcolor: var(--color-text-maxcontrast);\n\tflex: none;\n}\n\n/* feed */\n.cn-record-list--feed .cn-record-list__item + .cn-record-list__item {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-record-list__feed-row {\n\talign-items: flex-start;\n\tdisplay: flex;\n\tgap: 14px;\n\tpadding: 12px 0;\n}\n\n.cn-record-list__feed-icon {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tdisplay: flex;\n\tflex: none;\n\theight: 36px;\n\tjustify-content: center;\n\twidth: 36px;\n}\n\n.cn-record-list__text {\n\tfont-size: 15px;\n}\n\n.cn-record-list--feed .cn-record-list__main {\n\tcolor: var(--color-main-text);\n\ttext-decoration: none;\n}\n\n/* inline */\n.cn-record-list__inline-row {\n\tpadding: 4px 0;\n}\n\n.cn-record-list__label {\n\tmargin-inline-end: 4px;\n}\n\n.cn-record-list__inline-title {\n\tcolor: var(--color-main-text);\n}\n\na.cn-record-list__inline-title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n</style>\n\n<style>\n/* In a narrow side column a row stacks: title, meta, then pill and date. */\n.cn-detail-page__side .cn-record-list__row {\n\talign-items: flex-start;\n\tflex-direction: column;\n\tgap: 6px;\n}\n\n.cn-detail-page__side .cn-record-list__row .cn-record-list__title {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-detail-page__side .cn-record-list__chevron {\n\tdisplay: none;\n}\n\n/* A row is a touch target of at least 44px on a phone. */\n@media (max-width: 640px) {\n\t.cn-record-list__row {\n\t\tmin-height: 44px;\n\t}\n}\n</style>\n"],"names":["_normalizeClass","_createElementBlock","_toDisplayString","_openBlock","_createVNode","_createBlock","_resolveDynamicComponent","_mergeProps","_createTextVNode","_Fragment","_renderList","_toHandlers","_createElementVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnReportsPage.vue2.js';
2
2
  import { render } from './CnReportsPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-7629715a";
5
+ script.__scopeId = "data-v-327b60f8";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,7 +1,10 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
+ import { NcButton } from '@nextcloud/vue';
2
3
  import { defineAsyncComponent } from 'vue';
3
- import __sfc_main from '../CnIcon/CnIcon.vue.js';
4
+ import __sfc_main from '../CnBuildiqEditButton/CnBuildiqEditButton.vue.js';
5
+ import __sfc_main$1 from '../CnIcon/CnIcon.vue.js';
4
6
  import { useLook } from '../../composables/useLook.js';
7
+ import { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js';
5
8
  import { followLinkClick } from '../../utils/linkNavigation.js';
6
9
 
7
10
  var script = {
@@ -11,10 +14,18 @@ var script = {
11
14
  // Loaded only for a page that declares widgets: a cards-only reports
12
15
  // page does not pull the dashboard in.
13
16
  CnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue.js').then((m) => m.default)),
14
- CnIcon: __sfc_main,
17
+ CnBuildiqEditButton: __sfc_main,
18
+ CnIcon: __sfc_main$1,
19
+ NcButton,
15
20
  },
16
21
 
17
22
  inject: {
23
+ /**
24
+ * The v2 registry and the legacy customComponents map, provided by
25
+ * CnAppRoot: a header action's named handler resolves against them.
26
+ */
27
+ cnRegistry: { default: () => ({}) },
28
+ cnCustomComponents: { default: () => ({}) },
18
29
  /**
19
30
  * The consuming app's translate function, provided by CnAppRoot.
20
31
  * Defaults to identity so the page still renders outside one.
@@ -133,6 +144,38 @@ var script = {
133
144
  default: null,
134
145
  },
135
146
 
147
+ /**
148
+ * Buttons beside the title (`config.headerButtons`), as the board's
149
+ * "Downloaden". Each is `{ label, action, variant?, icon?, id? }`:
150
+ * `action` is the id of a `headerActions` entry, which says what the
151
+ * click does. Under the board look the buildiq square sits between the
152
+ * secondary buttons and the primary one. Empty (the default) changes
153
+ * nothing.
154
+ *
155
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
156
+ * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}
157
+ */
158
+ headerButtons: {
159
+ type: Array,
160
+ default: null,
161
+ },
162
+
163
+ /**
164
+ * The page's header actions (`config.headerActions`), in the shape of
165
+ * an index page's: a header button names one by id. `handler` is a
166
+ * function, `navigate` (with `route`, and `params`), `emit`, `none`,
167
+ * or a registry name; `type: "navigate"` with a `target` URL opens
168
+ * that URL in a new tab. Every click also emits `header-action`
169
+ * `{ action, id }`, except for `handler: "none"`.
170
+ *
171
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
172
+ * @type {Array<object>|null}
173
+ */
174
+ headerActions: {
175
+ type: Array,
176
+ default: null,
177
+ },
178
+
136
179
  /**
137
180
  * The look to draw in: `board` or `nextcloud`. Absent: the look
138
181
  * CnAppRoot provides (`cnLook`), else `nextcloud`.
@@ -155,6 +198,8 @@ var script = {
155
198
  },
156
199
  },
157
200
 
201
+ emits: ['header-action'],
202
+
158
203
  setup(props) {
159
204
  const { isBoard } = useLook(props);
160
205
  return { isBoard }
@@ -204,6 +249,8 @@ var script = {
204
249
  'emptyLabel',
205
250
  'widgets',
206
251
  'layout',
252
+ 'headerButtons',
253
+ 'headerActions',
207
254
  ]) {
208
255
  if (this[key] !== null && this[key] !== undefined) {
209
256
  fromProps[key] = this[key];
@@ -243,6 +290,51 @@ var script = {
243
290
  })
244
291
  },
245
292
 
293
+ /**
294
+ * The declared header buttons that have a label and an action,
295
+ * translated, each with a stable key and a variant.
296
+ *
297
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
298
+ * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.
299
+ */
300
+ resolvedHeaderButtons() {
301
+ const declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : [];
302
+ return declared
303
+ .filter((button) => button && typeof button === 'object'
304
+ && typeof button.action === 'string' && button.action !== ''
305
+ && typeof button.label === 'string' && button.label !== '')
306
+ .map((button) => ({
307
+ key: String(button.id || button.action),
308
+ label: this.tr(button.label),
309
+ action: button.action,
310
+ variant: button.variant === 'primary' ? 'primary' : 'secondary',
311
+ icon: typeof button.icon === 'string' ? button.icon : '',
312
+ }))
313
+ },
314
+
315
+ /**
316
+ * The header row in the order it renders. Without the board look:
317
+ * the declared buttons in the declared order. Under the board look:
318
+ * the secondary buttons, the buildiq square, then the primary ones,
319
+ * as the board draws "Downloaden" and the square. No declared button
320
+ * leaves the header as it was, without the square.
321
+ *
322
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
323
+ * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.
324
+ */
325
+ headerButtonsRow() {
326
+ const buttons = this.resolvedHeaderButtons;
327
+ if (buttons.length === 0 || !this.isBoard) {
328
+ return buttons
329
+ }
330
+ const square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' };
331
+ return [
332
+ ...buttons.filter((button) => button.variant !== 'primary'),
333
+ square,
334
+ ...buttons.filter((button) => button.variant === 'primary'),
335
+ ]
336
+ },
337
+
246
338
  /**
247
339
  * The heading.
248
340
  *
@@ -432,6 +524,47 @@ var script = {
432
524
  }
433
525
  },
434
526
 
527
+ /**
528
+ * A header button was clicked: run the `headerActions` entry it names
529
+ * and emit `header-action`, as an index page's header button does.
530
+ *
531
+ * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons
532
+ * @param {{action: string}} button The resolved button.
533
+ * @return {void}
534
+ */
535
+ onHeaderButton(button) {
536
+ const id = button.action;
537
+ const actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : [];
538
+ const entry = actions.find((action) => action && action.id === id) || null;
539
+ const handler = entry ? entry.handler : undefined;
540
+ if (handler === 'none') {
541
+ return
542
+ }
543
+ if (typeof handler === 'function') {
544
+ handler({ actionId: id });
545
+ } else if (handler === 'navigate' && entry.route) {
546
+ const params = (entry.params && typeof entry.params === 'object') ? entry.params : null;
547
+ if (this.$router && typeof this.$router.push === 'function') {
548
+ this.$router.push(params ? { name: entry.route, params } : { name: entry.route });
549
+ }
550
+ } else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {
551
+ window.open(entry.target, '_blank', 'noopener,noreferrer');
552
+ } else if (typeof handler === 'string' && handler !== 'emit') {
553
+ const resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents);
554
+ if (typeof resolved === 'function') {
555
+ resolved({ actionId: id });
556
+ }
557
+ }
558
+ /**
559
+ * A header button was clicked (`headerButtons`), after its
560
+ * `headerActions` entry ran; not for `handler: "none"`.
561
+ *
562
+ * @event header-action
563
+ * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.
564
+ */
565
+ this.$emit('header-action', { action: id, id });
566
+ },
567
+
435
568
  /**
436
569
  * Navigate by route NAME on a plain click; a modified or middle click
437
570
  * is left to the browser so it can open the card's href elsewhere.
@@ -1 +1 @@
1
- {"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { defineAsyncComponent } from 'vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnIcon,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["CnIcon","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnReportsPage.vue2.js","sources":["../../../../src/components/CnReportsPage/CnReportsPage.vue"],"sourcesContent":["<!--\n CnReportsPage — Manifest-driven Reports surface.\n\n v2 manifest page primitive for `type: \"reports\"`. One page holding every\n report an app offers, as cards, instead of a Reports SUBMENU with one entry\n per report.\n\n WHY THIS IS A PAGE AND NOT A MENU BRANCH. A submenu grows one entry per\n report and never shrinks. shillinq reached 96 report types; as menu children\n that is a scroll, not a navigation. Cards carry what a menu item cannot — a\n description, a category, a count — and a category filter turns 96 into a\n short list. shillinq proved the shape in `ReportingComplianceOverview`; this\n is that shape with the app-specific parts taken out, so every app declares it\n rather than copying 600 lines of Vue.\n\n Declare it as:\n\n { \"id\": \"Reports\", \"route\": \"/reports\", \"type\": \"reports\",\n \"config\": {\n \"categories\": { \"operational\": \"Operational\" },\n \"cards\": [\n { \"id\": \"Throughput\", \"label\": \"Processing time\",\n \"description\": \"How long cases take, by type.\",\n \"icon\": \"ChartLine\", \"category\": \"operational\",\n \"route\": \"Doorlooptijd\" }\n ]\n } }\n\n A card names a ROUTE by name, never a path: a path is editable per app and a\n card pointing at a stale one is a dead end that still looks like a report.\n\n Spec: hydra openspec/architecture/adr-112-reports-are-one-page.md (ConductionNL/hydra#640).\n-->\n<template>\n\t<div class=\"cn-reports-page\" :class=\"{ 'cn-reports-page--board': isBoard }\">\n\t\t<div class=\"cn-reports-page__header\">\n\t\t\t<!-- The header buttons (`headerButtons`) beside the title, as the\n\t\t\t board's \"Downloaden\" and the buildiq square (board DcRapportages).\n\t\t\t Without them, and outside the board look, the title stands alone\n\t\t\t as before. -->\n\t\t\t<div v-if=\"headerButtonsRow.length > 0\" class=\"cn-reports-page__heading-row\">\n\t\t\t\t<h2 class=\"cn-reports-page__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h2>\n\t\t\t\t<div class=\"cn-reports-page__header-buttons\" data-testid=\"cn-reports-header-buttons\">\n\t\t\t\t\t<template v-for=\"button in headerButtonsRow\" :key=\"button.key\">\n\t\t\t\t\t\t<CnBuildiqEditButton v-if=\"button.action === '__buildiq'\" />\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t:variant=\"button.variant\"\n\t\t\t\t\t\t\t:data-testid=\"`cn-reports-header-button-${button.key}`\"\n\t\t\t\t\t\t\t@click=\"onHeaderButton(button)\">\n\t\t\t\t\t\t\t<template v-if=\"button.icon\" #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"button.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ button.label }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<h2 v-else class=\"cn-reports-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-reports-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<!-- The board look (board DcRapportages): one section per category,\n\t\t headed by its label, instead of the category filter; each card\n\t\t draws its icon in a tile. -->\n\t\t<template v-if=\"isBoard\">\n\t\t\t<p v-if=\"resolvedCards.length === 0\"\n\t\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t\t{{ resolvedEmptyLabel }}\n\t\t\t</p>\n\t\t\t<section v-for=\"section in boardSections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-reports-page__section\"\n\t\t\t\tdata-testid=\"cn-reports-section\">\n\t\t\t\t<h3 v-if=\"section.label\" class=\"cn-reports-page__section-title\">\n\t\t\t\t\t{{ section.label }}\n\t\t\t\t</h3>\n\t\t\t\t<ul class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t\t\t<li v-for=\"card in section.cards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-reports-page__card-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"22\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-text\">\n\t\t\t\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</a>\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</section>\n\t\t</template>\n\n\t\t<div v-if=\"!isBoard && categoryOptions.length > 1\" class=\"cn-reports-page__filter\">\n\t\t\t<label class=\"cn-reports-page__filter-label\" :for=\"filterId\">\n\t\t\t\t{{ resolvedCategoryLabel }}\n\t\t\t</label>\n\t\t\t<select :id=\"filterId\"\n\t\t\t\tv-model=\"activeCategory\"\n\t\t\t\tclass=\"cn-reports-page__filter-select\"\n\t\t\t\tdata-testid=\"cn-reports-category\">\n\t\t\t\t<option v-for=\"option in categoryOptions\"\n\t\t\t\t\t:key=\"option.value\"\n\t\t\t\t\t:value=\"option.value\">\n\t\t\t\t\t{{ option.label }}\n\t\t\t\t</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<p v-if=\"!isBoard && visibleCards.length === 0\"\n\t\t\tclass=\"cn-reports-page__empty\"\n\t\t\tdata-testid=\"cn-reports-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<ul v-else-if=\"!isBoard\" class=\"cn-reports-page__grid\" data-testid=\"cn-reports-grid\">\n\t\t\t<li v-for=\"card in visibleCards\" :key=\"card.id\" class=\"cn-reports-page__cell\">\n\t\t\t\t<a class=\"cn-reports-page__card\"\n\t\t\t\t\tdata-testid=\"cn-report-card\"\n\t\t\t\t\t:href=\"hrefFor(card)\"\n\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t<span class=\"cn-reports-page__card-title\">{{ card.label }}</span>\n\t\t\t\t\t<span v-if=\"card.description\" class=\"cn-reports-page__card-description\">\n\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<span v-if=\"categoryName(card)\" class=\"cn-reports-page__card-category\">\n\t\t\t\t\t\t{{ categoryName(card) }}\n\t\t\t\t\t</span>\n\t\t\t\t</a>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<!-- `widgets` + `layout`: a grid of dashboard widgets under the cards,\n\t\t such as a highlighted report with stat tiles and a chart. -->\n\t\t<div v-if=\"sectionWidgets.length > 0\" class=\"cn-reports-page__widgets\" data-testid=\"cn-reports-widgets\">\n\t\t\t<CnDashboardPage\n\t\t\t\t:widgets=\"sectionWidgets\"\n\t\t\t\t:layout=\"sectionLayout\"\n\t\t\t\t:showHeader=\"false\" />\n\t\t</div>\n\t</div>\n</template>\n\n<script>\n/**\n * The Reports page: one surface listing an app's reports as cards.\n */\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton } from '@nextcloud/vue'\nimport { defineAsyncComponent } from 'vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport { useLook } from '../../composables/useLook.js'\nimport { resolveRegisteredHandler } from '../../utils/actionsDispatcher.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\n\nexport default {\n\tname: 'CnReportsPage',\n\n\tcomponents: {\n\t\t// Loaded only for a page that declares widgets: a cards-only reports\n\t\t// page does not pull the dashboard in.\n\t\tCnDashboardPage: defineAsyncComponent(() => import('../CnDashboardPage/CnDashboardPage.vue').then((m) => m.default)),\n\t\tCnBuildiqEditButton,\n\t\tCnIcon,\n\t\tNcButton,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * The v2 registry and the legacy customComponents map, provided by\n\t\t * CnAppRoot: a header action's named handler resolves against them.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tcnCustomComponents: { default: () => ({}) },\n\t\t/**\n\t\t * The consuming app's translate function, provided by CnAppRoot.\n\t\t * Defaults to identity so the page still renders outside one.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The manifest page.\n\t\t *\n\t\t * ⚠️ CnPageRenderer does NOT pass this. `resolvedProps()` returns\n\t\t * `{ ...topLevel, ...normalizedConfig, ...params }` and has no `page`\n\t\t * key, so a dispatched `type: \"reports\"` page arrived here with the\n\t\t * default `{}` — `config` resolved to `{}`, `cards` to `[]`, and every\n\t\t * reports page in the fleet rendered its empty state while its manifest\n\t\t * declared cards. Kept because a host that renders this component\n\t\t * directly can still hand it the whole page; the flattened props below\n\t\t * are what the renderer actually supplies, and they win.\n\t\t */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The report cards, as CnPageRenderer flattens them out of `config`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Category key/label pairs, flattened out of `config`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, flattened out of `config` or lifted from the page.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The heading, lifted from the page by the renderer.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the \"all categories\" filter option.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tallCategoriesLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Label for the category filter control.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcategoryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Text shown when the active filter admits no card.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dashboard widgets drawn under the report cards (`config.widgets`),\n\t\t * placed by `layout`, as on a `type: \"dashboard\"` page.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Grid placement of `widgets` (`config.layout`): `{ id, widgetId,\n\t\t * gridX, gridY, gridWidth, gridHeight }` per widget. Without it each\n\t\t * widget takes a full row in the order declared.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Buttons beside the title (`config.headerButtons`), as the board's\n\t\t * \"Downloaden\". Each is `{ label, action, variant?, icon?, id? }`:\n\t\t * `action` is the id of a `headerActions` entry, which says what the\n\t\t * click does. Under the board look the buildiq square sits between the\n\t\t * secondary buttons and the primary one. Empty (the default) changes\n\t\t * nothing.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<{label: string, action: string, variant?: ('primary'|'secondary'), icon?: string, id?: string}>|null}\n\t\t */\n\t\theaderButtons: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's header actions (`config.headerActions`), in the shape of\n\t\t * an index page's: a header button names one by id. `handler` is a\n\t\t * function, `navigate` (with `route`, and `params`), `emit`, `none`,\n\t\t * or a registry name; `type: \"navigate\"` with a `target` URL opens\n\t\t * that URL in a new tab. Every click also emits `header-action`\n\t\t * `{ action, id }`, except for `handler: \"none\"`.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look to draw in: `board` or `nextcloud`. Absent: the look\n\t\t * CnAppRoot provides (`cnLook`), else `nextcloud`.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header-action'],\n\n\tsetup(props) {\n\t\tconst { isBoard } = useLook(props)\n\t\treturn { isBoard }\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveCategory: 'all',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The translate function actually in force.\n\t\t *\n\t\t * @return {(key: string) => string} The translator.\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The declared config, always an object, from whichever direction it\n\t\t * arrived.\n\t\t *\n\t\t * TWO CALLERS, TWO SHAPES. CnPageRenderer flattens `page.config.*` into\n\t\t * individual props and passes NO `page`, so under the renderer the keys\n\t\t * arrive as props. A host that mounts this component itself hands it the\n\t\t * whole `page`. Reading only `page.config` is what made every dispatched\n\t\t * reports page render empty.\n\t\t *\n\t\t * Props win where both are present: they are the nearer declaration, and\n\t\t * the renderer has already resolved route sentinels in them.\n\t\t *\n\t\t * @return {object} The config.\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of [\n\t\t\t\t'cards',\n\t\t\t\t'categories',\n\t\t\t\t'description',\n\t\t\t\t'title',\n\t\t\t\t'allCategoriesLabel',\n\t\t\t\t'categoryLabel',\n\t\t\t\t'emptyLabel',\n\t\t\t\t'widgets',\n\t\t\t\t'layout',\n\t\t\t\t'headerButtons',\n\t\t\t\t'headerActions',\n\t\t\t]) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The widgets under the cards.\n\t\t *\n\t\t * @return {Array<object>} The declared widgets, or none.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionWidgets() {\n\t\t\tconst widgets = this.config.widgets\n\t\t\treturn Array.isArray(widgets) ? widgets.filter((w) => w && typeof w === 'object' && w.id) : []\n\t\t},\n\n\t\t/**\n\t\t * Where the widgets go: the declared `layout`, else one full row per\n\t\t * widget in the order declared.\n\t\t *\n\t\t * @return {Array<object>} The placements.\n\t\t * @spec openspec/changes/screens-reports-page-widgets/specs/reports-page/spec.md\n\t\t */\n\t\tsectionLayout() {\n\t\t\tif (Array.isArray(this.config.layout) && this.config.layout.length > 0) {\n\t\t\t\treturn this.config.layout\n\t\t\t}\n\t\t\tlet y = 0\n\t\t\treturn this.sectionWidgets.map((widget, index) => {\n\t\t\t\tconst placement = { id: String(index + 1), widgetId: widget.id, gridX: 0, gridY: y, gridWidth: 12, gridHeight: 4 }\n\t\t\t\ty += 4\n\t\t\t\treturn placement\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The declared header buttons that have a label and an action,\n\t\t * translated, each with a stable key and a variant.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<{key: string, label: string, action: string, variant: string, icon: string}>} The buttons.\n\t\t */\n\t\tresolvedHeaderButtons() {\n\t\t\tconst declared = Array.isArray(this.config.headerButtons) ? this.config.headerButtons : []\n\t\t\treturn declared\n\t\t\t\t.filter((button) => button && typeof button === 'object'\n\t\t\t\t\t&& typeof button.action === 'string' && button.action !== ''\n\t\t\t\t\t&& typeof button.label === 'string' && button.label !== '')\n\t\t\t\t.map((button) => ({\n\t\t\t\t\tkey: String(button.id || button.action),\n\t\t\t\t\tlabel: this.tr(button.label),\n\t\t\t\t\taction: button.action,\n\t\t\t\t\tvariant: button.variant === 'primary' ? 'primary' : 'secondary',\n\t\t\t\t\ticon: typeof button.icon === 'string' ? button.icon : '',\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The header row in the order it renders. Without the board look:\n\t\t * the declared buttons in the declared order. Under the board look:\n\t\t * the secondary buttons, the buildiq square, then the primary ones,\n\t\t * as the board draws \"Downloaden\" and the square. No declared button\n\t\t * leaves the header as it was, without the square.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @return {Array<object>} The buttons; the square is `{ key: '__buildiq', action: '__buildiq' }`.\n\t\t */\n\t\theaderButtonsRow() {\n\t\t\tconst buttons = this.resolvedHeaderButtons\n\t\t\tif (buttons.length === 0 || !this.isBoard) {\n\t\t\t\treturn buttons\n\t\t\t}\n\t\t\tconst square = { key: '__buildiq', action: '__buildiq', variant: 'secondary', label: '', icon: '' }\n\t\t\treturn [\n\t\t\t\t...buttons.filter((button) => button.variant !== 'primary'),\n\t\t\t\tsquare,\n\t\t\t\t...buttons.filter((button) => button.variant === 'primary'),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The heading.\n\t\t *\n\t\t * @return {string} The title.\n\t\t */\n\t\tresolvedTitle() {\n\t\t\tconst declared = this.config.title || (this.page && this.page.title)\n\n\t\t\treturn declared ? this.tr(declared) : t('nextcloud-vue', 'Reports')\n\t\t},\n\n\t\t/**\n\t\t * The optional lead paragraph.\n\t\t *\n\t\t * @return {string} The description.\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The declared cards, ignoring any that name no route.\n\t\t *\n\t\t * A card with no route is a card that cannot be opened. Rendering it\n\t\t * would put a report on the page that silently does nothing, which is\n\t\t * worse than leaving it off.\n\t\t *\n\t\t * @return {Array<object>} The usable cards.\n\t\t */\n\t\tresolvedCards() {\n\t\t\tconst declared = this.config.cards\n\t\t\tif (!Array.isArray(declared)) {\n\t\t\t\treturn []\n\t\t\t}\n\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.route && card.label)\n\t\t\t\t.map((card) => ({\n\t\t\t\t\t...card,\n\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Category value/label pairs, offered only for categories that hold a\n\t\t * card. A filter listing empty categories teaches the reader that the\n\t\t * app has reports it does not have.\n\t\t *\n\t\t * @return {Array<object>} The options.\n\t\t */\n\t\tcategoryOptions() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst used = new Set(this.resolvedCards.map((card) => card.category).filter(Boolean))\n\t\t\tconst options = [{ value: 'all', label: this.allLabel }]\n\n\t\t\tObject.keys(declared)\n\t\t\t\t.filter((key) => used.has(key))\n\t\t\t\t.forEach((key) => options.push({ value: key, label: this.tr(declared[key]) }))\n\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * The board look's sections: the cards without a category first, under\n\t\t * no heading, then one section per declared category that holds a\n\t\t * card, in the declared order, headed by its label.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-page-board-look/specs/reports-page/spec.md#requirement-the-reports-page-takes-the-board-look\n\t\t * @return {Array<{key: string, label: string, cards: Array<object>}>} The sections.\n\t\t */\n\t\tboardSections() {\n\t\t\tconst declared = this.config.categories || {}\n\t\t\tconst sections = []\n\t\t\tconst loose = this.resolvedCards.filter((card) => !card.category || !Object.hasOwn(declared, card.category))\n\t\t\tif (loose.length > 0) {\n\t\t\t\tsections.push({ key: '__none', label: '', cards: loose })\n\t\t\t}\n\t\t\tfor (const key of Object.keys(declared)) {\n\t\t\t\tconst cards = this.resolvedCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tsections.push({ key, label: this.tr(declared[key]), cards })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * The cards the active filter admits.\n\t\t *\n\t\t * @return {Array<object>} The visible cards.\n\t\t */\n\t\tvisibleCards() {\n\t\t\tif (this.activeCategory === 'all') {\n\t\t\t\treturn this.resolvedCards\n\t\t\t}\n\n\t\t\treturn this.resolvedCards.filter((card) => card.category === this.activeCategory)\n\t\t},\n\n\t\t/**\n\t\t * A stable id for the filter control's label association.\n\t\t *\n\t\t * @return {string} The id.\n\t\t */\n\t\tfilterId() {\n\t\t\treturn `cn-reports-category-${(this.page && this.page.id) || 'page'}`\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The \"all categories\" option label.\n\t\t */\n\t\tallLabel() {\n\t\t\treturn this.config.allCategoriesLabel\n\t\t\t\t? this.tr(this.config.allCategoriesLabel)\n\t\t\t\t: t('nextcloud-vue', 'All categories')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The filter's label.\n\t\t */\n\t\tresolvedCategoryLabel() {\n\t\t\treturn this.config.categoryLabel\n\t\t\t\t? this.tr(this.config.categoryLabel)\n\t\t\t\t: t('nextcloud-vue', 'Category')\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The empty-state text.\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel\n\t\t\t\t? this.tr(this.config.emptyLabel)\n\t\t\t\t: t('nextcloud-vue', 'No reports in this category.')\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest-declared string through the app's own\n\t\t * translate function.\n\t\t *\n\t\t * The manifest is data the renderer walks, not source the extractor\n\t\t * reads, so every string it carries has to be handed to the app's\n\t\t * translator explicitly. A card rendered straight from the manifest\n\t\t * shows its source language whatever locale the reader is in.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translated string, or the input unchanged.\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\n\t\t\treturn this.effectiveTranslate(value)\n\t\t},\n\n\t\t/**\n\t\t * The human label for a card's category, if it has one.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The label, or an empty string.\n\t\t */\n\t\tcategoryName(card) {\n\t\t\tconst declared = this.config.categories || {}\n\n\t\t\treturn this.tr((card.category && declared[card.category]) || '')\n\t\t},\n\n\t\t/**\n\t\t * A real href for the card, so it is a link a reader can middle-click\n\t\t * or open in a new tab rather than a div that only answers to a click.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {string} The href.\n\t\t */\n\t\threfFor(card) {\n\t\t\tif (!this.$router) {\n\t\t\t\treturn '#'\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\treturn this.$router.resolve({ name: card.route }).href\n\t\t\t} catch {\n\t\t\t\treturn '#'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A header button was clicked: run the `headerActions` entry it names\n\t\t * and emit `header-action`, as an index page's header button does.\n\t\t *\n\t\t * @spec openspec/changes/screens-reports-header-buttons/specs/reports-page/spec.md#requirement-a-reports-page-carries-header-buttons\n\t\t * @param {{action: string}} button The resolved button.\n\t\t * @return {void}\n\t\t */\n\t\tonHeaderButton(button) {\n\t\t\tconst id = button.action\n\t\t\tconst actions = Array.isArray(this.config.headerActions) ? this.config.headerActions : []\n\t\t\tconst entry = actions.find((action) => action && action.id === id) || null\n\t\t\tconst handler = entry ? entry.handler : undefined\n\t\t\tif (handler === 'none') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof handler === 'function') {\n\t\t\t\thandler({ actionId: id })\n\t\t\t} else if (handler === 'navigate' && entry.route) {\n\t\t\t\tconst params = (entry.params && typeof entry.params === 'object') ? entry.params : null\n\t\t\t\tif (this.$router && typeof this.$router.push === 'function') {\n\t\t\t\t\tthis.$router.push(params ? { name: entry.route, params } : { name: entry.route })\n\t\t\t\t}\n\t\t\t} else if (entry && entry.type === 'navigate' && typeof entry.target === 'string' && entry.target !== '') {\n\t\t\t\twindow.open(entry.target, '_blank', 'noopener,noreferrer')\n\t\t\t} else if (typeof handler === 'string' && handler !== 'emit') {\n\t\t\t\tconst resolved = resolveRegisteredHandler(handler, this.cnRegistry, this.cnCustomComponents)\n\t\t\t\tif (typeof resolved === 'function') {\n\t\t\t\t\tresolved({ actionId: id })\n\t\t\t\t}\n\t\t\t}\n\t\t\t/**\n\t\t\t * A header button was clicked (`headerButtons`), after its\n\t\t\t * `headerActions` entry ran; not for `handler: \"none\"`.\n\t\t\t *\n\t\t\t * @event header-action\n\t\t\t * @property {{action: string, id: string}} payload The id of the `headerActions` entry the button names, as `action` and `id`.\n\t\t\t */\n\t\t\tthis.$emit('header-action', { action: id, id })\n\t\t},\n\n\t\t/**\n\t\t * Navigate by route NAME on a plain click; a modified or middle click\n\t\t * is left to the browser so it can open the card's href elsewhere.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t */\n\t\topen(card, event) {\n\t\t\tfollowLinkClick(event, { name: card.route }, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-reports-page {\n\tpadding: 16px;\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the body keeps the full width. */\n.cn-reports-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-reports-page__title {\n\tmargin: 0 0 4px;\n}\n\n.cn-reports-page__heading-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tjustify-content: space-between;\n\tgap: 12px 24px;\n}\n\n.cn-reports-page__heading-row .cn-reports-page__title {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-reports-page__header-buttons {\n\tdisplay: flex;\n\tflex: none;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-reports-page__description {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__filter {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 16px;\n}\n\n.cn-reports-page__filter-select {\n\tmin-width: 220px;\n}\n\n.cn-reports-page__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tgap: 12px;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-reports-page__card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\theight: 100%;\n\tpadding: 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large);\n\tcolor: inherit;\n\ttext-decoration: none;\n}\n\n.cn-reports-page__card:hover,\n.cn-reports-page__card:focus {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-reports-page__card-title {\n\tfont-weight: bold;\n}\n\n.cn-reports-page__card-description,\n.cn-reports-page__card-category {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-reports-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-reports-page__widgets {\n\tmargin-top: 20px;\n}\n</style>\n"],"names":["CnBuildiqEditButton","CnIcon","t"],"mappings":"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"}