@conduction/nextcloud-vue 2.83.0 → 2.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +38 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +78 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1030 -533
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +41 -4
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17159 -13422
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1030 -533
- package/l10n/en.json +38 -1
- package/l10n/nl.json +78 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +3 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +108 -7
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +137 -6
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +40 -3
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -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-
|
|
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":"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"}
|
|
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;;;;;;;;;;;;"}
|