@conduction/nextcloud-vue 2.83.0 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) 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 +125 -3
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  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 +31 -2
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  49. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  50. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  51. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  53. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  55. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  56. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  57. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  58. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
  61. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  62. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  64. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  65. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  67. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  73. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  74. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  76. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  77. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  78. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  79. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  80. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  81. package/dist/esm/l10n/en.json.js +16 -1
  82. package/dist/esm/l10n/en.json.js.map +1 -1
  83. package/dist/esm/l10n/nl.json.js +38 -1
  84. package/dist/esm/l10n/nl.json.js.map +1 -1
  85. package/dist/esm/library-style.js +1 -1
  86. package/dist/esm/nextcloud-vue.css +448 -296
  87. package/dist/esm/utils/buildManifest.js +37 -3
  88. package/dist/esm/utils/buildManifest.js.map +1 -1
  89. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  90. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  91. package/dist/esm/utils/formatMetric.js +39 -3
  92. package/dist/esm/utils/formatMetric.js.map +1 -1
  93. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  94. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  95. package/dist/esm/utils/sentinelTokens.js +9 -0
  96. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  97. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  98. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  99. package/dist/nextcloud-vue.cjs.js +1706 -454
  100. package/dist/nextcloud-vue.cjs.js.map +2 -2
  101. package/dist/nextcloud-vue.css +448 -296
  102. package/l10n/en.json +16 -1
  103. package/l10n/nl.json +38 -1
  104. package/package.json +1 -1
  105. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  106. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  107. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  108. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +128 -6
  109. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
  110. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  111. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  112. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  113. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  114. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  115. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  116. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  117. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +31 -2
  118. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  119. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  120. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  121. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  122. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  123. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  124. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  125. package/src/css/form-field.css +14 -0
  126. package/src/css/form-widget.css +11 -0
  127. package/src/css/look-board-reports.css +58 -0
  128. package/src/css/look-board.css +12 -0
  129. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  130. package/src/schemas/app-manifest-v2.schema.json +23 -2
  131. package/src/utils/buildManifest.js +36 -2
  132. package/src/utils/detailWidgetVisibility.js +145 -0
  133. package/src/utils/formatMetric.js +38 -2
  134. package/src/utils/resolveManifestTokens.js +1 -0
  135. package/src/utils/sentinelTokens.js +9 -0
  136. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"CnFieldListWidget.vue3.js","sources":["../../../../src/components/CnFieldListWidget/CnFieldListWidget.vue?vue&type=template&id=7d90316d&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-field-list-widget\" data-testid=\"cn-field-list-widget\">\n\t\t<div v-if=\"canAdd && addTrigger && addPlacement === 'header'\" class=\"cn-field-list-widget__toolbar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-open-add\"\n\t\t\t\t@click=\"adding = !adding\">\n\t\t\t\t+ {{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<form\n\t\t\tv-if=\"canAdd && addTrigger && addPlacement === 'header' && adding\"\n\t\t\tclass=\"cn-field-list-widget__add\"\n\t\t\tdata-testid=\"cn-field-list-add\"\n\t\t\t@submit.prevent=\"add\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in addFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-field-list-widget__field\"\n\t\t\t\t:data-testid=\"`cn-field-list-input-${field.key}`\">\n\t\t\t\t<NcSelect\n\t\t\t\t\tv-if=\"field.type === 'select'\"\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:inputLabel=\"field.label\"\n\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t:reduce=\"(option) => option.value\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\tlabel=\"label\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:label=\"field.label\"\n\t\t\t\t\t:error=\"Boolean(errors[field.key])\"\n\t\t\t\t\t:helperText=\"errors[field.key] || ''\" />\n\t\t\t</div>\n\t\t\t<NcButton type=\"submit\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-submit\">\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</form>\n\t\t<p v-if=\"rows.length === 0\" class=\"cn-field-list-widget__empty\" data-testid=\"cn-field-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"isTable\" class=\"cn-field-list-widget__table-wrap\">\n\t\t\t<table class=\"cn-field-list-widget__table\" data-testid=\"cn-field-list-table\">\n\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-for=\"column in tableColumns\"\n\t\t\t\t\t\t\t:key=\"column.key\"\n\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\tclass=\"cn-field-list-widget__th\">\n\t\t\t\t\t\t\t{{ column.header }}\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<th v-if=\"hasMenu\" scope=\"col\" class=\"cn-field-list-widget__th cn-field-list-widget__th--menu\">\n\t\t\t\t\t\t\t<span class=\"cn-field-list-widget__sr\">{{ tr('Actions') }}</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr\n\t\t\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-field-list-widget__tr\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-row-${row.key}`\">\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-for=\"(column, index) in tableColumns\"\n\t\t\t\t\t\t\t:key=\"column.key\"\n\t\t\t\t\t\t\tclass=\"cn-field-list-widget__td\">\n\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\tv-if=\"column.pill && row.cells[index].pill\"\n\t\t\t\t\t\t\t\t:label=\"row.cells[index].pill.label\"\n\t\t\t\t\t\t\t\t:variant=\"row.cells[index].pill.variant\"\n\t\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t\t<strong v-else-if=\"column.strong\">{{ row.cells[index].text }}</strong>\n\t\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t\t{{ row.cells[index].text }}\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td v-if=\"hasMenu\" class=\"cn-field-list-widget__td cn-field-list-widget__td--menu\">\n\t\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\t\tv-if=\"menuItems(row).length > 0\"\n\t\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t\t\t:ariaLabel=\"`${tr('More actions')}: ${row.title}`\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-field-list-menu-${row.key}`\">\n\t\t\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t\t\tv-for=\"item in menuItems(row)\"\n\t\t\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"`cn-field-list-menu-${item.id}-${row.key}`\"\n\t\t\t\t\t\t\t\t\t@click=\"item.run\">\n\t\t\t\t\t\t\t\t\t{{ item.label }}\n\t\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t\t<div\n\t\t\t\tv-for=\"row in bandRows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-field-list-widget__band\"\n\t\t\t\trole=\"note\"\n\t\t\t\t:data-testid=\"`cn-field-list-band-${row.key}`\">\n\t\t\t\t<span class=\"cn-field-list-widget__band-text\">{{ row.detail }}</span>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"action in row.bandActions\"\n\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-field-list-band-action-${action.id}-${row.key}`\"\n\t\t\t\t\t@click=\"runAction(action, row)\">\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\t\t<ul v-else class=\"cn-field-list-widget__rows\">\n\t\t\t<li\n\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-field-list-widget__row\"\n\t\t\t\t:data-testid=\"`cn-field-list-row-${row.key}`\">\n\t\t\t\t<div class=\"cn-field-list-widget__line\">\n\t\t\t\t\t<span v-if=\"row.initials\" class=\"cn-field-list-widget__avatar\" aria-hidden=\"true\">{{ row.initials }}</span>\n\t\t\t\t\t<span class=\"cn-field-list-widget__main\">\n\t\t\t\t\t\t<span class=\"cn-field-list-widget__title\">{{ row.title }}</span>\n\t\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-field-list-widget__meta\">{{ row.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-if=\"row.pill\"\n\t\t\t\t\t\t:label=\"row.pill.label\"\n\t\t\t\t\t\t:variant=\"row.pill.variant\"\n\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t<span v-if=\"row.aside\" class=\"cn-field-list-widget__aside\">{{ row.aside }}</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"action in row.actions\"\n\t\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-action-${action.id}-${row.key}`\"\n\t\t\t\t\t\t@click=\"runAction(action, row)\">\n\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"canRemove\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:aria-label=\"removeAriaLabel(row)\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-remove-${row.key}`\"\n\t\t\t\t\t\t@click=\"remove(row)\">\n\t\t\t\t\t\t{{ removeLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<p v-if=\"row.detail\" class=\"cn-field-list-widget__detail\">\n\t\t\t\t\t{{ row.detail }}\n\t\t\t\t</p>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<p v-if=\"hint\" class=\"cn-field-list-widget__hint\">\n\t\t\t{{ hint }}\n\t\t</p>\n\n\t\t<div v-if=\"canAdd && addTrigger && addPlacement !== 'header' && !adding\" class=\"cn-field-list-widget__toolbar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-open-add\"\n\t\t\t\t@click=\"adding = true\">\n\t\t\t\t+ {{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<form\n\t\t\tv-if=\"canAdd && !(addTrigger && addPlacement === 'header') && (!addTrigger || adding)\"\n\t\t\tclass=\"cn-field-list-widget__add\"\n\t\t\tdata-testid=\"cn-field-list-add\"\n\t\t\t@submit.prevent=\"add\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in addFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-field-list-widget__field\"\n\t\t\t\t:data-testid=\"`cn-field-list-input-${field.key}`\">\n\t\t\t\t<NcSelect\n\t\t\t\t\tv-if=\"field.type === 'select'\"\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:inputLabel=\"field.label\"\n\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t:reduce=\"(option) => option.value\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\tlabel=\"label\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:label=\"field.label\"\n\t\t\t\t\t:error=\"Boolean(errors[field.key])\"\n\t\t\t\t\t:helperText=\"errors[field.key] || ''\" />\n\t\t\t</div>\n\t\t\t<NcButton type=\"submit\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-submit\">\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</form>\n\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-field-list-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\n/**\n * CnFieldListWidget — the `field-list` widget: a list stored INSIDE one field\n * of the current object (a portal's `domains[]`, its `editors[]`), shown as\n * rows and edited in place.\n *\n * Rows are drawn from configuration (title, meta line, pill, aside, a detail\n * line); a small form adds an entry, a button removes one, and each change is\n * saved as ONE PATCH of that field on the object, after re-reading the stored\n * value so a concurrent change to the same list is not overwritten with a\n * stale copy. Optional row actions call a configured endpoint (a \"check\n * again\"), and an optional `rowSource` merges live per-row state read from an\n * endpoint by key without writing it back.\n *\n * The server decides who may write: a refused PATCH shows the server's words\n * and leaves the list as stored.\n *\n * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md\n */\nexport default {\n\tname: 'CnFieldListWidget',\n\n\tcomponents: { CnStatusBadge, NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the object field holding the array.\n\t\t * - `key` — the item field that identifies an entry (default: the item itself for strings).\n\t\t * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when? } }`.\n\t\t * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.\n\t\t * - `remove` — `{ label? }`; omitted = no remove.\n\t\t * - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.\n\t\t * - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.\n\t\t * - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap? }]`: rows as a table; a per-row \"...\" menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.\n\t\t * - `add.trigger: 'button'`, `add.placement: 'header' | 'footer'` — the add form opens from a button.\n\t\t * - `readOnly`, `hint`, `emptyText`.\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 detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The list as last saved by this widget; null = read it off objectData. */\n\t\t\tsaved: null,\n\t\t\t/** Live per-row state from `rowSource`, by key. */\n\t\t\tlive: {},\n\t\t\tdraft: {},\n\t\t\t/** Whether the add form is open (only with `add.trigger: 'button'`). */\n\t\t\tadding: false,\n\t\t\terrors: {},\n\t\t\tbusy: '',\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'error',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The stored list.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\titems() {\n\t\t\tconst list = this.saved !== null ? this.saved : readPath(this.objectData || {}, this.content.field)\n\t\t\treturn Array.isArray(list) ? list : []\n\t\t},\n\n\t\t/**\n\t\t * The rows to draw.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-draws-the-entries-of-one-object-field\n\t\t */\n\t\trows() {\n\t\t\tconst spec = this.content.row || {}\n\t\t\treturn this.items.map((item, index) => {\n\t\t\t\tconst key = this.keyOf(item, index)\n\t\t\t\tconst entry = item && typeof item === 'object'\n\t\t\t\t\t? { ...item, ...(this.live[key] || {}) }\n\t\t\t\t\t: { value: item, ...(this.live[key] || {}) }\n\t\t\t\tconst title = this.text(entry, spec.title) || this.text(entry, spec.titleFallback) || (typeof item === 'string' ? item : key)\n\t\t\t\tconst meta = (Array.isArray(spec.meta) ? spec.meta : [])\n\t\t\t\t\t.map((part) => this.text(entry, part))\n\t\t\t\t\t.filter((part) => part !== '')\n\t\t\t\t\t.join(' · ')\n\t\t\t\treturn {\n\t\t\t\t\tkey,\n\t\t\t\t\titem,\n\t\t\t\t\tentry,\n\t\t\t\t\ttitle,\n\t\t\t\t\tinitials: spec.avatar ? this.initials(title) : '',\n\t\t\t\t\tmeta,\n\t\t\t\t\tpill: this.pillOf(entry, spec.pill),\n\t\t\t\t\taside: spec.aside ? this.asideOf(entry, spec.aside) : '',\n\t\t\t\t\tdetail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',\n\t\t\t\t\tactions: this.actionsFor(entry),\n\t\t\t\t\tcells: this.isTable ? this.cellsOf(entry) : [],\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** Whether the list is read-only. */\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether entries can be added. */\n\t\tcanAdd() {\n\t\t\treturn !this.readOnly && this.addFields.length > 0 && Boolean(this.content.field)\n\t\t},\n\n\t\t/** Whether entries can be removed. */\n\t\tcanRemove() {\n\t\t\treturn !this.readOnly && Boolean(this.content.remove) && Boolean(this.content.field)\n\t\t},\n\n\t\t/**\n\t\t * The add form's fields, labels translated, select options translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\taddFields() {\n\t\t\tconst fields = this.content.add && Array.isArray(this.content.add.fields) ? this.content.add.fields : []\n\t\t\treturn fields\n\t\t\t\t.filter((field) => field && field.key)\n\t\t\t\t.map((field) => ({\n\t\t\t\t\t...field,\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' ? { value: option.value, label: this.tr(option.label || String(option.value)) } : { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\t/** Whether the rows are drawn as a table (`variant: 'table'`). */\n\t\tisTable() {\n\t\t\treturn this.content.variant === 'table' && this.tableColumns.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The table columns, headers translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\ttableColumns() {\n\t\t\treturn (Array.isArray(this.content.columns) ? this.content.columns : [])\n\t\t\t\t.filter((column) => column && column.path)\n\t\t\t\t.map((column, index) => ({ ...column, key: `${column.path}-${index}`, header: this.tr(column.header || '') }))\n\t\t},\n\n\t\t/** Whether each row has a \"more actions\" menu (row actions not placed in the band, and remove). */\n\t\thasMenu() {\n\t\t\treturn this.isTable && !this.readOnly && (this.canRemove || this.rows.some((row) => this.menuItems(row).length > 0))\n\t\t},\n\n\t\t/** Rows with a detail line: in the table variant it is a warning band under the table. */\n\t\tbandRows() {\n\t\t\treturn this.rows\n\t\t\t\t.filter((row) => row.detail !== '')\n\t\t\t\t.map((row) => ({ ...row, bandActions: row.actions.filter((action) => action.placement === 'band') }))\n\t\t},\n\n\t\t/** Whether the add form opens from a button (`add.trigger: 'button'`). */\n\t\taddTrigger() {\n\t\t\treturn Boolean(this.content.add && this.content.add.trigger === 'button')\n\t\t},\n\n\t\t/** Where the add button sits: `header` (above the rows) or `footer` (default). */\n\t\taddPlacement() {\n\t\t\treturn this.content.add && this.content.add.placement === 'header' ? 'header' : 'footer'\n\t\t},\n\n\t\taddLabel() {\n\t\t\treturn this.tr((this.content.add && this.content.add.label) || 'Add')\n\t\t},\n\n\t\tremoveLabel() {\n\t\t\treturn this.tr((this.content.remove && this.content.remove.label) || 'Remove')\n\t\t},\n\n\t\thint() {\n\t\t\treturn this.tr(this.content.hint || '')\n\t\t},\n\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content.emptyText || 'Nothing listed yet')\n\t\t},\n\n\t\t/** The object URL the list is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\tconst id = this.objectId || (this.objectData && this.objectData.id)\n\t\t\tif (!this.register || !this.schema || !id || typeof this.register !== 'string' || typeof this.schema !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(this.register)}/${encodeURIComponent(this.schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.saved = null\n\t\t\tthis.loadLive()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.resetDraft()\n\t\tthis.loadLive()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The cells of one table row, one per column.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {Array<{text: string, pill: object|null}>}\n\t\t */\n\t\tcellsOf(entry) {\n\t\t\treturn this.tableColumns.map((column) => ({\n\t\t\t\ttext: this.text(entry, column),\n\t\t\t\tpill: column.pill ? this.pillOf(entry, column) : null,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The items of a row's menu in the table variant: the row actions not\n\t\t * placed in the band, then remove.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<{id: string, label: string, run: Function}>}\n\t\t */\n\t\tmenuItems(row) {\n\t\t\tconst items = row.actions\n\t\t\t\t.filter((action) => action.placement !== 'band')\n\t\t\t\t.map((action) => ({ id: action.id, label: action.label, run: () => this.runAction(action, row) }))\n\t\t\tif (this.canRemove) {\n\t\t\t\titems.push({ id: 'remove', label: this.removeLabel, run: () => this.remove(row) })\n\t\t\t}\n\t\t\treturn items\n\t\t},\n\n\t\t/**\n\t\t * The identity of an entry.\n\t\t *\n\t\t * @param {unknown} item The entry.\n\t\t * @param {number} index Its position.\n\t\t * @return {string}\n\t\t */\n\t\tkeyOf(item, index) {\n\t\t\tif (item === null || typeof item !== 'object') {\n\t\t\t\treturn String(item)\n\t\t\t}\n\t\t\tconst key = this.content.key ? readPath(item, this.content.key) : undefined\n\t\t\treturn key === undefined || key === null || key === '' ? `item-${index}` : String(key)\n\t\t},\n\n\t\t/**\n\t\t * A display text off an entry: a path, or `{ path, map?, format? }`.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {string|object} spec The spec.\n\t\t * @return {string}\n\t\t */\n\t\ttext(entry, spec) {\n\t\t\tif (!spec) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst def = typeof spec === 'string' ? { path: spec } : spec\n\t\t\tconst raw = readPath(entry, def.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (def.format === 'date' || def.format === 'date-long') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\tif (date === null) {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\treturn def.format === 'date-long' ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1)) : formatBoardDate(date)\n\t\t\t}\n\t\t\tif (def.map && Object.hasOwn(def.map, String(raw))) {\n\t\t\t\treturn this.tr(def.map[String(raw)])\n\t\t\t}\n\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t},\n\n\t\tpillOf(entry, spec) {\n\t\t\tif (!spec || !spec.path) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(entry, spec.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.text(entry, spec)\n\t\t\tconst variant = spec.variantMap && spec.variantMap[String(raw)]\n\t\t\treturn { label, variant: isBadgeVariant(variant) ? variant : 'default' }\n\t\t},\n\n\t\tasideOf(entry, spec) {\n\t\t\tconst value = this.text(entry, spec)\n\t\t\tif (value === '') {\n\t\t\t\t// No value: the optional `fallback` label, translated, instead of an empty aside.\n\t\t\t\treturn this.tr(spec.fallback)\n\t\t\t}\n\t\t\treturn spec.prefix ? `${this.tr(spec.prefix)} ${value}` : value\n\t\t},\n\n\t\t/**\n\t\t * Whether an entry matches a `{ field: value }` condition (every field equal).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {object|undefined} when The condition.\n\t\t * @return {boolean}\n\t\t */\n\t\tmatches(entry, when) {\n\t\t\tif (!when || typeof when !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Object.entries(when).every(([field, expected]) => {\n\t\t\t\tconst actual = readPath(entry, field)\n\t\t\t\tif (expected === false) {\n\t\t\t\t\treturn actual === false || actual === undefined || actual === null || actual === ''\n\t\t\t\t}\n\t\t\t\treturn String(actual) === String(expected)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Fill `{path}` placeholders from an entry; '' when one stays empty.\n\t\t *\n\t\t * @param {string} template The template, translated first.\n\t\t * @param {object} entry The entry.\n\t\t * @return {string}\n\t\t */\n\t\tfill(template, entry) {\n\t\t\tif (typeof template !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tlet missing = false\n\t\t\tconst out = this.tr(template).replace(/\\{([\\w.@]+)\\}/g, (match, path) => {\n\t\t\t\tconst value = readPath(entry, path)\n\t\t\t\tif (value === undefined || value === null || value === '') {\n\t\t\t\t\tmissing = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(value)\n\t\t\t})\n\t\t\treturn missing ? '' : out\n\t\t},\n\n\t\tactionsFor(entry) {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst actions = Array.isArray(this.content.rowActions) ? this.content.rowActions : []\n\t\t\treturn actions\n\t\t\t\t.filter((action) => action && action.id && action.request && action.request.url && this.matches(entry, action.when))\n\t\t\t\t.map((action) => ({ ...action, label: this.tr(action.label || action.id) }))\n\t\t},\n\n\t\tinitials(name) {\n\t\t\treturn String(name || '').split(/[\\s@._-]+/).filter(Boolean).slice(0, 2).map((part) => part.charAt(0).toUpperCase()).join('')\n\t\t},\n\n\t\tremoveAriaLabel(row) {\n\t\t\treturn `${this.removeLabel}: ${row.title}`\n\t\t},\n\n\t\tresetDraft() {\n\t\t\tconst draft = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst first = field.type === 'select' && field.options.length > 0 ? field.options[0].value : ''\n\t\t\t\tdraft[field.key] = field.default !== undefined ? field.default : first\n\t\t\t}\n\t\t\tthis.draft = draft\n\t\t\tthis.errors = {}\n\t\t},\n\n\t\t/**\n\t\t * Resolve `@objectId`, `@object.<field>` and `@row.<field>` inside a string or a payload.\n\t\t *\n\t\t * @param {unknown} value The value.\n\t\t * @param {object} entry The row entry.\n\t\t * @return {unknown}\n\t\t */\n\t\tresolve(value, entry) {\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\treturn value.map((v) => this.resolve(v, entry))\n\t\t\t}\n\t\t\tif (value && typeof value === 'object') {\n\t\t\t\treturn Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.resolve(v, entry)]))\n\t\t\t}\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst object = this.objectData || {}\n\t\t\tconst read = (source, path) => {\n\t\t\t\tconst v = readPath(source || {}, path)\n\t\t\t\treturn v === undefined || v === null ? '' : v\n\t\t\t}\n\t\t\tconst whole = value.match(/^@(row|object)\\.([\\w.@]+)$/)\n\t\t\tif (whole) {\n\t\t\t\treturn read(whole[1] === 'row' ? entry : object, whole[2])\n\t\t\t}\n\t\t\treturn value\n\t\t\t\t.replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))\n\t\t\t\t.replace(/@(row|object)\\.([\\w.@]+)/g, (m, scope, path) => encodeURIComponent(String(read(scope === 'row' ? entry : object, path))))\n\t\t},\n\n\t\tasync http() {\n\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\treturn { axios }\n\t\t},\n\n\t\t/**\n\t\t * Read live per-row state from `rowSource` and keep it by key.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLive() {\n\t\t\tconst source = this.content.rowSource\n\t\t\tif (!source || !source.url || !source.key || !this.objectData) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst response = await axios.get(prefixUrl(this.resolve(source.url, {})))\n\t\t\t\tconst data = response && response.data\n\t\t\t\tconst list = source.path ? readPath(data, source.path) : data\n\t\t\t\tconst live = {}\n\t\t\t\tfor (const item of Array.isArray(list) ? list : []) {\n\t\t\t\t\tconst key = item && readPath(item, source.key)\n\t\t\t\t\tif (key !== undefined && key !== null) {\n\t\t\t\t\t\tlive[String(key)] = item\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.live = live\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFieldListWidget] rowSource could not be read:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save a new list: re-read the stored value, apply the change to it, and\n\t\t * PATCH that one field.\n\t\t *\n\t\t * @param {(stored: Array) => Array} change Builds the next list from the stored one, or throws an Error with a `userMessage`.\n\t\t * @param {string} busyKey What is busy.\n\t\t * @return {Promise<boolean>} Whether it saved.\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-saves-one-field-with-one-patch\n\t\t */\n\t\tasync save(change, busyKey) {\n\t\t\tif (!this.objectUrl) {\n\t\t\t\tthis.notify(this.tr('This list cannot be saved here.'))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthis.busy = busyKey\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst { data: stored } = await axios.get(prefixUrl(this.objectUrl))\n\t\t\t\tconst current = readPath(stored || {}, this.content.field)\n\t\t\t\tconst next = change(Array.isArray(current) ? current : [])\n\t\t\t\tconst { data } = await axios.patch(prefixUrl(this.objectUrl), { [this.content.field]: next })\n\t\t\t\tconst written = readPath(data || {}, this.content.field)\n\t\t\t\tthis.saved = Array.isArray(written) ? written : next\n\t\t\t\t/**\n\t\t\t\t * @event saved The list was saved: one PATCH of the field succeeded.\n\t\t\t\t * @type {{field: string, value: Array}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: this.saved })\n\t\t\t\treturn true\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(e && e.userMessage ? e.userMessage : serverErrorMessage(e))\n\t\t\t\treturn false\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\n\t\tnotify(message, type = 'error') {\n\t\t\tthis.notice = message\n\t\t\tthis.noticeType = type\n\t\t},\n\n\t\t/**\n\t\t * Validate the draft and add it.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-adds-and-removes-entries\n\t\t */\n\t\tasync add() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tconst empty = value === undefined || value === null || String(value).trim() === ''\n\t\t\t\tif (field.required && empty) {\n\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t} else if (!empty && field.pattern && !new RegExp(field.pattern).test(String(value).trim())) {\n\t\t\t\t\terrors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.errors = errors\n\t\t\tif (Object.keys(errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst entry = { ...((this.content.add && this.content.add.defaults) || {}) }\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tif (value !== undefined && value !== null && String(value).trim() !== '') {\n\t\t\t\t\tentry[field.key] = typeof value === 'string' ? value.trim() : value\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unique = this.content.add && this.content.add.unique\n\t\t\tconst saved = await this.save((stored) => {\n\t\t\t\tif (unique && stored.some((item) => item && String(readPath(item, unique)) === String(entry[unique]))) {\n\t\t\t\t\tconst error = new Error('duplicate')\n\t\t\t\t\terror.userMessage = this.tr('This is already in the list.')\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\treturn [...stored, entry]\n\t\t\t}, '*')\n\t\t\tif (saved) {\n\t\t\t\tthis.resetDraft()\n\t\t\t\tthis.adding = false\n\t\t\t\tthis.loadLive()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an entry by its key.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync remove(row) {\n\t\t\tawait this.save((stored) => stored.filter((item, index) => this.keyOf(item, index) !== row.key), row.key)\n\t\t},\n\n\t\t/**\n\t\t * Run a row action's request, then re-read the live state.\n\t\t *\n\t\t * @param {object} action The action.\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request\n\t\t */\n\t\tasync runAction(action, row) {\n\t\t\tthis.busy = row.key\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst method = String(action.request.method || 'POST').toLowerCase()\n\t\t\t\tconst payload = this.resolve(action.request.payload || {}, row.entry)\n\t\t\t\tconst response = method === 'get'\n\t\t\t\t\t? await axios.get(prefixUrl(this.resolve(action.request.url, row.entry)))\n\t\t\t\t\t: await axios[method](prefixUrl(this.resolve(action.request.url, row.entry)), payload)\n\t\t\t\tconst message = response && response.data && typeof response.data.message === 'string' ? response.data.message : ''\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.notify(message, 'info')\n\t\t\t\t}\n\t\t\t\tawait this.loadLive()\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(serverErrorMessage(e))\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-list-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-field-list-widget__row {\n\tpadding: 10px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-field-list-widget__row:first-child {\n\tborder-top: none;\n\tpadding-top: 0;\n}\n\n.cn-field-list-widget__line {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__avatar {\n\tflex: 0 0 auto;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: 50%;\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text, var(--color-main-text));\n\tfont-size: 12px;\n\tfont-weight: 700;\n}\n\n.cn-field-list-widget__main {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__title {\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__detail {\n\tfont-size: 13px;\n}\n\n.cn-field-list-widget__aside {\n\twhite-space: nowrap;\n}\n\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tmargin: 4px 0 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__add {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tgap: 8px 12px;\n}\n\n.cn-field-list-widget__add > * {\n\tflex: 1 1 180px;\n}\n\n.cn-field-list-widget__add > button {\n\tflex: 0 0 auto;\n}\n\n.cn-field-list-widget__toolbar {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n}\n\n.cn-field-list-widget__table-wrap {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\toverflow-x: auto;\n}\n\n.cn-field-list-widget__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-field-list-widget__th {\n\tpadding: 8px 12px;\n\ttext-align: start;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-list-widget__th--menu,\n.cn-field-list-widget__td--menu {\n\twidth: 1%;\n\ttext-align: end;\n}\n\n.cn-field-list-widget__tr {\n\tborder-top: 1px solid var(--cn-board-hairline, var(--color-border));\n}\n\n.cn-field-list-widget__td {\n\tpadding: 10px 12px;\n\tvertical-align: middle;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__sr {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n}\n\n.cn-field-list-widget__band {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-warning);\n\tborder-radius: 10px;\n\tbackground: color-mix(in srgb, var(--color-warning) 14%, var(--color-main-background));\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createTextVNode","_Fragment","_renderList","_createBlock","_createVNode","_toDisplayString","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnFieldListWidget.vue3.js","sources":["../../../../src/components/CnFieldListWidget/CnFieldListWidget.vue?vue&type=template&id=643f1dd6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-field-list-widget\" data-testid=\"cn-field-list-widget\">\n\t\t<div v-if=\"canAdd && addTrigger && addPlacement === 'header'\" class=\"cn-field-list-widget__toolbar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-open-add\"\n\t\t\t\t@click=\"adding = !adding\">\n\t\t\t\t+ {{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<form\n\t\t\tv-if=\"canAdd && addTrigger && addPlacement === 'header' && adding\"\n\t\t\tclass=\"cn-field-list-widget__add\"\n\t\t\tdata-testid=\"cn-field-list-add\"\n\t\t\t@submit.prevent=\"add\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in addFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-field-list-widget__field\"\n\t\t\t\t:data-testid=\"`cn-field-list-input-${field.key}`\">\n\t\t\t\t<NcSelect\n\t\t\t\t\tv-if=\"field.type === 'select'\"\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:inputLabel=\"field.label\"\n\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t:reduce=\"(option) => option.value\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\tlabel=\"label\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:label=\"field.label\"\n\t\t\t\t\t:error=\"Boolean(errors[field.key])\"\n\t\t\t\t\t:helperText=\"errors[field.key] || ''\" />\n\t\t\t</div>\n\t\t\t<NcButton type=\"submit\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-submit\">\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</form>\n\t\t<p v-if=\"rows.length === 0\" class=\"cn-field-list-widget__empty\" data-testid=\"cn-field-list-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<div v-else-if=\"isTable\" class=\"cn-field-list-widget__table-wrap\">\n\t\t\t<table class=\"cn-field-list-widget__table\" data-testid=\"cn-field-list-table\">\n\t\t\t\t<thead>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<th\n\t\t\t\t\t\t\tv-for=\"column in tableColumns\"\n\t\t\t\t\t\t\t:key=\"column.key\"\n\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\tclass=\"cn-field-list-widget__th\">\n\t\t\t\t\t\t\t{{ column.header }}\n\t\t\t\t\t\t</th>\n\t\t\t\t\t\t<th v-if=\"hasMenu\" scope=\"col\" class=\"cn-field-list-widget__th cn-field-list-widget__th--menu\">\n\t\t\t\t\t\t\t<span class=\"cn-field-list-widget__sr\">{{ tr('Actions') }}</span>\n\t\t\t\t\t\t</th>\n\t\t\t\t\t</tr>\n\t\t\t\t</thead>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr\n\t\t\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-field-list-widget__tr\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-row-${row.key}`\">\n\t\t\t\t\t\t<td\n\t\t\t\t\t\t\tv-for=\"(column, index) in tableColumns\"\n\t\t\t\t\t\t\t:key=\"column.key\"\n\t\t\t\t\t\t\tclass=\"cn-field-list-widget__td\">\n\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\tv-if=\"column.pill && row.cells[index].pill\"\n\t\t\t\t\t\t\t\t:label=\"row.cells[index].pill.label\"\n\t\t\t\t\t\t\t\t:variant=\"row.cells[index].pill.variant\"\n\t\t\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t\t\t<strong v-else-if=\"column.strong\">{{ row.cells[index].text }}</strong>\n\t\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t\t{{ row.cells[index].text }}\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t\t<td v-if=\"hasMenu\" class=\"cn-field-list-widget__td cn-field-list-widget__td--menu\">\n\t\t\t\t\t\t\t<NcActions\n\t\t\t\t\t\t\t\tv-if=\"menuItems(row).length > 0\"\n\t\t\t\t\t\t\t\t:forceMenu=\"true\"\n\t\t\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t\t\t:ariaLabel=\"`${tr('More actions')}: ${row.title}`\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-field-list-menu-${row.key}`\">\n\t\t\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t\t\tv-for=\"item in menuItems(row)\"\n\t\t\t\t\t\t\t\t\t:key=\"item.id\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"`cn-field-list-menu-${item.id}-${row.key}`\"\n\t\t\t\t\t\t\t\t\t@click=\"item.run\">\n\t\t\t\t\t\t\t\t\t{{ item.label }}\n\t\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t\t</NcActions>\n\t\t\t\t\t\t</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t\t<div\n\t\t\t\tv-for=\"row in bandRows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-field-list-widget__band\"\n\t\t\t\trole=\"note\"\n\t\t\t\t:data-testid=\"`cn-field-list-band-${row.key}`\">\n\t\t\t\t<span class=\"cn-field-list-widget__band-text\">{{ row.detail }}</span>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"copy in row.copies\"\n\t\t\t\t\t:key=\"`copy-${copy.path}`\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t:data-testid=\"`cn-field-list-copy-${copy.path}-${row.key}`\"\n\t\t\t\t\t@click=\"copyValue(copy)\">\n\t\t\t\t\t{{ copy.label }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"action in row.bandActions\"\n\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t:data-testid=\"`cn-field-list-band-action-${action.id}-${row.key}`\"\n\t\t\t\t\t@click=\"runAction(action, row)\">\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\t\t<ul v-else class=\"cn-field-list-widget__rows\">\n\t\t\t<li\n\t\t\t\tv-for=\"row in rows\"\n\t\t\t\t:key=\"row.key\"\n\t\t\t\tclass=\"cn-field-list-widget__row\"\n\t\t\t\t:data-testid=\"`cn-field-list-row-${row.key}`\">\n\t\t\t\t<div class=\"cn-field-list-widget__line\">\n\t\t\t\t\t<span v-if=\"row.initials\" class=\"cn-field-list-widget__avatar\" aria-hidden=\"true\">{{ row.initials }}</span>\n\t\t\t\t\t<span class=\"cn-field-list-widget__main\">\n\t\t\t\t\t\t<span class=\"cn-field-list-widget__title\">{{ row.title }}</span>\n\t\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-field-list-widget__meta\">{{ row.meta }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-if=\"row.pill\"\n\t\t\t\t\t\t:label=\"row.pill.label\"\n\t\t\t\t\t\t:variant=\"row.pill.variant\"\n\t\t\t\t\t\tsize=\"small\" />\n\t\t\t\t\t<span v-if=\"row.aside\" class=\"cn-field-list-widget__aside\">{{ row.aside }}</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"action in row.actions\"\n\t\t\t\t\t\t:key=\"action.id\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-action-${action.id}-${row.key}`\"\n\t\t\t\t\t\t@click=\"runAction(action, row)\">\n\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-if=\"canRemove\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\t\t\t:aria-label=\"removeAriaLabel(row)\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-remove-${row.key}`\"\n\t\t\t\t\t\t@click=\"remove(row)\">\n\t\t\t\t\t\t{{ removeLabel }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<p v-if=\"row.detail\" class=\"cn-field-list-widget__detail\">\n\t\t\t\t\t{{ row.detail }}\n\t\t\t\t</p>\n\t\t\t\t<div v-if=\"row.detail && row.copies.length > 0\" class=\"cn-field-list-widget__copies\">\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"copy in row.copies\"\n\t\t\t\t\t\t:key=\"`copy-${copy.path}`\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:data-testid=\"`cn-field-list-copy-${copy.path}-${row.key}`\"\n\t\t\t\t\t\t@click=\"copyValue(copy)\">\n\t\t\t\t\t\t{{ copy.label }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<p v-if=\"hint\" class=\"cn-field-list-widget__hint\">\n\t\t\t{{ hint }}\n\t\t</p>\n\n\t\t<div v-if=\"canAdd && addTrigger && addPlacement !== 'header' && !adding\" class=\"cn-field-list-widget__toolbar\">\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-open-add\"\n\t\t\t\t@click=\"adding = true\">\n\t\t\t\t+ {{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<form\n\t\t\tv-if=\"canAdd && !(addTrigger && addPlacement === 'header') && (!addTrigger || adding)\"\n\t\t\tclass=\"cn-field-list-widget__add\"\n\t\t\tdata-testid=\"cn-field-list-add\"\n\t\t\t@submit.prevent=\"add\">\n\t\t\t<div\n\t\t\t\tv-for=\"field in addFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\tclass=\"cn-field-list-widget__field\"\n\t\t\t\t:data-testid=\"`cn-field-list-input-${field.key}`\">\n\t\t\t\t<NcSelect\n\t\t\t\t\tv-if=\"field.type === 'select'\"\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:inputLabel=\"field.label\"\n\t\t\t\t\t:options=\"field.options\"\n\t\t\t\t\t:reduce=\"(option) => option.value\"\n\t\t\t\t\t:clearable=\"false\"\n\t\t\t\t\tlabel=\"label\" />\n\t\t\t\t<NcTextField\n\t\t\t\t\tv-else\n\t\t\t\t\tv-model=\"draft[field.key]\"\n\t\t\t\t\t:label=\"field.label\"\n\t\t\t\t\t:error=\"Boolean(errors[field.key])\"\n\t\t\t\t\t:helperText=\"errors[field.key] || ''\" />\n\t\t\t</div>\n\t\t\t<NcButton type=\"submit\"\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:disabled=\"busy !== ''\"\n\t\t\t\tdata-testid=\"cn-field-list-submit\">\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</form>\n\n\t\t<span class=\"cn-field-list-widget__sr\" aria-live=\"polite\" data-testid=\"cn-field-list-copied\">{{ copied }}</span>\n\t\t<NcNoteCard v-if=\"notice\" :type=\"noticeType\" data-testid=\"cn-field-list-notice\">\n\t\t\t{{ notice }}\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\n\n/**\n * CnFieldListWidget — the `field-list` widget: a list stored INSIDE one field\n * of the current object (a portal's `domains[]`, its `editors[]`), shown as\n * rows and edited in place.\n *\n * Rows are drawn from configuration (title, meta line, pill, aside, a detail\n * line); a small form adds an entry, a button removes one, and each change is\n * saved as ONE PATCH of that field on the object, after re-reading the stored\n * value so a concurrent change to the same list is not overwritten with a\n * stale copy. Optional row actions call a configured endpoint (a \"check\n * again\"), and an optional `rowSource` merges live per-row state read from an\n * endpoint by key without writing it back.\n *\n * The server decides who may write: a refused PATCH shows the server's words\n * and leaves the list as stored.\n *\n * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md\n */\nexport default {\n\tname: 'CnFieldListWidget',\n\n\tcomponents: { CnStatusBadge, NcActionButton, NcActions, NcButton, NcNoteCard, NcSelect, NcTextField },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `field` (required) — the object field holding the array.\n\t\t * - `key` — the item field that identifies an entry (default: the item itself for strings).\n\t\t * - `row` — `{ title, titleFallback?, avatar?, meta?: [paths|{ path, map?, format? }], pill?: { path, map?, variantMap? }, aside?: { path, format?, prefix?, fallback? }, detail?: { template, when?, copy?: [{ path, label }] } }`. `detail.copy` puts a copy button beside the detail line for each named value of the row (an empty value gets none), such as a DNS record's name and value.\n\t\t * - `add` — `{ label?, fields: [{ key, label, type?: 'text'|'select', options?, required?, pattern?, patternMessage? }], unique?, defaults? }`.\n\t\t * - `remove` — `{ label? }`; omitted = no remove.\n\t\t * - `rowActions` — `[{ id, label, when?, placement?: 'band', request: { method?, url, payload? } }]`; tokens `@object.<field>`, `@objectId`, `@row.<field>`.\n\t\t * - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.\n\t\t * - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap? }]`: rows as a table; a per-row \"...\" menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.\n\t\t * - `add.trigger: 'button'`, `add.placement: 'header' | 'footer'` — the add form opens from a button.\n\t\t * - `readOnly`, `hint`, `emptyText`.\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 detail-page object. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current object id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['saved'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The list as last saved by this widget; null = read it off objectData. */\n\t\t\tsaved: null,\n\t\t\t/** Live per-row state from `rowSource`, by key. */\n\t\t\tlive: {},\n\t\t\tdraft: {},\n\t\t\t/** Whether the add form is open (only with `add.trigger: 'button'`). */\n\t\t\tadding: false,\n\t\t\terrors: {},\n\t\t\tbusy: '',\n\t\t\tnotice: '',\n\t\t\tnoticeType: 'error',\n\t\t\t// What the last copy button put on the clipboard, said to a screen reader.\n\t\t\tcopied: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The stored list.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\titems() {\n\t\t\tconst list = this.saved !== null ? this.saved : readPath(this.objectData || {}, this.content.field)\n\t\t\treturn Array.isArray(list) ? list : []\n\t\t},\n\n\t\t/**\n\t\t * The rows to draw.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-draws-the-entries-of-one-object-field\n\t\t */\n\t\trows() {\n\t\t\tconst spec = this.content.row || {}\n\t\t\treturn this.items.map((item, index) => {\n\t\t\t\tconst key = this.keyOf(item, index)\n\t\t\t\tconst entry = item && typeof item === 'object'\n\t\t\t\t\t? { ...item, ...(this.live[key] || {}) }\n\t\t\t\t\t: { value: item, ...(this.live[key] || {}) }\n\t\t\t\tconst title = this.text(entry, spec.title) || this.text(entry, spec.titleFallback) || (typeof item === 'string' ? item : key)\n\t\t\t\tconst meta = (Array.isArray(spec.meta) ? spec.meta : [])\n\t\t\t\t\t.map((part) => this.text(entry, part))\n\t\t\t\t\t.filter((part) => part !== '')\n\t\t\t\t\t.join(' · ')\n\t\t\t\treturn {\n\t\t\t\t\tkey,\n\t\t\t\t\titem,\n\t\t\t\t\tentry,\n\t\t\t\t\ttitle,\n\t\t\t\t\tinitials: spec.avatar ? this.initials(title) : '',\n\t\t\t\t\tmeta,\n\t\t\t\t\tpill: this.pillOf(entry, spec.pill),\n\t\t\t\t\taside: spec.aside ? this.asideOf(entry, spec.aside) : '',\n\t\t\t\t\tdetail: spec.detail && this.matches(entry, spec.detail.when) ? this.fill(spec.detail.template, entry) : '',\n\t\t\t\t\tcopies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],\n\t\t\t\t\tactions: this.actionsFor(entry),\n\t\t\t\t\tcells: this.isTable ? this.cellsOf(entry) : [],\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/** Whether the list is read-only. */\n\t\treadOnly() {\n\t\t\treturn this.content.readOnly === true\n\t\t},\n\n\t\t/** Whether entries can be added. */\n\t\tcanAdd() {\n\t\t\treturn !this.readOnly && this.addFields.length > 0 && Boolean(this.content.field)\n\t\t},\n\n\t\t/** Whether entries can be removed. */\n\t\tcanRemove() {\n\t\t\treturn !this.readOnly && Boolean(this.content.remove) && Boolean(this.content.field)\n\t\t},\n\n\t\t/**\n\t\t * The add form's fields, labels translated, select options translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\taddFields() {\n\t\t\tconst fields = this.content.add && Array.isArray(this.content.add.fields) ? this.content.add.fields : []\n\t\t\treturn fields\n\t\t\t\t.filter((field) => field && field.key)\n\t\t\t\t.map((field) => ({\n\t\t\t\t\t...field,\n\t\t\t\t\tlabel: this.tr(field.label || field.key),\n\t\t\t\t\toptions: (Array.isArray(field.options) ? field.options : []).map((option) => (\n\t\t\t\t\t\ttypeof option === 'object' ? { value: option.value, label: this.tr(option.label || String(option.value)) } : { value: option, label: this.tr(String(option)) }\n\t\t\t\t\t)),\n\t\t\t\t}))\n\t\t},\n\n\t\t/** Whether the rows are drawn as a table (`variant: 'table'`). */\n\t\tisTable() {\n\t\t\treturn this.content.variant === 'table' && this.tableColumns.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The table columns, headers translated.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\ttableColumns() {\n\t\t\treturn (Array.isArray(this.content.columns) ? this.content.columns : [])\n\t\t\t\t.filter((column) => column && column.path)\n\t\t\t\t.map((column, index) => ({ ...column, key: `${column.path}-${index}`, header: this.tr(column.header || '') }))\n\t\t},\n\n\t\t/** Whether each row has a \"more actions\" menu (row actions not placed in the band, and remove). */\n\t\thasMenu() {\n\t\t\treturn this.isTable && !this.readOnly && (this.canRemove || this.rows.some((row) => this.menuItems(row).length > 0))\n\t\t},\n\n\t\t/** Rows with a detail line: in the table variant it is a warning band under the table. */\n\t\tbandRows() {\n\t\t\treturn this.rows\n\t\t\t\t.filter((row) => row.detail !== '')\n\t\t\t\t.map((row) => ({ ...row, bandActions: row.actions.filter((action) => action.placement === 'band') }))\n\t\t},\n\n\t\t/** Whether the add form opens from a button (`add.trigger: 'button'`). */\n\t\taddTrigger() {\n\t\t\treturn Boolean(this.content.add && this.content.add.trigger === 'button')\n\t\t},\n\n\t\t/** Where the add button sits: `header` (above the rows) or `footer` (default). */\n\t\taddPlacement() {\n\t\t\treturn this.content.add && this.content.add.placement === 'header' ? 'header' : 'footer'\n\t\t},\n\n\t\taddLabel() {\n\t\t\treturn this.tr((this.content.add && this.content.add.label) || 'Add')\n\t\t},\n\n\t\tremoveLabel() {\n\t\t\treturn this.tr((this.content.remove && this.content.remove.label) || 'Remove')\n\t\t},\n\n\t\thint() {\n\t\t\treturn this.tr(this.content.hint || '')\n\t\t},\n\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content.emptyText || 'Nothing listed yet')\n\t\t},\n\n\t\t/** The object URL the list is saved to, or '' when it is not known. */\n\t\tobjectUrl() {\n\t\t\tconst id = this.objectId || (this.objectData && this.objectData.id)\n\t\t\tif (!this.register || !this.schema || !id || typeof this.register !== 'string' || typeof this.schema !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(this.register)}/${encodeURIComponent(this.schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\t},\n\n\twatch: {\n\t\tobjectData() {\n\t\t\tthis.saved = null\n\t\t\tthis.loadLive()\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.resetDraft()\n\t\tthis.loadLive()\n\t},\n\n\tmethods: {\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The cells of one table row, one per column.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @return {Array<{text: string, pill: object|null}>}\n\t\t */\n\t\tcellsOf(entry) {\n\t\t\treturn this.tableColumns.map((column) => ({\n\t\t\t\ttext: this.text(entry, column),\n\t\t\t\tpill: column.pill ? this.pillOf(entry, column) : null,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The items of a row's menu in the table variant: the row actions not\n\t\t * placed in the band, then remove.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Array<{id: string, label: string, run: Function}>}\n\t\t */\n\t\tmenuItems(row) {\n\t\t\tconst items = row.actions\n\t\t\t\t.filter((action) => action.placement !== 'band')\n\t\t\t\t.map((action) => ({ id: action.id, label: action.label, run: () => this.runAction(action, row) }))\n\t\t\tif (this.canRemove) {\n\t\t\t\titems.push({ id: 'remove', label: this.removeLabel, run: () => this.remove(row) })\n\t\t\t}\n\t\t\treturn items\n\t\t},\n\n\t\t/**\n\t\t * The identity of an entry.\n\t\t *\n\t\t * @param {unknown} item The entry.\n\t\t * @param {number} index Its position.\n\t\t * @return {string}\n\t\t */\n\t\tkeyOf(item, index) {\n\t\t\tif (item === null || typeof item !== 'object') {\n\t\t\t\treturn String(item)\n\t\t\t}\n\t\t\tconst key = this.content.key ? readPath(item, this.content.key) : undefined\n\t\t\treturn key === undefined || key === null || key === '' ? `item-${index}` : String(key)\n\t\t},\n\n\t\t/**\n\t\t * A display text off an entry: a path, or `{ path, map?, format? }`.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {string|object} spec The spec.\n\t\t * @return {string}\n\t\t */\n\t\ttext(entry, spec) {\n\t\t\tif (!spec) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst def = typeof spec === 'string' ? { path: spec } : spec\n\t\t\tconst raw = readPath(entry, def.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (def.format === 'date' || def.format === 'date-long') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\tif (date === null) {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\treturn def.format === 'date-long' ? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1)) : formatBoardDate(date)\n\t\t\t}\n\t\t\tif (def.map && Object.hasOwn(def.map, String(raw))) {\n\t\t\t\treturn this.tr(def.map[String(raw)])\n\t\t\t}\n\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t},\n\n\t\tpillOf(entry, spec) {\n\t\t\tif (!spec || !spec.path) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = readPath(entry, spec.path)\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.text(entry, spec)\n\t\t\tconst variant = spec.variantMap && spec.variantMap[String(raw)]\n\t\t\treturn { label, variant: isBadgeVariant(variant) ? variant : 'default' }\n\t\t},\n\n\t\tasideOf(entry, spec) {\n\t\t\tconst value = this.text(entry, spec)\n\t\t\tif (value === '') {\n\t\t\t\t// No value: the optional `fallback` label, translated, instead of an empty aside.\n\t\t\t\treturn this.tr(spec.fallback)\n\t\t\t}\n\t\t\treturn spec.prefix ? `${this.tr(spec.prefix)} ${value}` : value\n\t\t},\n\n\t\t/**\n\t\t * Whether an entry matches a `{ field: value }` condition (every field equal).\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {object|undefined} when The condition.\n\t\t * @return {boolean}\n\t\t */\n\t\tmatches(entry, when) {\n\t\t\tif (!when || typeof when !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn Object.entries(when).every(([field, expected]) => {\n\t\t\t\tconst actual = readPath(entry, field)\n\t\t\t\tif (expected === false) {\n\t\t\t\t\treturn actual === false || actual === undefined || actual === null || actual === ''\n\t\t\t\t}\n\t\t\t\treturn String(actual) === String(expected)\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Fill `{path}` placeholders from an entry; '' when one stays empty.\n\t\t *\n\t\t * @param {string} template The template, translated first.\n\t\t * @param {object} entry The entry.\n\t\t * @return {string}\n\t\t */\n\t\tfill(template, entry) {\n\t\t\tif (typeof template !== 'string') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tlet missing = false\n\t\t\tconst out = this.tr(template).replace(/\\{([\\w.@]+)\\}/g, (match, path) => {\n\t\t\t\tconst value = readPath(entry, path)\n\t\t\t\tif (value === undefined || value === null || value === '') {\n\t\t\t\t\tmissing = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(value)\n\t\t\t})\n\t\t\treturn missing ? '' : out\n\t\t},\n\n\t\tactionsFor(entry) {\n\t\t\tif (this.readOnly) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst actions = Array.isArray(this.content.rowActions) ? this.content.rowActions : []\n\t\t\treturn actions\n\t\t\t\t.filter((action) => action && action.id && action.request && action.request.url && this.matches(entry, action.when))\n\t\t\t\t.map((action) => ({ ...action, label: this.tr(action.label || action.id) }))\n\t\t},\n\n\t\tinitials(name) {\n\t\t\treturn String(name || '').split(/[\\s@._-]+/).filter(Boolean).slice(0, 2).map((part) => part.charAt(0).toUpperCase()).join('')\n\t\t},\n\n\t\tremoveAriaLabel(row) {\n\t\t\treturn `${this.removeLabel}: ${row.title}`\n\t\t},\n\n\t\tresetDraft() {\n\t\t\tconst draft = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst first = field.type === 'select' && field.options.length > 0 ? field.options[0].value : ''\n\t\t\t\tdraft[field.key] = field.default !== undefined ? field.default : first\n\t\t\t}\n\t\t\tthis.draft = draft\n\t\t\tthis.errors = {}\n\t\t},\n\n\t\t/**\n\t\t * Resolve `@objectId`, `@object.<field>` and `@row.<field>` inside a string or a payload.\n\t\t *\n\t\t * @param {unknown} value The value.\n\t\t * @param {object} entry The row entry.\n\t\t * @return {unknown}\n\t\t */\n\t\tresolve(value, entry) {\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\treturn value.map((v) => this.resolve(v, entry))\n\t\t\t}\n\t\t\tif (value && typeof value === 'object') {\n\t\t\t\treturn Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.resolve(v, entry)]))\n\t\t\t}\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tconst object = this.objectData || {}\n\t\t\tconst read = (source, path) => {\n\t\t\t\tconst v = readPath(source || {}, path)\n\t\t\t\treturn v === undefined || v === null ? '' : v\n\t\t\t}\n\t\t\tconst whole = value.match(/^@(row|object)\\.([\\w.@]+)$/)\n\t\t\tif (whole) {\n\t\t\t\treturn read(whole[1] === 'row' ? entry : object, whole[2])\n\t\t\t}\n\t\t\treturn value\n\t\t\t\t.replace(/@objectId/g, () => encodeURIComponent(String(this.objectId || object.id || '')))\n\t\t\t\t.replace(/@(row|object)\\.([\\w.@]+)/g, (m, scope, path) => encodeURIComponent(String(read(scope === 'row' ? entry : object, path))))\n\t\t},\n\n\t\tasync http() {\n\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\treturn { axios }\n\t\t},\n\n\t\t/**\n\t\t * Read live per-row state from `rowSource` and keep it by key.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLive() {\n\t\t\tconst source = this.content.rowSource\n\t\t\tif (!source || !source.url || !source.key || !this.objectData) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst response = await axios.get(prefixUrl(this.resolve(source.url, {})))\n\t\t\t\tconst data = response && response.data\n\t\t\t\tconst list = source.path ? readPath(data, source.path) : data\n\t\t\t\tconst live = {}\n\t\t\t\tfor (const item of Array.isArray(list) ? list : []) {\n\t\t\t\t\tconst key = item && readPath(item, source.key)\n\t\t\t\t\tif (key !== undefined && key !== null) {\n\t\t\t\t\t\tlive[String(key)] = item\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.live = live\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFieldListWidget] rowSource could not be read:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save a new list: re-read the stored value, apply the change to it, and\n\t\t * PATCH that one field.\n\t\t *\n\t\t * @param {(stored: Array) => Array} change Builds the next list from the stored one, or throws an Error with a `userMessage`.\n\t\t * @param {string} busyKey What is busy.\n\t\t * @return {Promise<boolean>} Whether it saved.\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-saves-one-field-with-one-patch\n\t\t */\n\t\tasync save(change, busyKey) {\n\t\t\tif (!this.objectUrl) {\n\t\t\t\tthis.notify(this.tr('This list cannot be saved here.'))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tthis.busy = busyKey\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst { data: stored } = await axios.get(prefixUrl(this.objectUrl))\n\t\t\t\tconst current = readPath(stored || {}, this.content.field)\n\t\t\t\tconst next = change(Array.isArray(current) ? current : [])\n\t\t\t\tconst { data } = await axios.patch(prefixUrl(this.objectUrl), { [this.content.field]: next })\n\t\t\t\tconst written = readPath(data || {}, this.content.field)\n\t\t\t\tthis.saved = Array.isArray(written) ? written : next\n\t\t\t\t/**\n\t\t\t\t * @event saved The list was saved: one PATCH of the field succeeded.\n\t\t\t\t * @type {{field: string, value: Array}}\n\t\t\t\t */\n\t\t\t\tthis.$emit('saved', { field: this.content.field, value: this.saved })\n\t\t\t\treturn true\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(e && e.userMessage ? e.userMessage : serverErrorMessage(e))\n\t\t\t\treturn false\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\n\t\tnotify(message, type = 'error') {\n\t\t\tthis.notice = message\n\t\t\tthis.noticeType = type\n\t\t},\n\n\t\t/**\n\t\t * Validate the draft and add it.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-adds-and-removes-entries\n\t\t */\n\t\tasync add() {\n\t\t\tconst errors = {}\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tconst empty = value === undefined || value === null || String(value).trim() === ''\n\t\t\t\tif (field.required && empty) {\n\t\t\t\t\terrors[field.key] = this.tr('Fill this in.')\n\t\t\t\t} else if (!empty && field.pattern && !new RegExp(field.pattern).test(String(value).trim())) {\n\t\t\t\t\terrors[field.key] = this.tr(field.patternMessage || 'This value is not valid.')\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.errors = errors\n\t\t\tif (Object.keys(errors).length > 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst entry = { ...((this.content.add && this.content.add.defaults) || {}) }\n\t\t\tfor (const field of this.addFields) {\n\t\t\t\tconst value = this.draft[field.key]\n\t\t\t\tif (value !== undefined && value !== null && String(value).trim() !== '') {\n\t\t\t\t\tentry[field.key] = typeof value === 'string' ? value.trim() : value\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unique = this.content.add && this.content.add.unique\n\t\t\tconst saved = await this.save((stored) => {\n\t\t\t\tif (unique && stored.some((item) => item && String(readPath(item, unique)) === String(entry[unique]))) {\n\t\t\t\t\tconst error = new Error('duplicate')\n\t\t\t\t\terror.userMessage = this.tr('This is already in the list.')\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\treturn [...stored, entry]\n\t\t\t}, '*')\n\t\t\tif (saved) {\n\t\t\t\tthis.resetDraft()\n\t\t\t\tthis.adding = false\n\t\t\t\tthis.loadLive()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Remove an entry by its key.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync remove(row) {\n\t\t\tawait this.save((stored) => stored.filter((item, index) => this.keyOf(item, index) !== row.key), row.key)\n\t\t},\n\n\t\t/**\n\t\t * Run a row action's request, then re-read the live state.\n\t\t *\n\t\t * @param {object} action The action.\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/abstract-field-list/specs/field-list/spec.md#requirement-a-field-list-row-can-run-a-configured-request\n\t\t */\n\t\t/**\n\t\t * The copy buttons of a row's detail line: one per named value that is not empty.\n\t\t *\n\t\t * @param {object} entry The entry.\n\t\t * @param {Array<{path: string, label?: string}>|undefined} declared The `detail.copy` list.\n\t\t * @return {Array<{path: string, label: string, value: string}>}\n\t\t */\n\t\tcopiesOf(entry, declared) {\n\t\t\treturn (Array.isArray(declared) ? declared : [])\n\t\t\t\t.filter((copy) => copy && typeof copy.path === 'string' && copy.path !== '')\n\t\t\t\t.map((copy) => ({\n\t\t\t\t\tpath: copy.path,\n\t\t\t\t\tlabel: this.tr(copy.label || 'Copy'),\n\t\t\t\t\tvalue: String(readPath(entry, copy.path) ?? ''),\n\t\t\t\t}))\n\t\t\t\t.filter((copy) => copy.value !== '')\n\t\t},\n\n\t\t/**\n\t\t * Put one value on the clipboard and say so; a browser that refuses says that instead.\n\t\t *\n\t\t * @param {{label: string, value: string}} copy The copy button.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync copyValue(copy) {\n\t\t\tthis.copied = ''\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(copy.value)\n\t\t\t\tthis.copied = `${this.tr('Copied')}: ${copy.label}`\n\t\t\t} catch {\n\t\t\t\tthis.notify(this.tr('Copying did not work. Select the text and copy it yourself.'))\n\t\t\t}\n\t\t},\n\n\t\tasync runAction(action, row) {\n\t\t\tthis.busy = row.key\n\t\t\tthis.notice = ''\n\t\t\ttry {\n\t\t\t\tconst { axios } = await this.http()\n\t\t\t\tconst method = String(action.request.method || 'POST').toLowerCase()\n\t\t\t\tconst payload = this.resolve(action.request.payload || {}, row.entry)\n\t\t\t\tconst response = method === 'get'\n\t\t\t\t\t? await axios.get(prefixUrl(this.resolve(action.request.url, row.entry)))\n\t\t\t\t\t: await axios[method](prefixUrl(this.resolve(action.request.url, row.entry)), payload)\n\t\t\t\tconst message = response && response.data && typeof response.data.message === 'string' ? response.data.message : ''\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.notify(message, 'info')\n\t\t\t\t}\n\t\t\t\tawait this.loadLive()\n\t\t\t} catch (e) {\n\t\t\t\tthis.notify(serverErrorMessage(e))\n\t\t\t} finally {\n\t\t\t\tthis.busy = ''\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-field-list-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__rows {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-field-list-widget__row {\n\tpadding: 10px 0;\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-field-list-widget__row:first-child {\n\tborder-top: none;\n\tpadding-top: 0;\n}\n\n.cn-field-list-widget__line {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__avatar {\n\tflex: 0 0 auto;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 32px;\n\theight: 32px;\n\tborder-radius: 50%;\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text, var(--color-main-text));\n\tfont-size: 12px;\n\tfont-weight: 700;\n}\n\n.cn-field-list-widget__main {\n\tdisplay: flex;\n\tflex-direction: column;\n\tflex: 1 1 auto;\n\tmin-width: 0;\n}\n\n.cn-field-list-widget__title {\n\tfont-weight: 600;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-field-list-widget__meta,\n.cn-field-list-widget__aside,\n.cn-field-list-widget__copies {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px;\n}\n\n.cn-field-list-widget__detail {\n\tfont-size: 13px;\n}\n\n.cn-field-list-widget__aside {\n\twhite-space: nowrap;\n}\n\n.cn-field-list-widget__detail,\n.cn-field-list-widget__hint,\n.cn-field-list-widget__empty {\n\tmargin: 4px 0 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__add {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-end;\n\tgap: 8px 12px;\n}\n\n.cn-field-list-widget__add > * {\n\tflex: 1 1 180px;\n}\n\n.cn-field-list-widget__add > button {\n\tflex: 0 0 auto;\n}\n\n.cn-field-list-widget__toolbar {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n}\n\n.cn-field-list-widget__table-wrap {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\toverflow-x: auto;\n}\n\n.cn-field-list-widget__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-field-list-widget__th {\n\tpadding: 8px 12px;\n\ttext-align: start;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n\tbackground: var(--color-background-hover);\n}\n\n.cn-field-list-widget__th--menu,\n.cn-field-list-widget__td--menu {\n\twidth: 1%;\n\ttext-align: end;\n}\n\n.cn-field-list-widget__tr {\n\tborder-top: 1px solid var(--cn-board-hairline, var(--color-border));\n}\n\n.cn-field-list-widget__td {\n\tpadding: 10px 12px;\n\tvertical-align: middle;\n\toverflow-wrap: anywhere;\n}\n\n.cn-field-list-widget__sr {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n}\n\n.cn-field-list-widget__band {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 8px 12px;\n\tpadding: 12px 14px;\n\tborder: 1px solid var(--color-warning);\n\tborder-radius: 10px;\n\tbackground: color-mix(in srgb, var(--color-warning) 14%, var(--color-main-background));\n\tfont-size: 14px;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createTextVNode","_Fragment","_renderList","_createBlock","_createVNode","_toDisplayString","_createElementVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnGaugeWidget.vue2.js';
2
2
  import { render } from './CnGaugeWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-0ac8e0e8";
5
+ script.__scopeId = "data-v-006ab8f8";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -247,7 +247,7 @@ var script = {
247
247
  * @return {string} The formatted string.
248
248
  */
249
249
  formatNumber(value) {
250
- return formatMetricValue(value, this.resolvedFormat, this.configCtx)
250
+ return formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)
251
251
  },
252
252
 
253
253
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CnGaugeWidget.vue2.js","sources":["../../../../src/components/CnGaugeWidget/CnGaugeWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-gauge-widget\"\n\t\t:class=\"{ 'cn-gauge-widget--linked': isLinked }\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div class=\"cn-gauge-widget__head\">\n\t\t\t<span v-if=\"content.label\" class=\"cn-gauge-widget__label\">{{ resolvedLabel }}</span>\n\t\t\t<span class=\"cn-gauge-widget__pct\" :style=\"{ color: pctColor }\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"16\" />\n\t\t\t\t<span v-else-if=\"error\" :title=\"error\">—</span>\n\t\t\t\t<template v-else>{{ pctLabel }}</template>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<div class=\"cn-gauge-widget__track\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-gauge-widget__fill\"\n\t\t\t\t:style=\"{ width: fillWidth, backgroundColor: barColor }\" />\n\t\t</div>\n\n\t\t<div v-if=\"!loading && !error\" class=\"cn-gauge-widget__foot\">\n\t\t\t<span class=\"cn-gauge-widget__value\">{{ formattedValue }}</span>\n\t\t\t<span class=\"cn-gauge-widget__target\">/ {{ formattedTarget }}</span>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_FILL_COLORS, STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n/**\n * CnGaugeWidget — an abstract utilization / progress-to-target gauge.\n *\n * Reads a `value` scalar from OpenRegister's aggregation `/value` endpoint and\n * compares it against a `target` (either a static number or a second\n * aggregate), rendering the ratio as a labelled progress bar whose fill\n * percentage and colour reflect utilization. Optional warning / danger\n * thresholds recolour the bar (e.g. green under 80%, amber 80–100%, red over\n * 100%). Use it for budget burn, quota usage, pipeline coverage, SLA capacity,\n * etc. Editable in-app like every catalog widget (ADR-041).\n *\n * Resolved by its registry type key `gauge`; reference it from a manifest\n * placement `{ id, type: 'gauge', content: { label, source, target, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Pipeline coverage',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: { register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value', filter: { status: 'open' } },\n * target: { kind: 'static', value: 500000 },\n * thresholds: { warn: 80, danger: 100, invert: false },\n * }\n * ```\n */\nexport default {\n\tname: 'CnGaugeWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` is run through it, exactly as\n\t\t * CnStatWidget does. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location) or `link` (external href) turns the whole\n\t\t * tile into a click-through target (see the widgetLink mixin).\n\t\t *\n\t\t * @type {{label?: string, route?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, filter?: object}, target?: {kind?: ('static'|'aggregate'), value?: number, metric?: string, field?: string, filter?: object}, thresholds?: {warn?: number, danger?: number, invert?: boolean}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnGaugeWidget outside a CnAppRoot ancestor.\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\tdata() {\n\t\treturn {\n\t\t\tvalue: null,\n\t\t\ttarget: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\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 tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/** The unwrapped page-level app config map for `@config.*` resolution. */\n\t\tconfigCtx() {\n\t\t\treturn unwrapAppConfig(this.cnAppConfig)\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Utilization ratio (0–n) of value to target, or null. */\n\t\tratio() {\n\t\t\tconst t = Number(this.target)\n\t\t\tconst v = Number(this.value)\n\t\t\tif (!Number.isFinite(t) || t === 0 || !Number.isFinite(v)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn v / t\n\t\t},\n\n\t\t/** Percentage label, e.g. \"83%\". */\n\t\tpctLabel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\treturn Math.round(this.ratio * 100) + '%'\n\t\t},\n\n\t\t/** Bar fill width, clamped to 100%. */\n\t\tfillWidth() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '0%'\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, this.ratio * 100)) + '%'\n\t\t},\n\n\t\t/** Bar colour from the threshold bands. */\n\t\tbarColor() {\n\t\t\treturn this.level ? STATUS_FILL_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/** Percentage text colour, the readable text shade of the bar's band. */\n\t\tpctColor() {\n\t\t\treturn this.level ? STATUS_TEXT_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/**\n\t\t * The threshold band the value sits in (invert flips good/bad), or null\n\t\t * while there is no value.\n\t\t *\n\t\t * @return {'success'|'warning'|'error'|null}\n\t\t */\n\t\tlevel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst th = this.content.thresholds || {}\n\t\t\tconst pct = this.ratio * 100\n\t\t\tconst warn = Number.isFinite(th.warn) ? th.warn : 80\n\t\t\tconst danger = Number.isFinite(th.danger) ? th.danger : 100\n\t\t\tlet level\n\t\t\tif (pct >= danger) {\n\t\t\t\tlevel = 'danger'\n\t\t\t} else if (pct >= warn) {\n\t\t\t\tlevel = 'warn'\n\t\t\t} else {\n\t\t\t\tlevel = 'ok'\n\t\t\t}\n\t\t\tif (th.invert) {\n\t\t\t\tlevel = level === 'ok' ? 'danger' : level === 'danger' ? 'ok' : 'warn'\n\t\t\t}\n\t\t\tif (level === 'danger') {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'success'\n\t\t},\n\n\t\t/** The value, number-formatted per content.format. */\n\t\tformattedValue() {\n\t\t\treturn this.formatNumber(this.value)\n\t\t},\n\n\t\t/** The target, number-formatted per content.format. */\n\t\tformattedTarget() {\n\t\t\treturn this.formatNumber(this.target)\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify({ s: this.content.source || {}, t: this.content.target || {} })\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchAll()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAll()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {unknown} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, this.configCtx)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the value and resolve the target (static or aggregate).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchAll() {\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst tgt = this.content.target || {}\n\t\t\t\tconst valuePromise = fetchAggregateValue(s)\n\t\t\t\tconst targetPromise = (tgt.kind === 'aggregate')\n\t\t\t\t\t? fetchAggregateValue({ register: s.register, schema: s.schema, metric: tgt.metric, field: tgt.field, filter: tgt.filter || {} })\n\t\t\t\t\t: Promise.resolve(Number(tgt.value))\n\t\t\t\tconst [v, t] = await Promise.all([valuePromise, targetPromise])\n\t\t\t\tthis.value = v\n\t\t\t\tthis.target = t\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-gauge-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 10px 4px;\n}\n\n/* Whole-tile click target when the widget declares a `route`/`link`. */\n.cn-gauge-widget--linked {\n\tcursor: pointer;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius-large, 8px);\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-gauge-widget--linked:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-gauge-widget--linked:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-gauge-widget__head {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-gauge-widget__label {\n\tfont-size: 0.95em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__pct {\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n}\n\n.cn-gauge-widget__track {\n\twidth: 100%;\n\theight: 10px;\n\tborder-radius: 5px;\n\tbackground: var(--color-background-dark);\n\toverflow: hidden;\n}\n\n.cn-gauge-widget__fill {\n\theight: 100%;\n\tborder-radius: 5px;\n\ttransition: width 0.3s ease;\n}\n\n.cn-gauge-widget__foot {\n\tdisplay: flex;\n\talign-items: baseline;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-gauge-widget__value {\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__target {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":[],"mappings":";;;;;;;AAwCA;;AAEC;AACA;;;;AAIA;AACA;;AAEA;;AAEA;AACA;AACA;;AAEA;;;AAGA;AACA;AACA;;AAEA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;CAErB,UAAU,EAAE;EACX,aAAa;EACb;;AAED,CAAA,MAAM,EAAE,CAAC,UAAU,CAAC;;AAEpB,CAAA,MAAM,EAAE;AACP;;AAEC;AACA;;;AAGD,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA,EAAG;AACpC;AACC;;;;AAIA;;AAED,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,KAAK;EAC5C;;AAED,CAAA,KAAK,EAAE;AACN;;;;AAIC;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;;AAED;;;;AAIC;AACA;;EAED,SAAS,EAAE;GACV,IAAI,EAAE,QAAQ;GACd,OAAO,EAAE,IAAI;GACb;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;GACN,KAAK,EAAE,IAAI;GACX,MAAM,EAAE,IAAI;GACZ,OAAO,EAAE,KAAK;AACd,GAAA,KAAK,EAAE,EAAE;AACV,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;;;AAGC;;;AAGD,EAAA,kBAAkB,GAAG;AACpB,GAAA,OAAO,IAAI,CAAC,SAAQ,IAAK,IAAI,CAAC,WAAU;GACxC;;AAED;;;AAGC;AACA;;AAED,EAAA,aAAa,GAAG;GACf,MAAM,KAAI,GAAI,IAAI,CAAC,OAAO,CAAC,MAAI;AAC/B,GAAA,OAAO,QAAQ,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAA,GAAI,EAAC;GACjD;;AAED;EACA,SAAS,GAAG;AACX,GAAA,OAAO,eAAe,CAAC,IAAI,CAAC,WAAW,CAAA;GACvC;;AAED;;;;;AAKC;AACA;AACA;;AAED,EAAA,cAAc,GAAG;AAChB,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,OAAO,CAAC,MAAO,IAAG,GAAC;AACpC,GAAA,MAAM,GAAI,GAAE,EAAE,MAAM,EAAE,IAAI,CAAC,SAAU,GAAA;AACrC,GAAA,MAAM,GAAE,GAAI,EAAE,GAAG,GAAI,GAAA;AACrB,GAAA,KAAK,MAAM,GAAI,IAAG,CAAC,UAAU,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE;AACnD,IAAA,MAAM,GAAE,GAAI,GAAG,CAAC,GAAG,EAAA;AACnB,IAAA,IAAI,OAAO,GAAI,KAAI,YAAY,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,EAAE;AACrD,KAAA,QAAO;AACR,KAAA;AACA,IAAA,MAAM,QAAS,GAAE,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAA;AAC5C,IAAA,GAAG,CAAC,GAAG,CAAE,GAAE,CAAC,OAAO,QAAO,KAAM,YAAY,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA,KAAM,GAAG,IAAI,SAAQ,GAAI,SAAO;AAC9F,IAAA;AACA,GAAA,OAAO,GAAE;GACT;;;AAGD,EAAA,KAAK,GAAG;GACP,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,EAAA;GAC5B,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,EAAA;GAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAE,IAAG,CAAA,KAAM,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;AAC1D,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,OAAO,IAAI,CAAA;GACX;;AAED;EACA,QAAQ,GAAG;AACV,GAAA,IAAI,IAAI,CAAC,KAAI,KAAM,IAAI,EAAE;AACxB,IAAA,OAAO,GAAE;AACV,IAAA;AACA,GAAA,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAM,GAAE,GAAG,CAAE,GAAE,GAAE;GACxC;;AAED;EACA,SAAS,GAAG;AACX,GAAA,IAAI,IAAI,CAAC,KAAI,KAAM,IAAI,EAAE;AACxB,IAAA,OAAO,IAAG;AACX,IAAA;GACA,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAI,GAAI,GAAG,CAAC,CAAA,GAAI,GAAE;GACxD;;AAED;EACA,QAAQ,GAAG;AACV,GAAA,OAAO,IAAI,CAAC,QAAQ,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAE,GAAE,8BAA6B;GAClF;;;EAGD,QAAQ,GAAG;AACV,GAAA,OAAO,IAAI,CAAC,QAAQ,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAE,GAAE,8BAA6B;GAClF;;AAED;;AAEC;AACA;AACA;;AAED,EAAA,KAAK,GAAG;AACP,GAAA,IAAI,IAAI,CAAC,KAAI,KAAM,IAAI,EAAE;AACxB,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,MAAM,EAAG,GAAE,IAAI,CAAC,OAAO,CAAC,UAAW,IAAG,GAAC;GACvC,MAAM,GAAE,GAAI,IAAI,CAAC,KAAM,GAAE,IAAE;AAC3B,GAAA,MAAM,IAAK,GAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,IAAG,GAAI,GAAC;GACnD,MAAM,MAAO,GAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,CAAA,GAAI,EAAE,CAAC,MAAK,GAAI,IAAE;AAC1D,GAAA,IAAI,MAAI;AACR,GAAA,IAAI,GAAI,IAAG,MAAM,EAAE;IAClB,KAAM,GAAE,SAAO;AAChB,IAAA,MAAO,IAAI,GAAE,IAAK,IAAI,EAAE;IACvB,KAAI,GAAI,OAAK;IACZ,MAAK;AACN,IAAA,QAAQ,KAAG;AACZ,IAAA;GACA,IAAI,EAAE,CAAC,MAAM,EAAE;IACd,KAAM,GAAE,KAAI,KAAM,IAAG,GAAI,QAAO,GAAI,KAAM,KAAI,QAAO,GAAI,IAAK,GAAE,OAAK;AACtE,IAAA;AACA,GAAA,IAAI,UAAU,QAAQ,EAAE;IACvB,OAAO,OAAM;AACd,IAAA;AACA,GAAA,IAAI,KAAM,KAAI,MAAM,EAAE;AACrB,IAAA,OAAO,SAAQ;AAChB,IAAA;AACA,GAAA,OAAO,SAAQ;GACf;;;AAGD,EAAA,cAAc,GAAG;AAChB,GAAA,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAA;GACnC;;;AAGD,EAAA,eAAe,GAAG;AACjB,GAAA,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,MAAM,CAAA;GACpC;;;EAGD,SAAS,GAAG;AACX,GAAA,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAK,IAAK,EAAE,EAAE,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAO,IAAG,IAAI,CAAA;GACpF;EACD;;AAED,CAAA,KAAK,EAAE;EACN,SAAS,GAAG;AACX,GAAA,IAAI,CAAC,QAAQ,GAAC;GACd;EACD;;AAED,CAAA,OAAO,GAAG;AACT,EAAA,IAAI,CAAC,QAAQ,GAAC;EACd;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;AACA;AACA;;AAED,EAAA,YAAY,CAAC,KAAK,EAAE;AACnB,GAAA,OAAO,iBAAiB,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,CAAC,SAAS,CAAA;GACnE;;AAED;;AAEC;;;AAGD,EAAA,MAAM,QAAQ,GAAG;GAChB,MAAM,CAAA,GAAI,IAAI,CAAC,OAAO,CAAC,MAAK,IAAK,GAAC;GAClC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,EAAE;AAC7B,IAAA,IAAI,CAAC,KAAM,GAAE,KAAG;AAChB,IAAA,IAAI,CAAC,MAAK,GAAI,KAAG;IACjB,IAAI,CAAC,KAAM,GAAE,GAAC;IACd,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,OAAM,GAAI,KAAG;GAClB,IAAI,CAAC,KAAM,GAAE,GAAC;AACd,GAAA,IAAI;AACH,IAAA,MAAM,GAAI,GAAE,IAAI,CAAC,OAAO,CAAC,MAAO,IAAG,GAAC;AACpC,IAAA,MAAM,eAAe,mBAAmB,CAAC,CAAC,EAAA;AAC1C,IAAA,MAAM,aAAY,GAAI,CAAC,GAAG,CAAC,IAAK,KAAI,WAAW;OAC5C,mBAAmB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAK,IAAK,EAAC,EAAG,CAAA;AAChI,OAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAA;IACpC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAE,GAAE,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,YAAY,EAAE,aAAa,CAAC,EAAA;IAC9D,IAAI,CAAC,KAAI,GAAI,EAAA;IACb,IAAI,CAAC,MAAK,GAAI,EAAA;AACf,IAAA,CAAE,OAAO,CAAC,EAAE;AACX,IAAA,IAAI,CAAC,KAAM,GAAE,CAAC,CAAA,IAAK,CAAC,CAAC,OAAO,KAAK,QAAM;AACvC,IAAA,IAAI,CAAC,KAAM,GAAE,KAAG;AAChB,IAAA,IAAI,CAAC,MAAK,GAAI,KAAG;AAClB,IAAA,SAAU;AACT,IAAA,IAAI,CAAC,OAAQ,GAAE,MAAI;AACpB,IAAA;GACA;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnGaugeWidget.vue2.js","sources":["../../../../src/components/CnGaugeWidget/CnGaugeWidget.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-gauge-widget\"\n\t\t:class=\"{ 'cn-gauge-widget--linked': isLinked }\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div class=\"cn-gauge-widget__head\">\n\t\t\t<span v-if=\"content.label\" class=\"cn-gauge-widget__label\">{{ resolvedLabel }}</span>\n\t\t\t<span class=\"cn-gauge-widget__pct\" :style=\"{ color: pctColor }\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"16\" />\n\t\t\t\t<span v-else-if=\"error\" :title=\"error\">—</span>\n\t\t\t\t<template v-else>{{ pctLabel }}</template>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<div class=\"cn-gauge-widget__track\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-gauge-widget__fill\"\n\t\t\t\t:style=\"{ width: fillWidth, backgroundColor: barColor }\" />\n\t\t</div>\n\n\t\t<div v-if=\"!loading && !error\" class=\"cn-gauge-widget__foot\">\n\t\t\t<span class=\"cn-gauge-widget__value\">{{ formattedValue }}</span>\n\t\t\t<span class=\"cn-gauge-widget__target\">/ {{ formattedTarget }}</span>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_FILL_COLORS, STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n/**\n * CnGaugeWidget — an abstract utilization / progress-to-target gauge.\n *\n * Reads a `value` scalar from OpenRegister's aggregation `/value` endpoint and\n * compares it against a `target` (either a static number or a second\n * aggregate), rendering the ratio as a labelled progress bar whose fill\n * percentage and colour reflect utilization. Optional warning / danger\n * thresholds recolour the bar (e.g. green under 80%, amber 80–100%, red over\n * 100%). Use it for budget burn, quota usage, pipeline coverage, SLA capacity,\n * etc. Editable in-app like every catalog widget (ADR-041).\n *\n * Resolved by its registry type key `gauge`; reference it from a manifest\n * placement `{ id, type: 'gauge', content: { label, source, target, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Pipeline coverage',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: { register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value', filter: { status: 'open' } },\n * target: { kind: 'static', value: 500000 },\n * thresholds: { warn: 80, danger: 100, invert: false },\n * }\n * ```\n */\nexport default {\n\tname: 'CnGaugeWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` is run through it, exactly as\n\t\t * CnStatWidget does. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location) or `link` (external href) turns the whole\n\t\t * tile into a click-through target (see the widgetLink mixin).\n\t\t *\n\t\t * @type {{label?: string, route?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, filter?: object}, target?: {kind?: ('static'|'aggregate'), value?: number, metric?: string, field?: string, filter?: object}, thresholds?: {warn?: number, danger?: number, invert?: boolean}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnGaugeWidget outside a CnAppRoot ancestor.\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\tdata() {\n\t\treturn {\n\t\t\tvalue: null,\n\t\t\ttarget: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\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 tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/** The unwrapped page-level app config map for `@config.*` resolution. */\n\t\tconfigCtx() {\n\t\t\treturn unwrapAppConfig(this.cnAppConfig)\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Utilization ratio (0–n) of value to target, or null. */\n\t\tratio() {\n\t\t\tconst t = Number(this.target)\n\t\t\tconst v = Number(this.value)\n\t\t\tif (!Number.isFinite(t) || t === 0 || !Number.isFinite(v)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn v / t\n\t\t},\n\n\t\t/** Percentage label, e.g. \"83%\". */\n\t\tpctLabel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\treturn Math.round(this.ratio * 100) + '%'\n\t\t},\n\n\t\t/** Bar fill width, clamped to 100%. */\n\t\tfillWidth() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '0%'\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, this.ratio * 100)) + '%'\n\t\t},\n\n\t\t/** Bar colour from the threshold bands. */\n\t\tbarColor() {\n\t\t\treturn this.level ? STATUS_FILL_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/** Percentage text colour, the readable text shade of the bar's band. */\n\t\tpctColor() {\n\t\t\treturn this.level ? STATUS_TEXT_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/**\n\t\t * The threshold band the value sits in (invert flips good/bad), or null\n\t\t * while there is no value.\n\t\t *\n\t\t * @return {'success'|'warning'|'error'|null}\n\t\t */\n\t\tlevel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst th = this.content.thresholds || {}\n\t\t\tconst pct = this.ratio * 100\n\t\t\tconst warn = Number.isFinite(th.warn) ? th.warn : 80\n\t\t\tconst danger = Number.isFinite(th.danger) ? th.danger : 100\n\t\t\tlet level\n\t\t\tif (pct >= danger) {\n\t\t\t\tlevel = 'danger'\n\t\t\t} else if (pct >= warn) {\n\t\t\t\tlevel = 'warn'\n\t\t\t} else {\n\t\t\t\tlevel = 'ok'\n\t\t\t}\n\t\t\tif (th.invert) {\n\t\t\t\tlevel = level === 'ok' ? 'danger' : level === 'danger' ? 'ok' : 'warn'\n\t\t\t}\n\t\t\tif (level === 'danger') {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'success'\n\t\t},\n\n\t\t/** The value, number-formatted per content.format. */\n\t\tformattedValue() {\n\t\t\treturn this.formatNumber(this.value)\n\t\t},\n\n\t\t/** The target, number-formatted per content.format. */\n\t\tformattedTarget() {\n\t\t\treturn this.formatNumber(this.target)\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify({ s: this.content.source || {}, t: this.content.target || {} })\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchAll()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAll()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {unknown} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the value and resolve the target (static or aggregate).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchAll() {\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst tgt = this.content.target || {}\n\t\t\t\tconst valuePromise = fetchAggregateValue(s)\n\t\t\t\tconst targetPromise = (tgt.kind === 'aggregate')\n\t\t\t\t\t? fetchAggregateValue({ register: s.register, schema: s.schema, metric: tgt.metric, field: tgt.field, filter: tgt.filter || {} })\n\t\t\t\t\t: Promise.resolve(Number(tgt.value))\n\t\t\t\tconst [v, t] = await Promise.all([valuePromise, targetPromise])\n\t\t\t\tthis.value = v\n\t\t\t\tthis.target = t\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-gauge-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 10px 4px;\n}\n\n/* Whole-tile click target when the widget declares a `route`/`link`. */\n.cn-gauge-widget--linked {\n\tcursor: pointer;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius-large, 8px);\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-gauge-widget--linked:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-gauge-widget--linked:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-gauge-widget__head {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-gauge-widget__label {\n\tfont-size: 0.95em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__pct {\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n}\n\n.cn-gauge-widget__track {\n\twidth: 100%;\n\theight: 10px;\n\tborder-radius: 5px;\n\tbackground: var(--color-background-dark);\n\toverflow: hidden;\n}\n\n.cn-gauge-widget__fill {\n\theight: 100%;\n\tborder-radius: 5px;\n\ttransition: width 0.3s ease;\n}\n\n.cn-gauge-widget__foot {\n\tdisplay: flex;\n\talign-items: baseline;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-gauge-widget__value {\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__target {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnGaugeWidget.vue3.js","sources":["../../../../src/components/CnGaugeWidget/CnGaugeWidget.vue?vue&type=template&id=0ac8e0e8&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-gauge-widget\"\n\t\t:class=\"{ 'cn-gauge-widget--linked': isLinked }\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div class=\"cn-gauge-widget__head\">\n\t\t\t<span v-if=\"content.label\" class=\"cn-gauge-widget__label\">{{ resolvedLabel }}</span>\n\t\t\t<span class=\"cn-gauge-widget__pct\" :style=\"{ color: pctColor }\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"16\" />\n\t\t\t\t<span v-else-if=\"error\" :title=\"error\">—</span>\n\t\t\t\t<template v-else>{{ pctLabel }}</template>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<div class=\"cn-gauge-widget__track\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-gauge-widget__fill\"\n\t\t\t\t:style=\"{ width: fillWidth, backgroundColor: barColor }\" />\n\t\t</div>\n\n\t\t<div v-if=\"!loading && !error\" class=\"cn-gauge-widget__foot\">\n\t\t\t<span class=\"cn-gauge-widget__value\">{{ formattedValue }}</span>\n\t\t\t<span class=\"cn-gauge-widget__target\">/ {{ formattedTarget }}</span>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_FILL_COLORS, STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n/**\n * CnGaugeWidget — an abstract utilization / progress-to-target gauge.\n *\n * Reads a `value` scalar from OpenRegister's aggregation `/value` endpoint and\n * compares it against a `target` (either a static number or a second\n * aggregate), rendering the ratio as a labelled progress bar whose fill\n * percentage and colour reflect utilization. Optional warning / danger\n * thresholds recolour the bar (e.g. green under 80%, amber 80–100%, red over\n * 100%). Use it for budget burn, quota usage, pipeline coverage, SLA capacity,\n * etc. Editable in-app like every catalog widget (ADR-041).\n *\n * Resolved by its registry type key `gauge`; reference it from a manifest\n * placement `{ id, type: 'gauge', content: { label, source, target, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Pipeline coverage',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: { register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value', filter: { status: 'open' } },\n * target: { kind: 'static', value: 500000 },\n * thresholds: { warn: 80, danger: 100, invert: false },\n * }\n * ```\n */\nexport default {\n\tname: 'CnGaugeWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` is run through it, exactly as\n\t\t * CnStatWidget does. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location) or `link` (external href) turns the whole\n\t\t * tile into a click-through target (see the widgetLink mixin).\n\t\t *\n\t\t * @type {{label?: string, route?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, filter?: object}, target?: {kind?: ('static'|'aggregate'), value?: number, metric?: string, field?: string, filter?: object}, thresholds?: {warn?: number, danger?: number, invert?: boolean}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnGaugeWidget outside a CnAppRoot ancestor.\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\tdata() {\n\t\treturn {\n\t\t\tvalue: null,\n\t\t\ttarget: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\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 tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/** The unwrapped page-level app config map for `@config.*` resolution. */\n\t\tconfigCtx() {\n\t\t\treturn unwrapAppConfig(this.cnAppConfig)\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Utilization ratio (0–n) of value to target, or null. */\n\t\tratio() {\n\t\t\tconst t = Number(this.target)\n\t\t\tconst v = Number(this.value)\n\t\t\tif (!Number.isFinite(t) || t === 0 || !Number.isFinite(v)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn v / t\n\t\t},\n\n\t\t/** Percentage label, e.g. \"83%\". */\n\t\tpctLabel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\treturn Math.round(this.ratio * 100) + '%'\n\t\t},\n\n\t\t/** Bar fill width, clamped to 100%. */\n\t\tfillWidth() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '0%'\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, this.ratio * 100)) + '%'\n\t\t},\n\n\t\t/** Bar colour from the threshold bands. */\n\t\tbarColor() {\n\t\t\treturn this.level ? STATUS_FILL_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/** Percentage text colour, the readable text shade of the bar's band. */\n\t\tpctColor() {\n\t\t\treturn this.level ? STATUS_TEXT_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/**\n\t\t * The threshold band the value sits in (invert flips good/bad), or null\n\t\t * while there is no value.\n\t\t *\n\t\t * @return {'success'|'warning'|'error'|null}\n\t\t */\n\t\tlevel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst th = this.content.thresholds || {}\n\t\t\tconst pct = this.ratio * 100\n\t\t\tconst warn = Number.isFinite(th.warn) ? th.warn : 80\n\t\t\tconst danger = Number.isFinite(th.danger) ? th.danger : 100\n\t\t\tlet level\n\t\t\tif (pct >= danger) {\n\t\t\t\tlevel = 'danger'\n\t\t\t} else if (pct >= warn) {\n\t\t\t\tlevel = 'warn'\n\t\t\t} else {\n\t\t\t\tlevel = 'ok'\n\t\t\t}\n\t\t\tif (th.invert) {\n\t\t\t\tlevel = level === 'ok' ? 'danger' : level === 'danger' ? 'ok' : 'warn'\n\t\t\t}\n\t\t\tif (level === 'danger') {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'success'\n\t\t},\n\n\t\t/** The value, number-formatted per content.format. */\n\t\tformattedValue() {\n\t\t\treturn this.formatNumber(this.value)\n\t\t},\n\n\t\t/** The target, number-formatted per content.format. */\n\t\tformattedTarget() {\n\t\t\treturn this.formatNumber(this.target)\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify({ s: this.content.source || {}, t: this.content.target || {} })\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchAll()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAll()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {unknown} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, this.configCtx)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the value and resolve the target (static or aggregate).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchAll() {\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst tgt = this.content.target || {}\n\t\t\t\tconst valuePromise = fetchAggregateValue(s)\n\t\t\t\tconst targetPromise = (tgt.kind === 'aggregate')\n\t\t\t\t\t? fetchAggregateValue({ register: s.register, schema: s.schema, metric: tgt.metric, field: tgt.field, filter: tgt.filter || {} })\n\t\t\t\t\t: Promise.resolve(Number(tgt.value))\n\t\t\t\tconst [v, t] = await Promise.all([valuePromise, targetPromise])\n\t\t\t\tthis.value = v\n\t\t\t\tthis.target = t\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-gauge-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 10px 4px;\n}\n\n/* Whole-tile click target when the widget declares a `route`/`link`. */\n.cn-gauge-widget--linked {\n\tcursor: pointer;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius-large, 8px);\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-gauge-widget--linked:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-gauge-widget--linked:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-gauge-widget__head {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-gauge-widget__label {\n\tfont-size: 0.95em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__pct {\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n}\n\n.cn-gauge-widget__track {\n\twidth: 100%;\n\theight: 10px;\n\tborder-radius: 5px;\n\tbackground: var(--color-background-dark);\n\toverflow: hidden;\n}\n\n.cn-gauge-widget__fill {\n\theight: 100%;\n\tborder-radius: 5px;\n\ttransition: width 0.3s ease;\n}\n\n.cn-gauge-widget__foot {\n\tdisplay: flex;\n\talign-items: baseline;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-gauge-widget__value {\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__target {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementVNode","_createElementBlock","_toDisplayString","_Fragment","_normalizeStyle","_openBlock"],"mappings":";;AAcO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAuB,GAAA;;;AACN,EAAA,KAAK,EAAC,wBAAS;;;AAQtC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,wBAAA,GAAA;;;AAMK,EAAA,KAAA,EAAA,uBAAA;;;;;;;;uBApBVA,oCACK,IAAO,CAAA,OAAA,CAAA,EADZC;IAEC,KAAI,EAAA,CAAE,iBAAgB,EAAA,EAAA,yBAAA,EACc,IAAO,CAAA,QAAA,EAAA,CAAA;KACpC,IAAS,CAAA,SAAA,CAAA,EAAA;qBACf,MAOI;AAPJ,MAAAC,kBAAA,CAOI,OAPJ,UAOI,EAAA;AANO,QAAA,CAAA,MAAA,CAAA,OAAO,CAAE,KAAI;0BAAxBC,kBAAyC,CAAA,MAAA,EAAzC,UAAyC,EAAAC,eAAA,CAAA,QAAA,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;QAC1CF,kBAIG,CAAA,MAAA,EAAA;AAJG,UAAA,KAAA,EAAA,sBAAA;AAAA,UAAA,KAAA,0BAAA,QAAA,CAAA,QAAA,EAAA,CAAA;;WACF,KAAA,CAAA,OAAA;4BAAHF,WAAG,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;;;;;;8BAEHG,kBAAiC,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;kDAAb,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;AAI9B,MAAAH,kBAAA,CAIK,OAJL,UAIK,EAAA;0BADwD,CAAA,KAAA,EAAA;AAD3D,UAAA,KAAK,EAAC,uBAAuB;UAC5B,KAAK,EAAAI,cAAA,CAAA,EAAA,KAAA,EAAW,QAAS,CAAA,SAAA,EAAA,eAAA,EAAmB,QAAS,CAAA,QAAA,EAAA,CAAA;;;AAG9C,MAAA,CAAA,CAAA,KAAA,CAAA,OAAA,KAAA,KAAA,CAAA,KAAA;AAAV,WAAAC,SAAA,EAAA,EAAAJ,kBAAA,QAAA;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnGaugeWidget.vue3.js","sources":["../../../../src/components/CnGaugeWidget/CnGaugeWidget.vue?vue&type=template&id=006ab8f8&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<component\n\t\t:is=\"linkTag\"\n\t\tclass=\"cn-gauge-widget\"\n\t\t:class=\"{ 'cn-gauge-widget--linked': isLinked }\"\n\t\tv-bind=\"linkAttrs\">\n\t\t<div class=\"cn-gauge-widget__head\">\n\t\t\t<span v-if=\"content.label\" class=\"cn-gauge-widget__label\">{{ resolvedLabel }}</span>\n\t\t\t<span class=\"cn-gauge-widget__pct\" :style=\"{ color: pctColor }\">\n\t\t\t\t<NcLoadingIcon v-if=\"loading\" :size=\"16\" />\n\t\t\t\t<span v-else-if=\"error\" :title=\"error\">—</span>\n\t\t\t\t<template v-else>{{ pctLabel }}</template>\n\t\t\t</span>\n\t\t</div>\n\n\t\t<div class=\"cn-gauge-widget__track\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-gauge-widget__fill\"\n\t\t\t\t:style=\"{ width: fillWidth, backgroundColor: barColor }\" />\n\t\t</div>\n\n\t\t<div v-if=\"!loading && !error\" class=\"cn-gauge-widget__foot\">\n\t\t\t<span class=\"cn-gauge-widget__value\">{{ formattedValue }}</span>\n\t\t\t<span class=\"cn-gauge-widget__target\">/ {{ formattedTarget }}</span>\n\t\t</div>\n\t</component>\n</template>\n\n<script>\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport widgetLink from '../../mixins/widgetLink.js'\nimport { fetchAggregateValue } from '../../utils/fetchAggregate.js'\nimport { formatMetricValue, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { resolveFilterValue } from '../../utils/resolveFilterTokens.js'\nimport { STATUS_FILL_COLORS, STATUS_TEXT_COLORS } from '../../utils/statusColors.js'\n\n/**\n * CnGaugeWidget — an abstract utilization / progress-to-target gauge.\n *\n * Reads a `value` scalar from OpenRegister's aggregation `/value` endpoint and\n * compares it against a `target` (either a static number or a second\n * aggregate), rendering the ratio as a labelled progress bar whose fill\n * percentage and colour reflect utilization. Optional warning / danger\n * thresholds recolour the bar (e.g. green under 80%, amber 80–100%, red over\n * 100%). Use it for budget burn, quota usage, pipeline coverage, SLA capacity,\n * etc. Editable in-app like every catalog widget (ADR-041).\n *\n * Resolved by its registry type key `gauge`; reference it from a manifest\n * placement `{ id, type: 'gauge', content: { label, source, target, ... } }`.\n *\n * Example content blob:\n * ```js\n * content: {\n * label: 'Pipeline coverage',\n * format: { style: 'currency', currency: 'EUR', decimals: 0 },\n * source: { register: 'pipelinq', schema: 'lead', metric: 'sum', field: 'value', filter: { status: 'open' } },\n * target: { kind: 'static', value: 500000 },\n * thresholds: { warn: 80, danger: 100, invert: false },\n * }\n * ```\n */\nexport default {\n\tname: 'CnGaugeWidget',\n\n\tcomponents: {\n\t\tNcLoadingIcon,\n\t},\n\n\tmixins: [widgetLink],\n\n\tinject: {\n\t\t/**\n\t\t * Page-level app config map (reactive `{ <key>: value }`) provided by\n\t\t * CnDashboardPage / CnDetailPage. Resolves `@config.<key>` tokens in the\n\t\t * `format.currency` / `format.prefix` / `format.suffix` strings (e.g. the\n\t\t * reporting currency captured by the setup wizard). Defaults to `{}`.\n\t\t */\n\t\tcnAppConfig: { default: () => ({}) },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.label` is run through it, exactly as\n\t\t * CnStatWidget does. Defaults to an identity function so an\n\t\t * untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. An optional `route`\n\t\t * (vue-router location) or `link` (external href) turns the whole\n\t\t * tile into a click-through target (see the widgetLink mixin).\n\t\t *\n\t\t * @type {{label?: string, route?: (object|string), link?: string, format?: {style?: string, currency?: string, decimals?: number, prefix?: string, suffix?: string}, source?: {register?: string, schema?: string, metric?: string, field?: string, filter?: object}, target?: {kind?: ('static'|'aggregate'), value?: number, metric?: string, field?: string, filter?: object}, thresholds?: {warn?: number, danger?: number, invert?: boolean}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`\n\t\t * (itself an identity function by default). Provide explicitly when\n\t\t * mounting CnGaugeWidget outside a CnAppRoot ancestor.\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\tdata() {\n\t\treturn {\n\t\t\tvalue: null,\n\t\t\ttarget: null,\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\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 tile label, run through the host translate function so a\n\t\t * manifest-authored source string localises to the user's language.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedLabel() {\n\t\t\tconst label = this.content.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/** The unwrapped page-level app config map for `@config.*` resolution. */\n\t\tconfigCtx() {\n\t\t\treturn unwrapAppConfig(this.cnAppConfig)\n\t\t},\n\n\t\t/**\n\t\t * The `content.format` spec with its `currency` / `prefix` / `suffix`\n\t\t * `@config.<key>` tokens resolved against the page-level app config. A\n\t\t * required token that stays unset is dropped so the downstream default\n\t\t * applies (EUR for currency, empty for prefix/suffix) rather than being\n\t\t * passed to `Intl.NumberFormat` as a literal `@config.…` (which throws).\n\t\t *\n\t\t * @return {object} The resolved format spec.\n\t\t */\n\t\tresolvedFormat() {\n\t\t\tconst fmt = this.content.format || {}\n\t\t\tconst ctx = { config: this.configCtx }\n\t\t\tconst out = { ...fmt }\n\t\t\tfor (const key of ['currency', 'prefix', 'suffix']) {\n\t\t\t\tconst raw = fmt[key]\n\t\t\t\tif (typeof raw !== 'string' || raw.charAt(0) !== '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst resolved = resolveFilterValue(raw, ctx)\n\t\t\t\tout[key] = (typeof resolved === 'string' && resolved.charAt(0) === '@') ? undefined : resolved\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/** Utilization ratio (0–n) of value to target, or null. */\n\t\tratio() {\n\t\t\tconst t = Number(this.target)\n\t\t\tconst v = Number(this.value)\n\t\t\tif (!Number.isFinite(t) || t === 0 || !Number.isFinite(v)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn v / t\n\t\t},\n\n\t\t/** Percentage label, e.g. \"83%\". */\n\t\tpctLabel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\treturn Math.round(this.ratio * 100) + '%'\n\t\t},\n\n\t\t/** Bar fill width, clamped to 100%. */\n\t\tfillWidth() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn '0%'\n\t\t\t}\n\t\t\treturn Math.min(100, Math.max(0, this.ratio * 100)) + '%'\n\t\t},\n\n\t\t/** Bar colour from the threshold bands. */\n\t\tbarColor() {\n\t\t\treturn this.level ? STATUS_FILL_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/** Percentage text colour, the readable text shade of the bar's band. */\n\t\tpctColor() {\n\t\t\treturn this.level ? STATUS_TEXT_COLORS[this.level] : 'var(--color-primary-element)'\n\t\t},\n\n\t\t/**\n\t\t * The threshold band the value sits in (invert flips good/bad), or null\n\t\t * while there is no value.\n\t\t *\n\t\t * @return {'success'|'warning'|'error'|null}\n\t\t */\n\t\tlevel() {\n\t\t\tif (this.ratio === null) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst th = this.content.thresholds || {}\n\t\t\tconst pct = this.ratio * 100\n\t\t\tconst warn = Number.isFinite(th.warn) ? th.warn : 80\n\t\t\tconst danger = Number.isFinite(th.danger) ? th.danger : 100\n\t\t\tlet level\n\t\t\tif (pct >= danger) {\n\t\t\t\tlevel = 'danger'\n\t\t\t} else if (pct >= warn) {\n\t\t\t\tlevel = 'warn'\n\t\t\t} else {\n\t\t\t\tlevel = 'ok'\n\t\t\t}\n\t\t\tif (th.invert) {\n\t\t\t\tlevel = level === 'ok' ? 'danger' : level === 'danger' ? 'ok' : 'warn'\n\t\t\t}\n\t\t\tif (level === 'danger') {\n\t\t\t\treturn 'error'\n\t\t\t}\n\t\t\tif (level === 'warn') {\n\t\t\t\treturn 'warning'\n\t\t\t}\n\t\t\treturn 'success'\n\t\t},\n\n\t\t/** The value, number-formatted per content.format. */\n\t\tformattedValue() {\n\t\t\treturn this.formatNumber(this.value)\n\t\t},\n\n\t\t/** The target, number-formatted per content.format. */\n\t\tformattedTarget() {\n\t\t\treturn this.formatNumber(this.target)\n\t\t},\n\n\t\t/** Stable signature so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\treturn JSON.stringify({ s: this.content.source || {}, t: this.content.target || {} })\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey() {\n\t\t\tthis.fetchAll()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.fetchAll()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Format a number per the content.format spec (number/currency/percent).\n\t\t *\n\t\t * @param {unknown} value The raw value.\n\t\t * @return {string} The formatted string.\n\t\t */\n\t\tformatNumber(value) {\n\t\t\treturn formatMetricValue(value, this.resolvedFormat, this.configCtx, null, this.effectiveTranslate)\n\t\t},\n\n\t\t/**\n\t\t * Fetch the value and resolve the target (static or aggregate).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchAll() {\n\t\t\tconst s = this.content.source || {}\n\t\t\tif (!s.register || !s.schema) {\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst tgt = this.content.target || {}\n\t\t\t\tconst valuePromise = fetchAggregateValue(s)\n\t\t\t\tconst targetPromise = (tgt.kind === 'aggregate')\n\t\t\t\t\t? fetchAggregateValue({ register: s.register, schema: s.schema, metric: tgt.metric, field: tgt.field, filter: tgt.filter || {} })\n\t\t\t\t\t: Promise.resolve(Number(tgt.value))\n\t\t\t\tconst [v, t] = await Promise.all([valuePromise, targetPromise])\n\t\t\t\tthis.value = v\n\t\t\t\tthis.target = t\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.value = null\n\t\t\t\tthis.target = null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-gauge-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tpadding: 10px 4px;\n}\n\n/* Whole-tile click target when the widget declares a `route`/`link`. */\n.cn-gauge-widget--linked {\n\tcursor: pointer;\n\ttext-decoration: none;\n\tcolor: inherit;\n\tborder-radius: var(--border-radius-large, 8px);\n\ttransition: background-color 0.1s ease-in-out;\n}\n\n.cn-gauge-widget--linked:hover {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-gauge-widget--linked:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-gauge-widget__head {\n\tdisplay: flex;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px;\n}\n\n.cn-gauge-widget__label {\n\tfont-size: 0.95em;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__pct {\n\tfont-size: 1.2em;\n\tfont-weight: 700;\n}\n\n.cn-gauge-widget__track {\n\twidth: 100%;\n\theight: 10px;\n\tborder-radius: 5px;\n\tbackground: var(--color-background-dark);\n\toverflow: hidden;\n}\n\n.cn-gauge-widget__fill {\n\theight: 100%;\n\tborder-radius: 5px;\n\ttransition: width 0.3s ease;\n}\n\n.cn-gauge-widget__foot {\n\tdisplay: flex;\n\talign-items: baseline;\n\tgap: 6px;\n\tfont-size: 0.9em;\n}\n\n.cn-gauge-widget__value {\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-gauge-widget__target {\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_createBlock","_mergeProps","_createElementVNode","_createElementBlock","_toDisplayString","_Fragment","_normalizeStyle","_openBlock"],"mappings":";;AAcO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAuB,GAAA;;;AACN,EAAA,KAAK,EAAC,wBAAS;;;AAQtC,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,wBAAA,GAAA;;;AAMK,EAAA,KAAA,EAAA,uBAAA;;;;;;;;uBApBVA,oCACK,IAAO,CAAA,OAAA,CAAA,EADZC;IAEC,KAAI,EAAA,CAAE,iBAAgB,EAAA,EAAA,yBAAA,EACc,IAAO,CAAA,QAAA,EAAA,CAAA;KACpC,IAAS,CAAA,SAAA,CAAA,EAAA;qBACf,MAOI;AAPJ,MAAAC,kBAAA,CAOI,OAPJ,UAOI,EAAA;AANO,QAAA,CAAA,MAAA,CAAA,OAAO,CAAE,KAAI;0BAAxBC,kBAAyC,CAAA,MAAA,EAAzC,UAAyC,EAAAC,eAAA,CAAA,QAAA,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;QAC1CF,kBAIG,CAAA,MAAA,EAAA;AAJG,UAAA,KAAA,EAAA,sBAAA;AAAA,UAAA,KAAA,0BAAA,QAAA,CAAA,QAAA,EAAA,CAAA;;WACF,KAAA,CAAA,OAAA;4BAAHF,WAAG,CAAA,wBAAA,EAAA;;AAAA,gBAAA,IAAA,EAAA,EAAA;;;;;;;8BAEHG,kBAAiC,CAAAE,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;kDAAb,QAAQ,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;AAI9B,MAAAH,kBAAA,CAIK,OAJL,UAIK,EAAA;0BADwD,CAAA,KAAA,EAAA;AAD3D,UAAA,KAAK,EAAC,uBAAuB;UAC5B,KAAK,EAAAI,cAAA,CAAA,EAAA,KAAA,EAAW,QAAS,CAAA,SAAA,EAAA,eAAA,EAAmB,QAAS,CAAA,QAAA,EAAA,CAAA;;;AAG9C,MAAA,CAAA,CAAA,KAAA,CAAA,OAAA,KAAA,KAAA,CAAA,KAAA;AAAV,WAAAC,SAAA,EAAA,EAAAJ,kBAAA,QAAA;;;;;;;;;;;;"}
@@ -132,7 +132,7 @@ var script = {
132
132
  /** Channel options `{ value, label }`. */
133
133
  channelOptions() {
134
134
  return Array.isArray(this.content.channels) && this.content.channels.length > 0
135
- ? this.content.channels
135
+ ? this.content.channels.map((channel) => ({ ...channel, label: this.copy(channel && channel.label) }))
136
136
  : [
137
137
  { value: 'telefoon', label: translate('nextcloud-vue', 'Phone') },
138
138
  { value: 'email', label: translate('nextcloud-vue', 'Email') },
@@ -153,10 +153,11 @@ var script = {
153
153
  const prop = properties[this.content.outcomeField || 'outcome'];
154
154
  if (prop && Array.isArray(prop.enum) && prop.enum.length > 0) {
155
155
  const labels = prop['x-enum-labels'] || prop.enumLabels || {};
156
- const tr = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k;
157
- return prop.enum.map((value) => ({ value, label: tr(labels[value] || String(value)) }))
156
+ return prop.enum.map((value) => ({ value, label: this.copy(labels[value] || String(value)) }))
158
157
  }
159
- return Array.isArray(this.content.outcomes) ? this.content.outcomes : []
158
+ return Array.isArray(this.content.outcomes)
159
+ ? this.content.outcomes.map((outcome) => ({ ...outcome, label: this.copy(outcome && outcome.label) }))
160
+ : []
160
161
  },
161
162
 
162
163
  /**
@@ -229,8 +230,7 @@ var script = {
229
230
  // `content.submitLabel` names what the form creates, e.g. "Save contact moment".
230
231
  registerLabel() {
231
232
  if (this.content.submitLabel) {
232
- const tr = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k;
233
- return tr(this.content.submitLabel)
233
+ return this.copy(this.content.submitLabel)
234
234
  }
235
235
  return translate('nextcloud-vue', 'Register')
236
236
  },
@@ -308,6 +308,29 @@ var script = {
308
308
  },
309
309
 
310
310
  /** The default channel value (first configured channel, else `telefoon`). */
311
+ /**
312
+ * A label the manifest supplies (a channel, an outcome, the submit
313
+ * label), translated: the app's own translation first, since the words
314
+ * are the app's, then the library's catalogue, then the text as
315
+ * written. Before, a channel label was shown as written and the submit
316
+ * label only reached the app's translate, so pipelinq showed "Phone"
317
+ * and "Save contact moment" in English to a Dutch reader.
318
+ *
319
+ * @spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language
320
+ * @param {string} key The label as the manifest writes it.
321
+ * @return {string} The translated label.
322
+ */
323
+ copy(key) {
324
+ if (typeof key !== 'string' || key === '') {
325
+ return key
326
+ }
327
+ const host = typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : key;
328
+ if (typeof host === 'string' && host !== '' && host !== key) {
329
+ return host
330
+ }
331
+ return translate('nextcloud-vue', key)
332
+ },
333
+
311
334
  firstChannel() {
312
335
  const ch = (this.content && this.content.channels) || [];
313
336
  return (Array.isArray(ch) && ch.length > 0 && ch[0].value) || 'telefoon'