@conduction/nextcloud-vue 2.56.0-beta.47 → 2.56.0-beta.49
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/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +46 -28
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +17 -10
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandBar/CnBrandBar.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +91 -36
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +115 -72
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +32 -3
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +56 -16
- 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/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +9 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js +240 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js +65 -0
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js +30 -0
- package/dist/esm/components/CnDeviceFramesWidget/index.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +17 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +30 -31
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js +6 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue.js.map +1 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js +90 -0
- package/dist/esm/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
- package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
- package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
- package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
- package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +51 -4
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +20 -2
- 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 +230 -25
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +447 -221
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
- package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
- package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +20 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/indexSources.js +9 -0
- package/dist/esm/composables/indexSources.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 +18 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +18 -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 +712 -330
- package/dist/esm/utils/dueText.js +49 -8
- package/dist/esm/utils/dueText.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.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/nextcloud-vue.cjs.js +8275 -6302
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +712 -330
- package/l10n/en.json +18 -1
- package/l10n/nl.json +18 -1
- package/package.json +1 -1
- package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +20 -0
- package/src/components/CnBrandBar/CnBrandBar.vue +5 -4
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +96 -2
- package/src/components/CnDataTable/CnDataTable.md +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +39 -10
- package/src/components/CnDetailPage/CnDetailPage.vue +55 -15
- package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +360 -0
- package/src/components/CnDeviceFramesWidget/index.js +29 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +46 -2
- package/src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue +107 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
- package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
- package/src/components/CnPageHeader/CnPageHeader.md +11 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
- package/src/components/CnPagination/CnPagination.vue +10 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +84 -4
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +10 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +511 -28
- package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
- package/src/components/CnTabs/CnTabs.vue +39 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +31 -2
- package/src/components/index.js +1 -0
- package/src/composables/indexSources.js +9 -0
- package/src/css/dialog.css +14 -0
- package/src/css/look-board-index.css +27 -1
- package/src/css/look-board.css +27 -0
- package/src/css/page-header.css +16 -0
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +27 -15
- package/src/utils/dueText.js +49 -8
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnFactsWidget.vue3.js","sources":["../../../../src/components/CnFactsWidget/CnFactsWidget.vue?vue&type=template&id=e1992d24&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-facts-widget\" data-testid=\"cn-facts-widget\">\n\t\t<p v-if=\"error\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-error\">\n\t\t\t{{ tr('Could not load this information') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && !record\" class=\"cn-facts-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t</div>\n\t\t<p v-else-if=\"blocks.length === 0\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<template v-for=\"block in blocks\" :key=\"block.key\">\n\t\t\t\t<dl\n\t\t\t\t\tv-if=\"block.kind === 'rows'\"\n\t\t\t\t\tclass=\"cn-facts-widget__rows\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in block.rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__row\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${row.key}`\">\n\t\t\t\t\t\t<dt class=\"cn-facts-widget__label\" :class=\"{ 'cn-facts-widget__label--icon': row.icon }\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"row.icon\"\n\t\t\t\t\t\t\t\t:name=\"row.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t\t<span :class=\"{ 'hidden-visually': row.icon }\">{{ row.label }}</span>\n\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t<dd class=\"cn-facts-widget__value\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\tv-if=\"row.href\"\n\t\t\t\t\t\t\t\t:href=\"row.href\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\"\n\t\t\t\t\t\t\t\t:target=\"row.external ? '_blank' : undefined\"\n\t\t\t\t\t\t\t\t:rel=\"row.external ? 'noopener noreferrer' : undefined\">{{ row.value }}</a>\n\t\t\t\t\t\t\t<span v-else>{{ row.masked && !revealed[row.key] ? row.masked : row.value }}</span>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.reveal && !revealed[row.key]\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"revealLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"reveal(row)\">\n\t\t\t\t\t\t\t\t<EyeOutline :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t{{ showLabel }}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.copy\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__icon-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:title=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-copy-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"copy(row)\">\n\t\t\t\t\t\t\t\t<Check v-if=\"copied === row.key\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t<ContentCopy v-else :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t<component\n\t\t\t\t\t:is=\"block.to ? 'router-link' : 'strong'\"\n\t\t\t\t\tv-else-if=\"block.kind === 'title'\"\n\t\t\t\t\t:to=\"block.to || undefined\"\n\t\t\t\t\tclass=\"cn-facts-widget__title\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</component>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'line'\"\n\t\t\t\t\tclass=\"cn-facts-widget__line\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'text'\"\n\t\t\t\t\tclass=\"cn-facts-widget__text\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'lead'\"\n\t\t\t\t\tclass=\"cn-facts-widget__lead\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-facts-widget__lead-tile\"\n\t\t\t\t\t\t:class=\"`cn-facts-widget__lead-tile--${block.tone}`\"\n\t\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon v-if=\"block.icon\" :name=\"block.icon\" :size=\"20\" />\n\t\t\t\t\t\t<template v-else>{{ block.initials }}</template>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__lead-text\">\n\t\t\t\t\t\t<strong class=\"cn-facts-widget__lead-title\">{{ block.value }}</strong>\n\t\t\t\t\t\t<span v-if=\"block.subtitle\" class=\"cn-facts-widget__lead-subtitle\">{{ block.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pill'\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t:label=\"block.value\"\n\t\t\t\t\t\t:variant=\"block.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t{{ link.label }}\n\t\t</component>\n\t\t<button\n\t\t\tv-if=\"action && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\tdata-testid=\"cn-facts-action\"\n\t\t\t@click=\"runAction(content.action)\">\n\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t{{ action.label }}\n\t\t</button>\n\t</div>\n</template>\n\n<script>\nimport { emit } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport EyeOutline from 'vue-material-design-icons/EyeOutline.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useRefLabels } from '../../composables/useRefLabels.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { formatMoney, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text'])\n\n/**\n * The label resolver every facts widget on the page shares, made on first use\n * (the object store needs an active pinia). One batched request per register and\n * schema for the ids it has not seen; a label it has read stays until the page\n * reloads, and an object the store already holds is not fetched at all.\n *\n * @type {{resolve: Function}|null}\n */\nlet refLabels = null\n\n/**\n * The entry that reads a resolved pill's tone: the same reference, with the\n * referenced object's `toneField` as the field to read.\n *\n * @param {object} spec A facts entry.\n * @return {object|null} `{ path, itemPath, resolve }`, or null when no `toneField` is set.\n */\nfunction toneSpec(spec) {\n\tconst resolve = spec && spec.resolve\n\tif (!resolve || typeof resolve !== 'object' || typeof resolve.toneField !== 'string' || resolve.toneField === '') {\n\t\treturn null\n\t}\n\treturn { path: spec.path, itemPath: spec.itemPath, resolve: { register: resolve.register, schema: resolve.schema, labelField: resolve.toneField } }\n}\n\n/** The page-wide reference label resolver. */\nfunction sharedRefLabels() {\n\tif (!refLabels) {\n\t\trefLabels = useRefLabels()\n\t}\n\treturn refLabels\n}\n\n/**\n * Key of a resolved reference in the widget's `labels` data.\n *\n * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.\n * @param {string} id The referenced object's id.\n * @return {string} The key.\n */\nfunction refKey(resolve, id) {\n\treturn `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`\n}\n\n/** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */\nexport const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'\n\n/**\n * CnFactsWidget — the `facts` widget: a card of facts about ONE object.\n *\n * Renders the configured fields of the current detail-page object, or of one\n * other object named by `content.source`, as the side cards on the screens\n * draw them: label/value rows (a two-column grid, label muted), a bold title\n * line that may link, muted meta lines that join several fields with \" · \",\n * a status pill, and one link at the foot.\n *\n * Everything is configuration; nothing in it knows an app:\n *\n * ```json\n * {\n * \"type\": \"facts\",\n * \"title\": \"Zaak\",\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"id\": \"@object.case\" },\n * \"fields\": [\n * { \"as\": \"title\", \"path\": \"title\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * { \"as\": \"line\", \"parts\": [\"identifier\", \"caseTypeTitle\"] },\n * { \"as\": \"pill\", \"path\": \"status\", \"map\": { \"open\": \"In behandeling\" } },\n * { \"path\": \"deadline\", \"label\": \"Termijn\", \"format\": \"date\" }\n * ],\n * \"link\": { \"label\": \"Naar de zaak\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * \"emptyText\": \"Geen zaak\"\n * }\n * }\n * ```\n *\n * Tokens: `source.id` takes the shared filter tokens; `source.url` takes them\n * inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)\n * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes\n * and the link also take `@source.<field>` (a field of the object shown).\n * A source whose id token does not resolve renders the empty text, never a\n * request for the literal token.\n *\n * Labels, map values, the link label and the empty text go through the host\n * app's translate (`cnTranslate`).\n */\nexport default {\n\tname: 'CnFactsWidget',\n\n\tcomponents: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The page's app config; its `currency` is the reporting currency a money value falls back to. */\n\t\tcnAppConfig: { default: null },\n\t\t/** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `fields` — the entries, in order. Each is `{ path, label?, as?, from?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, colorMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.\n\t\t * `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`) or `pill`.\n\t\t * A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.\n\t\t * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.\n\t\t * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.\n\t\t * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by \", \", each item read at `itemPath`) or `currency` (a money amount in the reader's language, \"€ 1.234,50\": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).\n\t\t * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default \" · \"); empty parts are dropped.\n\t\t * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `link` — `{ label, route? | href? }` at the foot.\n\t\t * - `hideEmpty` — drop an entry whose value is empty (default true).\n\t\t * - `emptyText` — shown when no entry has a value.\n\t\t *\n\t\t * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}\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 * The current detail-page object, passed by the detail page host.\n\t\t * Read when `content.source` is not set.\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current detail-page object id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header', 'reveal'],\n\n\tdata() {\n\t\treturn {\n\t\t\tfetched: null,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Keys of the masked rows the reader chose to show. */\n\t\t\trevealed: {},\n\t\t\t/** Key of the row whose value was just copied. */\n\t\t\tcopied: '',\n\t\t\t/** Labels of the referenced objects read so far, by `refKey`. */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The token context: the detail object (prop, else the injected context)\n\t\t * and the page workspace.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The source declaration with its tokens resolved, or null for the current object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedSource() {\n\t\t\tconst source = this.content?.source\n\t\t\tif (!source || typeof source !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof source.url === 'string' && source.url !== '') {\n\t\t\t\t// The endpoint grammar: tokens INSIDE the url, and a request\n\t\t\t\t// that is blocked while one leaves an empty path segment.\n\t\t\t\tconst request = resolveEndpointRequest({ url: source.url }, this.tokenCtx)\n\t\t\t\treturn { url: request.blocked ? '' : request.url }\n\t\t\t}\n\t\t\treturn resolveDeepTokens(source, this.tokenCtx)\n\t\t},\n\n\t\t/**\n\t\t * A stable key of the resolved source, so the widget refetches only when it changes.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The object the facts are read from.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecord() {\n\t\t\treturn this.resolvedSource ? this.fetched : this.objectData\n\t\t},\n\n\t\t/**\n\t\t * The entries to draw, consecutive rows grouped into one list.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tif (!sourceRecord) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst hideEmpty = this.content?.hideEmpty !== false\n\t\t\tconst out = []\n\t\t\tfields.forEach((field, index) => {\n\t\t\t\tif (!field || typeof field !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// `from: \"object\"` reads the page's own object while the card shows a `source`.\n\t\t\t\tconst record = field.from === 'object' && this.objectData ? this.objectData : sourceRecord\n\t\t\t\tconst kind = this.kindOf(field)\n\t\t\t\tconst key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\\w-]/g, '-')\n\t\t\t\tconst value = this.entryValue(record, field, kind)\n\t\t\t\tif (value === '' && (hideEmpty || kind === 'lead')) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (kind === 'row') {\n\t\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\t\tconst row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tout.push({ kind: 'rows', key: `rows-${index}`, rows: [row] })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst block = { kind, key, value }\n\t\t\t\tif (kind === 'lead') {\n\t\t\t\t\tblock.subtitle = this.partsValue(record, field.subtitle)\n\t\t\t\t\tblock.icon = typeof field.icon === 'string' ? field.icon : ''\n\t\t\t\t\tblock.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath))\n\t\t\t\t\tblock.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral'\n\t\t\t\t}\n\t\t\t\tif (kind === 'title' && field.route) {\n\t\t\t\t\tblock.to = this.resolveRoute(field.route, record)\n\t\t\t\t}\n\t\t\t\tif (kind === 'pill') {\n\t\t\t\t\tconst raw = readPath(record, field.path)\n\t\t\t\t\tconst colours = field.variantMap || field.colorMap\n\t\t\t\t\t// A resolved pill looks its tone up by the referenced object's tone field,\n\t\t\t\t\t// then by the resolved label, then by the stored value.\n\t\t\t\t\tconst keys = field.resolve && typeof field.resolve === 'object'\n\t\t\t\t\t\t? [toneSpec(field) ? this.refLabels(raw, toneSpec(field))[0] : undefined, this.refLabels(raw, field)[0], raw]\n\t\t\t\t\t\t: [raw]\n\t\t\t\t\tconst hit = colours ? keys.find((candidate) => (typeof candidate === 'string' || typeof candidate === 'number') && Object.hasOwn(colours, candidate)) : undefined\n\t\t\t\t\tconst variant = hit !== undefined ? colours[hit] : field.variant\n\t\t\t\t\tblock.variant = isBadgeVariant(variant) ? variant : 'default'\n\t\t\t\t}\n\t\t\t\tout.push(block)\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route) {\n\t\t\t\tconst to = this.resolveRoute(link.route, this.record)\n\t\t\t\treturn to ? { label: this.tr(link.label), to } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\tconst href = this.resolveTemplate(link.href, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The action under the facts, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\taction() {\n\t\t\treturn this.actionView(this.content?.action)\n\t\t},\n\n\t\t/**\n\t\t * The header action, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\theaderAction() {\n\t\t\treturn this.actionView(this.content?.headerAction)\n\t\t},\n\n\t\t/** @return {string} The reveal button's text. */\n\t\tshowLabel() {\n\t\t\treturn t('nextcloud-vue', 'Show')\n\t\t},\n\n\t\t/**\n\t\t * The empty line.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content?.emptyText || 'Nothing to show yet')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\t\t},\n\n\t\trecord: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t},\n\t\t},\n\n\t\theaderAction: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\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 kind of a field entry.\n\t\t *\n\t\t * @param {object} field The entry.\n\t\t * @return {string} `row`, `title`, `line` or `pill`.\n\t\t */\n\t\tkindOf(field) {\n\t\t\tif (KINDS.includes(field.as)) {\n\t\t\t\treturn field.as\n\t\t\t}\n\t\t\treturn field.label || field.icon ? 'row' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * The display text of an entry of any kind: a lead's title, the parts of\n\t\t * a line or text, a constant `text`, else the value at `path`.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} kind The entry's kind.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\tentryValue(record, field, kind) {\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {\n\t\t\t\treturn this.lineValue(record, field)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {\n\t\t\t\treturn this.tr(field.text)\n\t\t\t}\n\t\t\treturn this.valueOf(record, field)\n\t\t},\n\n\t\t/**\n\t\t * A lead's title or subtitle: a path, a part spec, or an array of parts\n\t\t * joined as a line joins them.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string|object|Array} spec The declaration.\n\t\t * @return {string}\n\t\t */\n\t\tpartsValue(record, spec) {\n\t\t\tif (typeof spec === 'string' && spec !== '') {\n\t\t\t\treturn this.valueOf(record, { path: spec })\n\t\t\t}\n\t\t\tif (Array.isArray(spec)) {\n\t\t\t\treturn this.lineValue(record, { parts: spec })\n\t\t\t}\n\t\t\tif (spec && typeof spec === 'object') {\n\t\t\t\treturn this.lineValue(record, { parts: [spec] })\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The initials of a name: \"Sanne de Vries\" reads \"SV\".\n\t\t *\n\t\t * @param {unknown} name The name.\n\t\t * @return {string}\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tif (typeof name !== 'string' || name.trim() === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst words = name.trim().split(/\\s+/).filter((word) => /^\\p{Lu}/u.test(word))\n\t\t\tconst picked = words.length ? words : name.trim().split(/\\s+/)\n\t\t\tconst first = picked[0].charAt(0)\n\t\t\tconst last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * What a row adds to its value: an icon, a link for the email, phone\n\t\t * and url formats, the masked text, and the copy and reveal buttons.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} value The display text.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trowExtras(record, field, value) {\n\t\t\tconst extras = {\n\t\t\t\ticon: typeof field.icon === 'string' ? field.icon : '',\n\t\t\t\tpath: field.path || '',\n\t\t\t\traw: readPath(record, field.path),\n\t\t\t\tcopy: field.copy === true && value !== '',\n\t\t\t\treveal: false,\n\t\t\t\tmasked: '',\n\t\t\t\thref: '',\n\t\t\t\texternal: false,\n\t\t\t}\n\t\t\tif (value === '') {\n\t\t\t\treturn extras\n\t\t\t}\n\t\t\tif (field.format === 'email') {\n\t\t\t\textras.href = `mailto:${value}`\n\t\t\t} else if (field.format === 'phone') {\n\t\t\t\textras.href = `tel:${value.replace(/[^\\d+]/g, '')}`\n\t\t\t} else if (field.format === 'url') {\n\t\t\t\textras.href = /^https?:\\/\\//i.test(value) ? value : `https://${value}`\n\t\t\t\textras.external = true\n\t\t\t} else if (field.format === 'masked') {\n\t\t\t\textras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value\n\t\t\t\textras.reveal = field.reveal === true && value.length > 4\n\t\t\t}\n\t\t\treturn extras\n\t\t},\n\n\t\t/**\n\t\t * Show a masked value, and say so: the `reveal` event and the event-bus\n\t\t * channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\treveal(row) {\n\t\t\tthis.revealed = { ...this.revealed, [row.key]: true }\n\t\t\tconst detail = { path: row.path }\n\t\t\t/**\n\t\t\t * @event reveal Emitted when the reader shows a masked value.\n\t\t\t * @type {{path: string}} The path of the shown field.\n\t\t\t */\n\t\t\tthis.$emit('reveal', detail)\n\t\t\temit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })\n\t\t},\n\n\t\t/**\n\t\t * Copy a row's value to the clipboard.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync copy(row) {\n\t\t\tconst text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw)\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(text)\n\t\t\t\tthis.copied = row.key\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (this.copied === row.key) {\n\t\t\t\t\t\tthis.copied = ''\n\t\t\t\t\t}\n\t\t\t\t}, 2000)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] could not copy:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The copy button's accessible name.\n\t\t */\n\t\tcopyLabel(row) {\n\t\t\treturn this.copied === row.key\n\t\t\t\t? t('nextcloud-vue', 'Copied')\n\t\t\t\t: t('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The reveal button's accessible name.\n\t\t */\n\t\trevealLabel(row) {\n\t\t\treturn t('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()\n\t\t},\n\n\t\t/**\n\t\t * The drawn part of an action declaration.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {{label: string, icon: string}|null}\n\t\t */\n\t\tactionView(action) {\n\t\t\tif (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch an action with the record's tokens resolved (`@objectId`,\n\t\t * `@object.<field>`), through the page's dispatcher when there is one.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trunAction(action) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null })\n\t\t},\n\n\t\t/** Tell the card around the facts what its header shows (the header action). */\n\t\temitHeader() {\n\t\t\tconst view = this.headerAction\n\t\t\t/**\n\t\t\t * @event header Emitted with the card header's parts: the header action, if any.\n\t\t\t * @type {{link: null, note: string, action: (object|null)}} `action` is `{ label, icon, run }`.\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\tlink: null,\n\t\t\t\tnote: '',\n\t\t\t\taction: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The text of a reference: the labels of the objects its id(s) name, joined\n\t\t * by \", \". An id whose label is not known yet (or cannot be read) adds nothing.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).\n\t\t * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.\n\t\t * @return {string}\n\t\t */\n\t\trefText(raw, spec) {\n\t\t\treturn this.refLabels(raw, spec)\n\t\t\t\t.map((label) => (spec.map && Object.hasOwn(spec.map, label) ? this.tr(spec.map[label]) : label))\n\t\t\t\t.join(', ')\n\t\t},\n\n\t\t/**\n\t\t * The labels of a reference as read, before `map` is applied: the key a\n\t\t * pill's `variantMap` and `colorMap` look up.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids.\n\t\t * @param {object} spec `{ resolve, itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefLabels(raw, spec) {\n\t\t\treturn this.refIds(raw, spec)\n\t\t\t\t.map((id) => this.labels[refKey(spec.resolve, id)])\n\t\t\t\t.filter((label) => typeof label === 'string' && label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The ids a reference value holds.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefIds(raw, spec) {\n\t\t\treturn (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t.map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')\n\t\t\t\t.map(String)\n\t\t},\n\n\t\t/**\n\t\t * Every entry or part that carries `resolve`, with the record's value at its path.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @return {Array<{spec: object, raw: unknown}>}\n\t\t */\n\t\trefSpecs(record) {\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst specs = []\n\t\t\tconst visit = (spec) => {\n\t\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (spec.resolve && typeof spec.resolve === 'object' && spec.path) {\n\t\t\t\t\tspecs.push({ spec, raw: readPath(record, spec.path) })\n\t\t\t\t\tif (toneSpec(spec)) {\n\t\t\t\t\t\tspecs.push({ spec: toneSpec(spec), raw: readPath(record, spec.path) })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]\n\t\t\t\tnested.forEach(visit)\n\t\t\t}\n\t\t\tfields.forEach(visit)\n\t\t\treturn specs\n\t\t},\n\n\t\t/**\n\t\t * Read the labels of the referenced objects, one batched request per\n\t\t * register and schema, and keep them in `labels`.\n\t\t *\n\t\t * @param {object|null} record The object shown (defaults to the current record).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLabels(record = this.record) {\n\t\t\tif (!record) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst groups = new Map()\n\t\t\tfor (const { spec, raw } of this.refSpecs(record)) {\n\t\t\t\tconst { register, schema, labelField } = spec.resolve\n\t\t\t\tif (!register || !schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst groupKey = `${register}/${schema}/${labelField || ''}`\n\t\t\t\tconst group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }\n\t\t\t\tthis.refIds(raw, spec).forEach((id) => group.ids.add(id))\n\t\t\t\tgroups.set(groupKey, group)\n\t\t\t}\n\t\t\tif (groups.size === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolver = sharedRefLabels()\n\t\t\tconst next = { ...this.labels }\n\t\t\tawait Promise.all([...groups.values()].map(async ({ resolve, ids }) => {\n\t\t\t\tconst found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)\n\t\t\t\tObject.entries(found).forEach(([id, label]) => {\n\t\t\t\t\tnext[refKey(resolve, id)] = label\n\t\t\t\t})\n\t\t\t}))\n\t\t\tthis.labels = next\n\t\t},\n\n\t\t/**\n\t\t * Read the source object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst source = this.resolvedSource\n\t\t\tthis.error = false\n\t\t\tif (!source) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst url = this.sourceUrl(source)\n\t\t\tif (!url) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\t\tconst response = await axios.get(full)\n\t\t\t\tconst data = response && response.data\n\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] the source could not be read:', e?.message || e)\n\t\t\t\tthis.fetched = null\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The URL of a resolved source, or '' when it cannot be read yet.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {string}\n\t\t */\n\t\tsourceUrl(source) {\n\t\t\tif (typeof source.url === 'string') {\n\t\t\t\treturn source.url\n\t\t\t}\n\t\t\tif (hasUnresolvedDeepTokens(source)) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst { register, schema, id } = source\n\t\t\tif (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\n\t\t/**\n\t\t * The display text of one field.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tvalueOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\treturn this.format(raw, field, record)\n\t\t},\n\n\t\t/**\n\t\t * The text of a `line` entry that joins several parts.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tlineValue(record, field) {\n\t\t\tconst separator = typeof field.separator === 'string' ? field.separator : ' · '\n\t\t\treturn field.parts\n\t\t\t\t.map((part) => {\n\t\t\t\t\tconst spec = typeof part === 'string' ? { path: part } : (part || {})\n\t\t\t\t\t// A constant part (`{ text }`): \"Handelsregister\" after the legal form.\n\t\t\t\t\tif (!spec.path && typeof spec.text === 'string') {\n\t\t\t\t\t\treturn this.tr(spec.text)\n\t\t\t\t\t}\n\t\t\t\t\tconst value = this.format(readPath(record, spec.path), spec, record)\n\t\t\t\t\tif (value === '') {\n\t\t\t\t\t\treturn ''\n\t\t\t\t\t}\n\t\t\t\t\treturn spec.label ? `${this.tr(spec.label)} ${value}` : value\n\t\t\t\t})\n\t\t\t\t.filter((value) => value !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * Format a raw value.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ format?, map?, itemPath?, currency?, currencyField?, decimals? }`.\n\t\t * @param {object|null} [record] The object shown, which `currencyField` reads.\n\t\t * @return {string} The text, '' when empty.\n\t\t * @spec openspec/changes/facts-currency-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-money-amount\n\t\t */\n\t\tformat(raw, spec, record = null) {\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (spec.resolve && typeof spec.resolve === 'object') {\n\t\t\t\treturn this.refText(raw, spec)\n\t\t\t}\n\t\t\tconst format = spec.format || 'text'\n\t\t\tif (format === 'list' || Array.isArray(raw)) {\n\t\t\t\tconst items = (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t\t.map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))\n\t\t\t\t\t.map((item) => this.format(item, { ...spec, format: 'text' }))\n\t\t\t\t\t.filter((item) => item !== '')\n\t\t\t\treturn items.join(', ')\n\t\t\t}\n\t\t\tif (format === 'date' || 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\t// A year far from now forces the long form, so date-long reuses the board formatter.\n\t\t\t\treturn format === 'date-long'\n\t\t\t\t\t? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))\n\t\t\t\t\t: formatBoardDate(date)\n\t\t\t}\n\t\t\tif (format === 'currency') {\n\t\t\t\tconst money = formatMoney(raw, { currency: spec.currency, currencyField: spec.currencyField, decimals: spec.decimals }, record, unwrapAppConfig(this.cnAppConfig))\n\t\t\t\tif (money !== '') {\n\t\t\t\t\treturn money\n\t\t\t\t}\n\t\t\t\t// Not a number: shown as written, never as \"[object Object]\".\n\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t}\n\t\t\tif (typeof raw === 'object') {\n\t\t\t\tconst label = raw.title || raw.name || raw.label || raw.displayName\n\t\t\t\treturn typeof label === 'string' ? label : ''\n\t\t\t}\n\t\t\tif (typeof raw === 'boolean') {\n\t\t\t\tconst mapped = spec.map && spec.map[String(raw)]\n\t\t\t\treturn mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')\n\t\t\t}\n\t\t\tif (spec.map && Object.hasOwn(spec.map, String(raw))) {\n\t\t\t\treturn this.tr(spec.map[String(raw)])\n\t\t\t}\n\t\t\treturn String(raw)\n\t\t},\n\n\t\t/**\n\t\t * Replace `@source.<field>` / `@source.id` and the shared tokens in a string.\n\t\t *\n\t\t * @param {string} value The template.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The value, '' when a token stays unresolved.\n\t\t */\n\t\tresolveTemplate(value, record) {\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tlet unresolved = false\n\t\t\tconst replaced = value.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined\n\t\t\t\tif (read === undefined || read === null || read === '') {\n\t\t\t\t\tunresolved = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(read)\n\t\t\t})\n\t\t\tif (unresolved) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (replaced.charAt(0) === '@') {\n\t\t\t\tconst resolved = resolveDeepTokens(replaced, this.tokenCtx)\n\t\t\t\treturn typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)\n\t\t\t}\n\t\t\treturn replaced\n\t\t},\n\n\t\t/**\n\t\t * Resolve a route declaration (`{ name, params?, query? }`).\n\t\t *\n\t\t * @param {object} route The declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {object|null} The route, or null when a param stays unresolved.\n\t\t */\n\t\tresolveRoute(route, record) {\n\t\t\tif (!route || typeof route !== 'object' || !route.name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst out = { name: route.name }\n\t\t\tfor (const key of ['params', 'query']) {\n\t\t\t\tif (route[key] && typeof route[key] === 'object') {\n\t\t\t\t\tout[key] = {}\n\t\t\t\t\tfor (const [name, value] of Object.entries(route[key])) {\n\t\t\t\t\t\tconst resolved = this.resolveTemplate(value, record)\n\t\t\t\t\t\tif (resolved === '') {\n\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t}\n\t\t\t\t\t\tout[key][name] = resolved\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-facts-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tfont-size: 14px;\n}\n\n/* Each row is a grid of its own children: the wrapper takes no box. */\n.cn-facts-widget__row {\n\tdisplay: contents;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which\n pushed the label to the right of its column and added 12px around both. The\n class is doubled so these resets outrank that rule, and a dossiq-style\n `:deep()` override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__value.cn-facts-widget__value {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tfont-weight: 500;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-facts-widget__title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__line {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__label .hidden-visually {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-facts-widget__label--icon {\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.cn-facts-widget__value-link {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-facts-widget__inline-button,\n.cn-facts-widget__icon-button,\n.cn-facts-widget__action {\n\talign-items: center;\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n}\n\n.cn-facts-widget__inline-button {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 16px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n}\n\n.cn-facts-widget__icon-button {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-radius: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin-inline-start: 4px;\n\tpadding: 2px;\n\tvertical-align: middle;\n}\n\n.cn-facts-widget__icon-button:hover,\n.cn-facts-widget__icon-button:focus-visible {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__text {\n\tcolor: var(--color-main-text);\n\tfont-size: 15px;\n}\n\n/* A lead: an icon tile or initials beside a bold title and a muted subtitle. */\n.cn-facts-widget__lead {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-facts-widget__lead-tile {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 8px;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 14px;\n\tfont-weight: 700;\n\theight: 40px;\n\tjustify-content: center;\n\twidth: 40px;\n}\n\n.cn-facts-widget__lead-tile--success {\n\tbackground: color-mix(in srgb, var(--color-success) 14%, var(--color-main-background));\n\tborder-radius: 50%;\n\tcolor: var(--color-success-text, var(--color-success));\n}\n\n.cn-facts-widget__lead-tile--info {\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n}\n\n.cn-facts-widget__lead-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__lead-title {\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__lead-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-facts-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n}\n\n.cn-facts-widget__action:hover,\n.cn-facts-widget__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-facts-widget__link {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createVNode","_Fragment","_renderList","_createElementVNode","_normalizeClass","_createBlock","_createTextVNode","_resolveDynamicComponent"],"mappings":";;;AAWM,EAAA,KAAK,EAAC,iBAAiB;AAAC,EAAA,aAAA,EAAA,iBAAA;;;;AACvB,EAAA,KAAA,EAAA,wBAAA;AAAA,EAAA,aAAA,EAAA,gBAAA;;;;AAGF,EAAA,KAAA,EAAA,0BAAA;;;;AAGC,EAAA,KAAA,EAAA,wBAAA;AAAA,EAAA,aAAA,EAAA,gBAAA;;;;AAOA,EAAA,KAAI,EAAC,uBAAuB;;;AAcxB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAuB,GAAA;;;;;;;;AA+D5B,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAAyB,GAAA;AAC5B,MAAA,WAAA,GAAA,EAAA,KAAI,EAAA,6BAAA,GAAA;;;AACO,EAAA,KAAA,EAAA,gCAAA;;;;;;;;;;;;;KA5FlB,KAAA,CAAA,KAAA;sBAALA,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAC,eAAA,CADA,QAAE,CAAA,EAAA,CAAA,iCAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAEH,QAAA,CAAA,KAAA,CAAA,OAAA,KAAA,QAAA,CAAA,MAAA;AAAH,WAAAC,SAAA,EAAA,EAAAF,kBAAA,CAEM,OAFN,UAEM,EAAA;YADLG,WAAe,CAAA,wBAAA,EAAA,EAAA,IAAA,EAAA,EAAA,EAAA,CAAA;;AAEZ,UAAA,CAAA,QAAA,CAAA,MAAA,CAAA,MAAA,KAAA,CAAA;0BAAFH,kBAEE,CAAA,GAAA,EAFF,UAEE,EAAAC,eAAA,CADA,QAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;gDA+FF,CAAAG,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAAC,UAAA,CA5Fe,QAAC,CAAA,MAAA,EAAA,CAAV,KAAK,KAAA;;AAAK,gBAAA,GAAA,EAAA,KAAA,CAAA,GAAA;;AAEnB,gBAAA,CAAA,KAAK,CAAC,IAAI,KAAA,MAAA;qDA8CZ,gBAAA,EAAA;AA5CH,uBAAAH,SAAA,CAAA,IAAA,CAAA,EAAAF,kBAAA,CA2CKI,QA1CE,EAAA,IAAA,EAAAC,UAAA,CAAA,KAAA,CAAA,IAAA,GAAA,GAAA,KAAA;6CADPL,kBA2CK,CAAA,KAAA,EAAA;0BAzCH,GAAE,EAAE,GAAG,CAAC,GAAG;AACX,0BAAA,KAAG,EAAC,sBAAW;0BACf,aAAS,EAAA,CAAA,QAAA,EAAA,GAAA,CAAA,GAAA,CAAA,CAAA;;0BACTM,kBAOG,CAAA,IAAA,EAAA;AAPD,4BAAA,KAAK,EAACC,cAAA,CAAA,CAAA,wBAAY,EAAA,EAAA,8BAAA,EAAA,GAAA,CAAA,IAAA,EAAA,CAAA,CAAA;;AAElB,4BAAA,CAAA,GAAA,CAAA,IAAA;8CADHC,WAIe,CAAA,iBAAA,EAAA;;kCAFd,IAAK,EAAE,GAAG,CAAC,IAAI;AACf,kCAAA,IAAA,EAAA,EAAA;AACA,kCAAA,aAAW,EAAC,MAAE;;;4BACdF,kBAAe,CAAA,MAAA,EAAA;8BAAT,KAAK,EAAAC,cAAA,CAAA,EAAA,iBAAA,EAAI,GAAA,CAAA,IAAA,EAAA,CAAA;AAAA,6BAAA,EAAAN,eAAA,CAAA,GAAA,CAAA,KAAA,CAAA,EAAA,CAAA,mBAAA;;AAEf,0BAAAK,kBAAA,CA6BG,MA7BH,UA6BG,EAAA;AA3BG,4BAAA,CAAA,GAAG,CAAC,IAAG;8CADbN,kBAKqC,CAAA,GAAA,EAAA;;kCAHpC,IAAI,EAAA,GAAA,CAAA,IAAA;AACL,kCAAA,KAAK,EAAC,6BAA6B;AAClC,kCAAA,MAAM,EAAE,GAAG,CAAA,QAAA,cAAA,SAAA;AACX,kCAAA,GAAG,EAAE,GAAG,CAAC,QAAQ,2BAAmB,SAAA;AAAA,iCAAA,EAAAC,eAAA,CAAA,GAAA,CAAA,KAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;iEACR,oCAAd,GAAG,CAAC,MAAM,IAAA,CAAI,cAAA,CAAA,GAAA,CAAA,GAAA,IAAA,GAAA,CAAA,MAAA,GAAA,GAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA,CAAA;AAEvB,4BAAA,CAAA,GAAG,CAAC,MAAM,IAAA,CAAI,cAAA,CAAA,GAAA,CAAA,GAAA,CAAA;8CADpBD,kBASQ,CAAA,QAAA,EAAA;;AAPR,kCAAA,IAAI,EAAC,QAAQ;AACb,kCAAA,KAAK,EAAC,gCAAQ;kCACb,YAAM,EAAA,QAAA,CAAA,WAAA,CAAA,GAAA,CAAA;kCACN,aAAI,EAAA,CAAA,eAAA,EAAA,GAAA,CAAA,GAAA,CAAA,CAAA;kCACJ,OAAK,EAAA,MAAA,KAAE,QAAM,CAAA,MAAA,CAAA,GAAA,CAAA,CAAA;;kCACdG,WAAY,CAAA,qBAAA,EAAA;AAAA,oCAAA,IAAA,EAAA,EAAA;AAAA,oCAAA,aAAA,EAAA,MAAA;;AAAA,kCAAAM,eAAA,CAAA,GACZ,mBAAG,QAAS,CAAA,SAAA,CAAA,EAAA,CAAA,YAAA;;;AAGN,4BAAA,CAAA,GAAG,CAAC,IAAI;gEASN,CAAA,QAAA,EAAA;;AARR,kCAAA,IAAI,EAAC,QAAQ;AACb,kCAAA,KAAK,EAAA,8BAA+B;kCACnC,YAAU,EAAE,QAAS,CAAA,SAAA,CAAC,GAAG,CAAA;kCACzB,KAAK,EAAC,QAAA,CAAA,SAAA,CAAA,GAAA,CAAA;kCACN,aAAC,EAAA,CAAA,aAAA,EAAA,GAAA,CAAA,GAAA,CAAA,CAAA;kCACD,OAAC,EAAA,MAAA,KAAA,QAAA,CAAA,IAAA,CAAA,GAAA,CAAA,CAAA;;mCACF,KAAA,CAAA,MAAA,KAAA,GAAA,CAAA,GAAA;oDAAAD,WAAA,CAAA,gBAAA,EAAA;;AAAA,wCAAA,IAAA,EAAA,EAAA;AAAA,wCAAA,aAAA,EAAA,MAAA;;oDACAA,WAAK,CAAA,sBAAA,EAAA;;AAAA,wCAAA,IAAA,EAAA,EAAA;AAAA,wCAAA,aAAA,EAAA,MAAA;;;;;;;;AAOE,oBAAA,CAAA,KAAA,CAAA,IAAA,KAAA,OAAA;oCAFVA,WAOW,CAAAE,uBAAA,CANN,KAAK,CAAC,EAAE,GAAA,aAAA,GAAA,QAAA,CAAA,EAAA;;AAEZ,wBAAA,EAAE,EAAA,KAAA,CAAA,EAAA,IAAA,SAAA;AACH,wBAAA,KAAK,EAAC,wBAAwB;wBAC7B,aAAW,EAAA,CAAA,QAAA,EAAa,KAAG,CAAA,GAAA,CAAA,CAAA;;yCAC5B,MAAiB;AAAd,0BAAAD,eAAA,CAAAR,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;;AAGX,sBAAA,CAAA,KAAA,CAAA,IAAA,KAAA,MAAA;sCADJD,kBAKO,CAAA,MAAA,EAAA;;AAHN,0BAAA,KAAK,EAAC,uBAAuB;0BAC5B,aAAW,EAAA,CAAA,QAAA,EAAa,KAAG,CAAA,GAAA,CAAA,CAAA;AACzB,yBAAA,EAAAC,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,WAAA,CAAA;AAGZ,wBAAA,CAAA,KAAA,CAAA,IAAA,KAAA,MAAA;wCADHD,kBAKK,CAAA,MAAA,EAAA;;AAHJ,4BAAA,KAAK,EAAC,uBAAuB;4BAC5B,aAAW,EAAA,CAAA,QAAA,EAAa,KAAG,CAAA,GAAA,CAAA,CAAA;AACzB,2BAAA,EAAAC,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,WAAA,CAAA;AAGH,0BAAA,CAAA,KAAK,CAAC,IAAI,KAAA,MAAA;0CADpBD,kBAeI,CAAA,KAAA,EAAA;;AAbJ,8BAAA,KAAI,EAAC,uBAAa;8BAClB,aAAW,EAAA,CAAA,QAAA,EAAa,KAAK,CAAC,GAAG,CAAA,CAAA;;8BACjCM,kBAMG,CAAA,MAAA,EAAA;AALH,gCAAA,KAAK,EAAAC,cAAA,CAAA,CAAA,4BAAA,EACkC,CAAA,4BAAA,EAAA,KAAA,CAAA,IAAA,CAAA,CAAA,CAAA,CAAA;AACvC,gCAAA,aAAW,EAAC,MAAM;;AACJ,gCAAA,CAAA,KAAK,CAAC,IAAI;kDAAxBC,WAA0D,CAAA,iBAAA,EAAA;;sCAAhC,IAAK,EAAE,KAAK,CAAC,IAAI;AAAG,sCAAA,IAAI,EAAE,EAAE;;kDACtDR,kBAAK,CAAAI,QAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,EAAA;AAAA,sCAAAK,eAAA,CAAAR,eAAA,CAAA,KAAA,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;AAEN,8BAAAK,kBAAA,CAGO,QAHP,WAGO,EAAA;AAFN,gCAAAA,kBAAA,CAAY,QAAZ,EAAA,WAAY,EAAAL,eAAA,CAAA,KAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;AACA,gCAAA,CAAA,KAAK,CAAC,QAAC;AAAnB,qCAAAC,SAAA,EAAA,EAAAF,kBAAA,CAAmB,MAAnB,EAAA,WAAmB,EAAAC,eAAA,CAAA,KAAA,CAAA,QAAA,CAAA,EAAA,CAAA,YAAA;;;;AAGL,4BAAA,CAAA,KAAK,CAAC,IAAI,KAAA,MAAA;4CAAxBD,kBAMI,CAAA,KAAA,EAAA,WAAA,EAAA;gCALLG,WAIgC,CAAA,wBAAA,EAAA;kCAH9B,KAAK,EAAC,KAAA,CAAA,KAAA;kCACN,OAAK,EAAA,KAAA,CAAA,OAAA;AACN,kCAAA,IAAI,EAAC,OAAA;kCACJ,aAAW,EAAA,CAAA,QAAA,EAAc,KAAI,CAAC,GAAA,CAAA,CAAA;;;;;;AAM5B,IAAA,CAAA,QAAA,CAAA,IAAI,IAAI,QAAC,CAAA,MAAA;iCAMJ,CAAAO,uBAAA,CAPN,QAAI,CAAA,IAAA,CAAC,EAAE,GAAA,aAAA,GAAA,GAAA,CAAA,EAAA;;AAEX,UAAA,EAAE,EAAE,QAAA,CAAA,IAAI,CAAC,EAAA,IAAA,SAAA;AACT,UAAA,IAAI,EAAA,QAAA,CAAA,IAAA,CAAA,IAAA,IAAA,SAAA;AACL,UAAA,KAAK,EAAC,uBAAgB;AACtB,UAAA,aAAW,EAAC,eAAA;;2BACZ,MAAgB;AAAb,YAAAD,eAAA,CAAAR,eAAA,CAAA,QAAA,CAAA,IAAI,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;;;AAGP,IAAA,CAAA,QAAA,CAAA,MAAM,IAAI,QAAM,CAAA,MAAA;sBADtBD;;AAEA,UAAA,IAAI,EAAC,QAAQ;AACb,UAAA,KAAK,EAAC,yBAAC;AACP,UAAA,aAAG,EAAA,iBAAA;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnFactsWidget.vue3.js","sources":["../../../../src/components/CnFactsWidget/CnFactsWidget.vue?vue&type=template&id=c2fcc10e&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-facts-widget\" data-testid=\"cn-facts-widget\">\n\t\t<p v-if=\"error\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-error\">\n\t\t\t{{ tr('Could not load this information') }}\n\t\t</p>\n\t\t<div v-else-if=\"loading && !record\" class=\"cn-facts-widget__loading\">\n\t\t\t<NcLoadingIcon :size=\"20\" />\n\t\t</div>\n\t\t<p v-else-if=\"blocks.length === 0\" class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t{{ emptyLabel }}\n\t\t</p>\n\t\t<template v-else>\n\t\t\t<template v-for=\"block in blocks\" :key=\"block.key\">\n\t\t\t\t<dl\n\t\t\t\t\tv-if=\"block.kind === 'rows'\"\n\t\t\t\t\tclass=\"cn-facts-widget__rows\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__rows--columns': twoColumns }\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in block.rows\"\n\t\t\t\t\t\t:key=\"row.key\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__row\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${row.key}`\">\n\t\t\t\t\t\t<dt class=\"cn-facts-widget__label\" :class=\"{ 'cn-facts-widget__label--icon': row.icon }\">\n\t\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\t\tv-if=\"row.icon\"\n\t\t\t\t\t\t\t\t:name=\"row.icon\"\n\t\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t\t<span :class=\"{ 'hidden-visually': row.icon }\">{{ row.label }}</span>\n\t\t\t\t\t\t</dt>\n\t\t\t\t\t\t<dd class=\"cn-facts-widget__value\">\n\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\tv-if=\"row.href\"\n\t\t\t\t\t\t\t\t:href=\"row.href\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\"\n\t\t\t\t\t\t\t\t:target=\"row.external ? '_blank' : undefined\"\n\t\t\t\t\t\t\t\t:rel=\"row.external ? 'noopener noreferrer' : undefined\">{{ row.value }}</a>\n\t\t\t\t\t\t\t<span v-else>{{ row.masked && !revealed[row.key] ? row.masked : row.value }}</span>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.reveal && !revealed[row.key]\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"revealLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"reveal(row)\">\n\t\t\t\t\t\t\t\t<EyeOutline :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t{{ showLabel }}\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\tv-if=\"row.copy\"\n\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__icon-button\"\n\t\t\t\t\t\t\t\t:aria-label=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:title=\"copyLabel(row)\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-copy-${row.key}`\"\n\t\t\t\t\t\t\t\t@click=\"copy(row)\">\n\t\t\t\t\t\t\t\t<Check v-if=\"copied === row.key\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t\t<ContentCopy v-else :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t</dd>\n\t\t\t\t\t</div>\n\t\t\t\t</dl>\n\t\t\t\t<component\n\t\t\t\t\t:is=\"block.to ? 'router-link' : 'strong'\"\n\t\t\t\t\tv-else-if=\"block.kind === 'title'\"\n\t\t\t\t\t:to=\"block.to || undefined\"\n\t\t\t\t\tclass=\"cn-facts-widget__title\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</component>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'line'\"\n\t\t\t\t\tclass=\"cn-facts-widget__line\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"block.kind === 'text'\"\n\t\t\t\t\tclass=\"cn-facts-widget__text\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t{{ block.value }}\n\t\t\t\t</span>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'lead'\"\n\t\t\t\t\tclass=\"cn-facts-widget__lead\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"cn-facts-widget__lead-tile\"\n\t\t\t\t\t\t:class=\"`cn-facts-widget__lead-tile--${block.tone}`\"\n\t\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t\t<CnIcon v-if=\"block.icon\" :name=\"block.icon\" :size=\"20\" />\n\t\t\t\t\t\t<template v-else>{{ block.initials }}</template>\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__lead-text\">\n\t\t\t\t\t\t<strong class=\"cn-facts-widget__lead-title\">{{ block.value }}</strong>\n\t\t\t\t\t\t<span v-if=\"block.subtitle\" class=\"cn-facts-widget__lead-subtitle\">{{ block.subtitle }}</span>\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pill'\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t:label=\"block.value\"\n\t\t\t\t\t\t:variant=\"block.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\" />\n\t\t\t\t</div>\n\t\t\t</template>\n\t\t</template>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t{{ link.label }}\n\t\t</component>\n\t\t<button\n\t\t\tv-if=\"action && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\tdata-testid=\"cn-facts-action\"\n\t\t\t@click=\"runAction(content.action)\">\n\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t{{ action.label }}\n\t\t</button>\n\t</div>\n</template>\n\n<script>\nimport { emit } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon } from '@nextcloud/vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ContentCopy from 'vue-material-design-icons/ContentCopy.vue'\nimport EyeOutline from 'vue-material-design-icons/EyeOutline.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useRefLabels } from '../../composables/useRefLabels.js'\nimport { dispatchAction } from '../../utils/actionsDispatcher.js'\nimport { isBadgeVariant } from '../../utils/badgeVariants.js'\nimport { formatBoardDate } from '../../utils/boardDate.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { formatMoney, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text'])\n\n/**\n * The label resolver every facts widget on the page shares, made on first use\n * (the object store needs an active pinia). One batched request per register and\n * schema for the ids it has not seen; a label it has read stays until the page\n * reloads, and an object the store already holds is not fetched at all.\n *\n * @type {{resolve: Function}|null}\n */\nlet refLabels = null\n\n/**\n * The entry that reads a resolved pill's tone: the same reference, with the\n * referenced object's `toneField` as the field to read.\n *\n * @param {object} spec A facts entry.\n * @return {object|null} `{ path, itemPath, resolve }`, or null when no `toneField` is set.\n */\nfunction toneSpec(spec) {\n\tconst resolve = spec && spec.resolve\n\tif (!resolve || typeof resolve !== 'object' || typeof resolve.toneField !== 'string' || resolve.toneField === '') {\n\t\treturn null\n\t}\n\treturn { path: spec.path, itemPath: spec.itemPath, resolve: { register: resolve.register, schema: resolve.schema, labelField: resolve.toneField } }\n}\n\n/** The page-wide reference label resolver. */\nfunction sharedRefLabels() {\n\tif (!refLabels) {\n\t\trefLabels = useRefLabels()\n\t}\n\treturn refLabels\n}\n\n/**\n * Key of a resolved reference in the widget's `labels` data.\n *\n * @param {{register: string, schema: string, labelField?: string}} resolve The entry's `resolve` config.\n * @param {string} id The referenced object's id.\n * @return {string} The key.\n */\nfunction refKey(resolve, id) {\n\treturn `${resolve.register}/${resolve.schema}/${resolve.labelField || ''}/${id}`\n}\n\n/** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */\nexport const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'\n\n/**\n * CnFactsWidget — the `facts` widget: a card of facts about ONE object.\n *\n * Renders the configured fields of the current detail-page object, or of one\n * other object named by `content.source`, as the side cards on the screens\n * draw them: label/value rows (a two-column grid, label muted), a bold title\n * line that may link, muted meta lines that join several fields with \" · \",\n * a status pill, and one link at the foot.\n *\n * Everything is configuration; nothing in it knows an app:\n *\n * ```json\n * {\n * \"type\": \"facts\",\n * \"title\": \"Zaak\",\n * \"content\": {\n * \"source\": { \"register\": \"dossiq\", \"schema\": \"case\", \"id\": \"@object.case\" },\n * \"fields\": [\n * { \"as\": \"title\", \"path\": \"title\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * { \"as\": \"line\", \"parts\": [\"identifier\", \"caseTypeTitle\"] },\n * { \"as\": \"pill\", \"path\": \"status\", \"map\": { \"open\": \"In behandeling\" } },\n * { \"path\": \"deadline\", \"label\": \"Termijn\", \"format\": \"date\" }\n * ],\n * \"link\": { \"label\": \"Naar de zaak\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"@source.id\" } } },\n * \"emptyText\": \"Geen zaak\"\n * }\n * }\n * ```\n *\n * Tokens: `source.id` takes the shared filter tokens; `source.url` takes them\n * inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)\n * (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes\n * and the link also take `@source.<field>` (a field of the object shown).\n * A source whose id token does not resolve renders the empty text, never a\n * request for the literal token.\n *\n * Labels, map values, the link label and the empty text go through the host\n * app's translate (`cnTranslate`).\n */\nexport default {\n\tname: 'CnFactsWidget',\n\n\tcomponents: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** The page's app config; its `currency` is the reporting currency a money value falls back to. */\n\t\tcnAppConfig: { default: null },\n\t\t/** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `fields` — the entries, in order. Each is `{ path, label?, as?, from?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, colorMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.\n\t\t * `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`) or `pill`.\n\t\t * A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.\n\t\t * A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.\n\t\t * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.\n\t\t * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by \", \", each item read at `itemPath`) or `currency` (a money amount in the reader's language, \"€ 1.234,50\": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).\n\t\t * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default \" · \"); empty parts are dropped.\n\t\t * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `link` — `{ label, route? | href? }` at the foot.\n\t\t * - `hideEmpty` — drop an entry whose value is empty (default true).\n\t\t * - `columns` — `2` lays the rows out in two columns with the label above the value (board DqContact); a row with `parts` joins them like a line (an address on one row).\n\t\t * - `emptyText` — shown when no entry has a value.\n\t\t *\n\t\t * @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}\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 * The current detail-page object, passed by the detail page host.\n\t\t * Read when `content.source` is not set.\n\t\t */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The current detail-page object id, for the `@objectId` token. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['header', 'reveal'],\n\n\tdata() {\n\t\treturn {\n\t\t\tfetched: null,\n\t\t\tloading: false,\n\t\t\terror: false,\n\t\t\t/** Keys of the masked rows the reader chose to show. */\n\t\t\trevealed: {},\n\t\t\t/** Key of the row whose value was just copied. */\n\t\t\tcopied: '',\n\t\t\t/** Labels of the referenced objects read so far, by `refKey`. */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the rows sit in two columns, label above value.\n\t\t *\n\t\t * @spec openspec/changes/facts-columns/specs/facts-widget/spec.md#requirement-facts-rows-can-sit-in-two-columns\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumns() {\n\t\t\treturn Number(this.content?.columns) === 2\n\t\t},\n\n\t\t/**\n\t\t * The token context: the detail object (prop, else the injected context)\n\t\t * and the page workspace.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c)\n\t\t\tconst injected = unwrap(this.cnObjectContext) || {}\n\t\t\treturn {\n\t\t\t\t...injected,\n\t\t\t\tobjectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,\n\t\t\t\tobject: this.objectData || injected.object || null,\n\t\t\t\tworkspace: unwrap(this.cnWorkspaceContext) || {},\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The source declaration with its tokens resolved, or null for the current object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedSource() {\n\t\t\tconst source = this.content?.source\n\t\t\tif (!source || typeof source !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof source.url === 'string' && source.url !== '') {\n\t\t\t\t// The endpoint grammar: tokens INSIDE the url, and a request\n\t\t\t\t// that is blocked while one leaves an empty path segment.\n\t\t\t\tconst request = resolveEndpointRequest({ url: source.url }, this.tokenCtx)\n\t\t\t\treturn { url: request.blocked ? '' : request.url }\n\t\t\t}\n\t\t\treturn resolveDeepTokens(source, this.tokenCtx)\n\t\t},\n\n\t\t/**\n\t\t * A stable key of the resolved source, so the widget refetches only when it changes.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tsourceKey() {\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The object the facts are read from.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecord() {\n\t\t\treturn this.resolvedSource ? this.fetched : this.objectData\n\t\t},\n\n\t\t/**\n\t\t * The entries to draw, consecutive rows grouped into one list.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tif (!sourceRecord) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst hideEmpty = this.content?.hideEmpty !== false\n\t\t\tconst out = []\n\t\t\tfields.forEach((field, index) => {\n\t\t\t\tif (!field || typeof field !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// `from: \"object\"` reads the page's own object while the card shows a `source`.\n\t\t\t\tconst record = field.from === 'object' && this.objectData ? this.objectData : sourceRecord\n\t\t\t\tconst kind = this.kindOf(field)\n\t\t\t\tconst key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\\w-]/g, '-')\n\t\t\t\tconst value = this.entryValue(record, field, kind)\n\t\t\t\tif (value === '' && (hideEmpty || kind === 'lead')) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (kind === 'row') {\n\t\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\t\tconst row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tout.push({ kind: 'rows', key: `rows-${index}`, rows: [row] })\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst block = { kind, key, value }\n\t\t\t\tif (kind === 'lead') {\n\t\t\t\t\tblock.subtitle = this.partsValue(record, field.subtitle)\n\t\t\t\t\tblock.icon = typeof field.icon === 'string' ? field.icon : ''\n\t\t\t\t\tblock.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath))\n\t\t\t\t\tblock.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral'\n\t\t\t\t}\n\t\t\t\tif (kind === 'title' && field.route) {\n\t\t\t\t\tblock.to = this.resolveRoute(field.route, record)\n\t\t\t\t}\n\t\t\t\tif (kind === 'pill') {\n\t\t\t\t\tconst raw = readPath(record, field.path)\n\t\t\t\t\tconst colours = field.variantMap || field.colorMap\n\t\t\t\t\t// A resolved pill looks its tone up by the referenced object's tone field,\n\t\t\t\t\t// then by the resolved label, then by the stored value.\n\t\t\t\t\tconst keys = field.resolve && typeof field.resolve === 'object'\n\t\t\t\t\t\t? [toneSpec(field) ? this.refLabels(raw, toneSpec(field))[0] : undefined, this.refLabels(raw, field)[0], raw]\n\t\t\t\t\t\t: [raw]\n\t\t\t\t\tconst hit = colours ? keys.find((candidate) => (typeof candidate === 'string' || typeof candidate === 'number') && Object.hasOwn(colours, candidate)) : undefined\n\t\t\t\t\tconst variant = hit !== undefined ? colours[hit] : field.variant\n\t\t\t\t\tblock.variant = isBadgeVariant(variant) ? variant : 'default'\n\t\t\t\t}\n\t\t\t\tout.push(block)\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tlink() {\n\t\t\tconst link = this.content?.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route) {\n\t\t\t\tconst to = this.resolveRoute(link.route, this.record)\n\t\t\t\treturn to ? { label: this.tr(link.label), to } : null\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\tconst href = this.resolveTemplate(link.href, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The action under the facts, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\taction() {\n\t\t\treturn this.actionView(this.content?.action)\n\t\t},\n\n\t\t/**\n\t\t * The header action, as drawn: `{ label, icon }`, or null.\n\t\t *\n\t\t * @return {{label: string, icon: string}|null}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\theaderAction() {\n\t\t\treturn this.actionView(this.content?.headerAction)\n\t\t},\n\n\t\t/** @return {string} The reveal button's text. */\n\t\tshowLabel() {\n\t\t\treturn t('nextcloud-vue', 'Show')\n\t\t},\n\n\t\t/**\n\t\t * The empty line.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\temptyLabel() {\n\t\t\treturn this.tr(this.content?.emptyText || 'Nothing to show yet')\n\t\t},\n\t},\n\n\twatch: {\n\t\tsourceKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.load()\n\t\t\t},\n\t\t},\n\n\t\trecord: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t},\n\t\t},\n\n\t\theaderAction: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\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 kind of a field entry.\n\t\t *\n\t\t * @param {object} field The entry.\n\t\t * @return {string} `row`, `title`, `line` or `pill`.\n\t\t */\n\t\tkindOf(field) {\n\t\t\tif (KINDS.includes(field.as)) {\n\t\t\t\treturn field.as\n\t\t\t}\n\t\t\treturn field.label || field.icon ? 'row' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * The display text of an entry of any kind: a lead's title, the parts of\n\t\t * a line or text, a constant `text`, else the value at `path`.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} kind The entry's kind.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\tentryValue(record, field, kind) {\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text' || kind === 'row') && Array.isArray(field.parts)) {\n\t\t\t\treturn this.lineValue(record, field)\n\t\t\t}\n\t\t\tif ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {\n\t\t\t\treturn this.tr(field.text)\n\t\t\t}\n\t\t\treturn this.valueOf(record, field)\n\t\t},\n\n\t\t/**\n\t\t * A lead's title or subtitle: a path, a part spec, or an array of parts\n\t\t * joined as a line joins them.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string|object|Array} spec The declaration.\n\t\t * @return {string}\n\t\t */\n\t\tpartsValue(record, spec) {\n\t\t\tif (typeof spec === 'string' && spec !== '') {\n\t\t\t\treturn this.valueOf(record, { path: spec })\n\t\t\t}\n\t\t\tif (Array.isArray(spec)) {\n\t\t\t\treturn this.lineValue(record, { parts: spec })\n\t\t\t}\n\t\t\tif (spec && typeof spec === 'object') {\n\t\t\t\treturn this.lineValue(record, { parts: [spec] })\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The initials of a name: \"Sanne de Vries\" reads \"SV\".\n\t\t *\n\t\t * @param {unknown} name The name.\n\t\t * @return {string}\n\t\t */\n\t\tinitialsOf(name) {\n\t\t\tif (typeof name !== 'string' || name.trim() === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst words = name.trim().split(/\\s+/).filter((word) => /^\\p{Lu}/u.test(word))\n\t\t\tconst picked = words.length ? words : name.trim().split(/\\s+/)\n\t\t\tconst first = picked[0].charAt(0)\n\t\t\tconst last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : ''\n\t\t\treturn (first + last).toUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * What a row adds to its value: an icon, a link for the email, phone\n\t\t * and url formats, the masked text, and the copy and reveal buttons.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @param {string} value The display text.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trowExtras(record, field, value) {\n\t\t\tconst extras = {\n\t\t\t\ticon: typeof field.icon === 'string' ? field.icon : '',\n\t\t\t\tpath: field.path || '',\n\t\t\t\traw: readPath(record, field.path),\n\t\t\t\tcopy: field.copy === true && value !== '',\n\t\t\t\treveal: false,\n\t\t\t\tmasked: '',\n\t\t\t\thref: '',\n\t\t\t\texternal: false,\n\t\t\t}\n\t\t\tif (value === '') {\n\t\t\t\treturn extras\n\t\t\t}\n\t\t\tif (field.format === 'email') {\n\t\t\t\textras.href = `mailto:${value}`\n\t\t\t} else if (field.format === 'phone') {\n\t\t\t\textras.href = `tel:${value.replace(/[^\\d+]/g, '')}`\n\t\t\t} else if (field.format === 'url') {\n\t\t\t\textras.href = /^https?:\\/\\//i.test(value) ? value : `https://${value}`\n\t\t\t\textras.external = true\n\t\t\t} else if (field.format === 'masked') {\n\t\t\t\textras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value\n\t\t\t\textras.reveal = field.reveal === true && value.length > 4\n\t\t\t}\n\t\t\treturn extras\n\t\t},\n\n\t\t/**\n\t\t * Show a masked value, and say so: the `reveal` event and the event-bus\n\t\t * channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\treveal(row) {\n\t\t\tthis.revealed = { ...this.revealed, [row.key]: true }\n\t\t\tconst detail = { path: row.path }\n\t\t\t/**\n\t\t\t * @event reveal Emitted when the reader shows a masked value.\n\t\t\t * @type {{path: string}} The path of the shown field.\n\t\t\t */\n\t\t\tthis.$emit('reveal', detail)\n\t\t\temit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null })\n\t\t},\n\n\t\t/**\n\t\t * Copy a row's value to the clipboard.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync copy(row) {\n\t\t\tconst text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw)\n\t\t\ttry {\n\t\t\t\tawait navigator.clipboard.writeText(text)\n\t\t\t\tthis.copied = row.key\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tif (this.copied === row.key) {\n\t\t\t\t\t\tthis.copied = ''\n\t\t\t\t\t}\n\t\t\t\t}, 2000)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] could not copy:', e?.message || e)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The copy button's accessible name.\n\t\t */\n\t\tcopyLabel(row) {\n\t\t\treturn this.copied === row.key\n\t\t\t\t? t('nextcloud-vue', 'Copied')\n\t\t\t\t: t('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row The row.\n\t\t * @return {string} The reveal button's accessible name.\n\t\t */\n\t\trevealLabel(row) {\n\t\t\treturn t('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()\n\t\t},\n\n\t\t/**\n\t\t * The drawn part of an action declaration.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {{label: string, icon: string}|null}\n\t\t */\n\t\tactionView(action) {\n\t\t\tif (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch an action with the record's tokens resolved (`@objectId`,\n\t\t * `@object.<field>`), through the page's dispatcher when there is one.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @return {unknown}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead\n\t\t */\n\t\trunAction(action) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst resolved = resolveDeepTokens(action, this.tokenCtx)\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(resolved)\n\t\t\t}\n\t\t\treturn dispatchAction(resolved, { router: this.$router || null })\n\t\t},\n\n\t\t/** Tell the card around the facts what its header shows (the header action). */\n\t\temitHeader() {\n\t\t\tconst view = this.headerAction\n\t\t\t/**\n\t\t\t * @event header Emitted with the card header's parts: the header action, if any.\n\t\t\t * @type {{link: null, note: string, action: (object|null)}} `action` is `{ label, icon, run }`.\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\tlink: null,\n\t\t\t\tnote: '',\n\t\t\t\taction: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The text of a reference: the labels of the objects its id(s) name, joined\n\t\t * by \", \". An id whose label is not known yet (or cannot be read) adds nothing.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids (or of `{ id }` objects).\n\t\t * @param {object} spec `{ resolve: { register, schema, labelField? }, itemPath? }`.\n\t\t * @return {string}\n\t\t */\n\t\trefText(raw, spec) {\n\t\t\treturn this.refLabels(raw, spec)\n\t\t\t\t.map((label) => (spec.map && Object.hasOwn(spec.map, label) ? this.tr(spec.map[label]) : label))\n\t\t\t\t.join(', ')\n\t\t},\n\n\t\t/**\n\t\t * The labels of a reference as read, before `map` is applied: the key a\n\t\t * pill's `variantMap` and `colorMap` look up.\n\t\t *\n\t\t * @param {unknown} raw The id, or a list of ids.\n\t\t * @param {object} spec `{ resolve, itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefLabels(raw, spec) {\n\t\t\treturn this.refIds(raw, spec)\n\t\t\t\t.map((id) => this.labels[refKey(spec.resolve, id)])\n\t\t\t\t.filter((label) => typeof label === 'string' && label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The ids a reference value holds.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ itemPath? }`.\n\t\t * @return {string[]}\n\t\t */\n\t\trefIds(raw, spec) {\n\t\t\treturn (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t.map((item) => (item && typeof item === 'object' ? (spec.itemPath ? readPath(item, spec.itemPath) : (item.id ?? item.uuid)) : item))\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '' && typeof id !== 'object')\n\t\t\t\t.map(String)\n\t\t},\n\n\t\t/**\n\t\t * Every entry or part that carries `resolve`, with the record's value at its path.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @return {Array<{spec: object, raw: unknown}>}\n\t\t */\n\t\trefSpecs(record) {\n\t\t\tconst fields = Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t\tconst specs = []\n\t\t\tconst visit = (spec) => {\n\t\t\t\tif (!spec || typeof spec !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (spec.resolve && typeof spec.resolve === 'object' && spec.path) {\n\t\t\t\t\tspecs.push({ spec, raw: readPath(record, spec.path) })\n\t\t\t\t\tif (toneSpec(spec)) {\n\t\t\t\t\t\tspecs.push({ spec: toneSpec(spec), raw: readPath(record, spec.path) })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst nested = [...(Array.isArray(spec.parts) ? spec.parts : []), ...(Array.isArray(spec.title) ? spec.title : [spec.title]), ...(Array.isArray(spec.subtitle) ? spec.subtitle : [spec.subtitle])]\n\t\t\t\tnested.forEach(visit)\n\t\t\t}\n\t\t\tfields.forEach(visit)\n\t\t\treturn specs\n\t\t},\n\n\t\t/**\n\t\t * Read the labels of the referenced objects, one batched request per\n\t\t * register and schema, and keep them in `labels`.\n\t\t *\n\t\t * @param {object|null} record The object shown (defaults to the current record).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLabels(record = this.record) {\n\t\t\tif (!record) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst groups = new Map()\n\t\t\tfor (const { spec, raw } of this.refSpecs(record)) {\n\t\t\t\tconst { register, schema, labelField } = spec.resolve\n\t\t\t\tif (!register || !schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst groupKey = `${register}/${schema}/${labelField || ''}`\n\t\t\t\tconst group = groups.get(groupKey) || { resolve: spec.resolve, ids: new Set() }\n\t\t\t\tthis.refIds(raw, spec).forEach((id) => group.ids.add(id))\n\t\t\t\tgroups.set(groupKey, group)\n\t\t\t}\n\t\t\tif (groups.size === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolver = sharedRefLabels()\n\t\t\tconst next = { ...this.labels }\n\t\t\tawait Promise.all([...groups.values()].map(async ({ resolve, ids }) => {\n\t\t\t\tconst found = await resolver.resolve(resolve.register, resolve.schema, [...ids], resolve.labelField)\n\t\t\t\tObject.entries(found).forEach(([id, label]) => {\n\t\t\t\t\tnext[refKey(resolve, id)] = label\n\t\t\t\t})\n\t\t\t}))\n\t\t\tthis.labels = next\n\t\t},\n\n\t\t/**\n\t\t * Read the source object.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync load() {\n\t\t\tconst source = this.resolvedSource\n\t\t\tthis.error = false\n\t\t\tif (!source) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst url = this.sourceUrl(source)\n\t\t\tif (!url) {\n\t\t\t\tthis.fetched = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\t\tconst response = await axios.get(full)\n\t\t\t\tconst data = response && response.data\n\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFactsWidget] the source could not be read:', e?.message || e)\n\t\t\t\tthis.fetched = null\n\t\t\t\tthis.error = true\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The URL of a resolved source, or '' when it cannot be read yet.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {string}\n\t\t */\n\t\tsourceUrl(source) {\n\t\t\tif (typeof source.url === 'string') {\n\t\t\t\treturn source.url\n\t\t\t}\n\t\t\tif (hasUnresolvedDeepTokens(source)) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst { register, schema, id } = source\n\t\t\tif (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`\n\t\t},\n\n\t\t/**\n\t\t * The display text of one field.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tvalueOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\treturn this.format(raw, field, record)\n\t\t},\n\n\t\t/**\n\t\t * The text of a `line` entry that joins several parts.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t */\n\t\tlineValue(record, field) {\n\t\t\tconst separator = typeof field.separator === 'string' ? field.separator : ' · '\n\t\t\treturn field.parts\n\t\t\t\t.map((part) => {\n\t\t\t\t\tconst spec = typeof part === 'string' ? { path: part } : (part || {})\n\t\t\t\t\t// A constant part (`{ text }`): \"Handelsregister\" after the legal form.\n\t\t\t\t\tif (!spec.path && typeof spec.text === 'string') {\n\t\t\t\t\t\treturn this.tr(spec.text)\n\t\t\t\t\t}\n\t\t\t\t\t// A group of parts with its own separator (\"Parkstraat 24\" inside\n\t\t\t\t\t// \"Parkstraat 24, 3421 AB Zuiddrecht\").\n\t\t\t\t\tif (!spec.path && Array.isArray(spec.parts)) {\n\t\t\t\t\t\treturn this.lineValue(record, spec)\n\t\t\t\t\t}\n\t\t\t\t\tconst value = this.format(readPath(record, spec.path), spec, record)\n\t\t\t\t\tif (value === '') {\n\t\t\t\t\t\treturn ''\n\t\t\t\t\t}\n\t\t\t\t\treturn spec.label ? `${this.tr(spec.label)} ${value}` : value\n\t\t\t\t})\n\t\t\t\t.filter((value) => value !== '')\n\t\t\t\t.join(separator)\n\t\t},\n\n\t\t/**\n\t\t * Format a raw value.\n\t\t *\n\t\t * @param {unknown} raw The value.\n\t\t * @param {object} spec `{ format?, map?, itemPath?, currency?, currencyField?, decimals? }`.\n\t\t * @param {object|null} [record] The object shown, which `currencyField` reads.\n\t\t * @return {string} The text, '' when empty.\n\t\t * @spec openspec/changes/facts-currency-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-money-amount\n\t\t */\n\t\tformat(raw, spec, record = null) {\n\t\t\tif (raw === null || raw === undefined || raw === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (spec.resolve && typeof spec.resolve === 'object') {\n\t\t\t\treturn this.refText(raw, spec)\n\t\t\t}\n\t\t\tconst format = spec.format || 'text'\n\t\t\tif (format === 'list' || Array.isArray(raw)) {\n\t\t\t\tconst items = (Array.isArray(raw) ? raw : [raw])\n\t\t\t\t\t.map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))\n\t\t\t\t\t.map((item) => this.format(item, { ...spec, format: 'text' }))\n\t\t\t\t\t.filter((item) => item !== '')\n\t\t\t\treturn items.join(', ')\n\t\t\t}\n\t\t\tif (format === 'date' || 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\t// A year far from now forces the long form, so date-long reuses the board formatter.\n\t\t\t\treturn format === 'date-long'\n\t\t\t\t\t? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))\n\t\t\t\t\t: formatBoardDate(date)\n\t\t\t}\n\t\t\tif (format === 'currency') {\n\t\t\t\tconst money = formatMoney(raw, { currency: spec.currency, currencyField: spec.currencyField, decimals: spec.decimals }, record, unwrapAppConfig(this.cnAppConfig))\n\t\t\t\tif (money !== '') {\n\t\t\t\t\treturn money\n\t\t\t\t}\n\t\t\t\t// Not a number: shown as written, never as \"[object Object]\".\n\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t}\n\t\t\tif (typeof raw === 'object') {\n\t\t\t\tconst label = raw.title || raw.name || raw.label || raw.displayName\n\t\t\t\treturn typeof label === 'string' ? label : ''\n\t\t\t}\n\t\t\tif (typeof raw === 'boolean') {\n\t\t\t\tconst mapped = spec.map && spec.map[String(raw)]\n\t\t\t\treturn mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')\n\t\t\t}\n\t\t\tif (spec.map && Object.hasOwn(spec.map, String(raw))) {\n\t\t\t\treturn this.tr(spec.map[String(raw)])\n\t\t\t}\n\t\t\treturn String(raw)\n\t\t},\n\n\t\t/**\n\t\t * Replace `@source.<field>` / `@source.id` and the shared tokens in a string.\n\t\t *\n\t\t * @param {string} value The template.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The value, '' when a token stays unresolved.\n\t\t */\n\t\tresolveTemplate(value, record) {\n\t\t\tif (typeof value !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\tlet unresolved = false\n\t\t\tconst replaced = value.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined\n\t\t\t\tif (read === undefined || read === null || read === '') {\n\t\t\t\t\tunresolved = true\n\t\t\t\t\treturn match\n\t\t\t\t}\n\t\t\t\treturn String(read)\n\t\t\t})\n\t\t\tif (unresolved) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (replaced.charAt(0) === '@') {\n\t\t\t\tconst resolved = resolveDeepTokens(replaced, this.tokenCtx)\n\t\t\t\treturn typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)\n\t\t\t}\n\t\t\treturn replaced\n\t\t},\n\n\t\t/**\n\t\t * Resolve a route declaration (`{ name, params?, query? }`).\n\t\t *\n\t\t * @param {object} route The declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {object|null} The route, or null when a param stays unresolved.\n\t\t */\n\t\tresolveRoute(route, record) {\n\t\t\tif (!route || typeof route !== 'object' || !route.name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst out = { name: route.name }\n\t\t\tfor (const key of ['params', 'query']) {\n\t\t\t\tif (route[key] && typeof route[key] === 'object') {\n\t\t\t\t\tout[key] = {}\n\t\t\t\t\tfor (const [name, value] of Object.entries(route[key])) {\n\t\t\t\t\t\tconst resolved = this.resolveTemplate(value, record)\n\t\t\t\t\t\tif (resolved === '') {\n\t\t\t\t\t\t\treturn null\n\t\t\t\t\t\t}\n\t\t\t\t\t\tout[key][name] = resolved\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-facts-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tfont-size: 14px;\n}\n\n/* Each row is a grid of its own children: the wrapper takes no box. */\n.cn-facts-widget__row {\n\tdisplay: contents;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding), which\n pushed the label to the right of its column and added 12px around both. The\n class is doubled so these resets outrank that rule, and a dossiq-style\n `:deep()` override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__value.cn-facts-widget__value {\n\tmargin: 0;\n\tpadding: 0;\n\ttext-align: start;\n\tfont-weight: 500;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-facts-widget__title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__line {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__muted {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__label .hidden-visually {\n\tclip-path: inset(50%);\n\theight: 1px;\n\toverflow: hidden;\n\tposition: absolute;\n\twhite-space: nowrap;\n\twidth: 1px;\n}\n\n.cn-facts-widget__label--icon {\n\talign-items: center;\n\tdisplay: flex;\n}\n\n.cn-facts-widget__value-link {\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n.cn-facts-widget__inline-button,\n.cn-facts-widget__icon-button,\n.cn-facts-widget__action {\n\talign-items: center;\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n}\n\n.cn-facts-widget__inline-button {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: 16px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n}\n\n.cn-facts-widget__icon-button {\n\tbackground: transparent;\n\tborder: 0;\n\tborder-radius: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tmargin-inline-start: 4px;\n\tpadding: 2px;\n\tvertical-align: middle;\n}\n\n.cn-facts-widget__icon-button:hover,\n.cn-facts-widget__icon-button:focus-visible {\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-main-text);\n}\n\n.cn-facts-widget__text {\n\tcolor: var(--color-main-text);\n\tfont-size: 15px;\n}\n\n/* A lead: an icon tile or initials beside a bold title and a muted subtitle. */\n.cn-facts-widget__lead {\n\talign-items: center;\n\tdisplay: flex;\n\tgap: 12px;\n}\n\n.cn-facts-widget__lead-tile {\n\talign-items: center;\n\tbackground: var(--color-background-dark);\n\tborder-radius: 8px;\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: none;\n\tfont-size: 14px;\n\tfont-weight: 700;\n\theight: 40px;\n\tjustify-content: center;\n\twidth: 40px;\n}\n\n.cn-facts-widget__lead-tile--success {\n\tbackground: color-mix(in srgb, var(--color-success) 14%, var(--color-main-background));\n\tborder-radius: 50%;\n\tcolor: var(--color-success-text, var(--color-success));\n}\n\n.cn-facts-widget__lead-tile--info {\n\tbackground: var(--color-primary-element-light);\n\tborder-radius: 50%;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n}\n\n.cn-facts-widget__lead-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-width: 0;\n}\n\n.cn-facts-widget__lead-title {\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__lead-subtitle {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-facts-widget__action {\n\talign-self: flex-start;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n}\n\n.cn-facts-widget__action:hover,\n.cn-facts-widget__action:focus-visible {\n\tbackground: var(--color-primary-element-light-hover, var(--color-primary-element-light));\n}\n\n.cn-facts-widget__link {\n\tfont-size: 14px;\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n}\n\n/* `columns: 2` (board DqContact): two columns, the label above the value, a\n hairline under each fact; one column on a phone. */\n.cn-facts-widget__rows--columns {\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\tgap: 14px 24px;\n\tfont-size: 15px;\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__row {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 3px;\n\tmin-width: 0;\n\tpadding-bottom: 10px;\n\tborder-bottom: 1px solid var(--color-border);\n}\n\n.cn-facts-widget__rows--columns .cn-facts-widget__label.cn-facts-widget__label {\n\tfont-size: 13px;\n}\n\n@media (max-width: 640px) {\n\t.cn-facts-widget__rows--columns {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createVNode","_Fragment","_renderList","_normalizeClass","_createElementVNode","_createBlock","_createTextVNode","_resolveDynamicComponent"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnHeaderSearchLabel.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const LABEL_SELECTOR = '.unified-search-input__label';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Put the app's own line on the search field in Nextcloud's header.
|
|
5
|
+
*
|
|
6
|
+
* @spec openspec/changes/screens-board-top-bar-search/specs/layout-components/spec.md#requirement-the-board-look-names-what-the-header-search-finds
|
|
7
|
+
*/
|
|
8
|
+
var script = {
|
|
9
|
+
name: 'CnHeaderSearchLabel',
|
|
10
|
+
|
|
11
|
+
props: {
|
|
12
|
+
/**
|
|
13
|
+
* The line to show, already translated.
|
|
14
|
+
*
|
|
15
|
+
* @type {string}
|
|
16
|
+
*/
|
|
17
|
+
label: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
data() {
|
|
24
|
+
return { observer: null, original: null, labelNode: null }
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
watch: {
|
|
28
|
+
label() {
|
|
29
|
+
this.apply();
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
mounted() {
|
|
34
|
+
const header = document.getElementById('header');
|
|
35
|
+
if (!header) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
this.apply();
|
|
39
|
+
if (typeof MutationObserver === 'function') {
|
|
40
|
+
this.observer = new MutationObserver(() => this.apply());
|
|
41
|
+
this.observer.observe(header, { childList: true, subtree: true, characterData: true });
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
beforeUnmount() {
|
|
46
|
+
this.observer?.disconnect();
|
|
47
|
+
this.observer = null;
|
|
48
|
+
if (this.labelNode && this.original !== null && this.labelNode.isConnected) {
|
|
49
|
+
this.labelNode.textContent = this.original;
|
|
50
|
+
}
|
|
51
|
+
this.labelNode = null;
|
|
52
|
+
this.original = null;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
methods: {
|
|
56
|
+
/**
|
|
57
|
+
* Write the line into the label when it shows something else.
|
|
58
|
+
* Remembers the first text Nextcloud drew in each label node, to put back.
|
|
59
|
+
*
|
|
60
|
+
* @return {void}
|
|
61
|
+
*/
|
|
62
|
+
apply() {
|
|
63
|
+
const node = document.querySelector('#header ' + LABEL_SELECTOR);
|
|
64
|
+
if (!node || !this.label) {
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (node !== this.labelNode) {
|
|
68
|
+
this.labelNode = node;
|
|
69
|
+
this.original = node.textContent;
|
|
70
|
+
}
|
|
71
|
+
if (node.textContent.trim() === this.label) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
// Keep Nextcloud's own text node where there is one, so its renderer still holds the live node.
|
|
75
|
+
const text = node.childNodes.length === 1 && node.firstChild.nodeType === 3 ? node.firstChild : null;
|
|
76
|
+
if (text) {
|
|
77
|
+
text.data = this.label;
|
|
78
|
+
} else {
|
|
79
|
+
node.textContent = this.label;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
return null
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export { script as default };
|
|
90
|
+
//# sourceMappingURL=CnHeaderSearchLabel.vue2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CnHeaderSearchLabel.vue2.js","sources":["../../../../src/components/CnHeaderSearchLabel/CnHeaderSearchLabel.vue"],"sourcesContent":["<!--\n CnHeaderSearchLabel — the app's own words on the search field in Nextcloud's\n header (screens-board-top-bar-search). Renders nothing of its own.\n\n Under the board look the screens give the top bar's search field a line that\n names what the app holds (\"Zoek zaken, documenten of contacten\"), where\n Nextcloud shows its global \"Search apps, files, tags, messages …\". Nextcloud\n offers no way for an app to set that line, so this component replaces the\n TEXT of the field's label and nothing else:\n - it changes only the text of `.unified-search-input__label` (which is also\n the search button's accessible name); it never adds, moves or removes a node;\n - it writes the text again when Nextcloud re-renders the field, and only\n when the text differs, so its own write never loops;\n - on unmount it puts Nextcloud's text back;\n - with no header or no such label it does nothing.\n\n The search Nextcloud opens is unchanged: this is the resting field's line.\n-->\n<script>\nconst LABEL_SELECTOR = '.unified-search-input__label'\n\n/**\n * Put the app's own line on the search field in Nextcloud's header.\n *\n * @spec openspec/changes/screens-board-top-bar-search/specs/layout-components/spec.md#requirement-the-board-look-names-what-the-header-search-finds\n */\nexport default {\n\tname: 'CnHeaderSearchLabel',\n\n\tprops: {\n\t\t/**\n\t\t * The line to show, already translated.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn { observer: null, original: null, labelNode: null }\n\t},\n\n\twatch: {\n\t\tlabel() {\n\t\t\tthis.apply()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tconst header = document.getElementById('header')\n\t\tif (!header) {\n\t\t\treturn\n\t\t}\n\t\tthis.apply()\n\t\tif (typeof MutationObserver === 'function') {\n\t\t\tthis.observer = new MutationObserver(() => this.apply())\n\t\t\tthis.observer.observe(header, { childList: true, subtree: true, characterData: true })\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.observer?.disconnect()\n\t\tthis.observer = null\n\t\tif (this.labelNode && this.original !== null && this.labelNode.isConnected) {\n\t\t\tthis.labelNode.textContent = this.original\n\t\t}\n\t\tthis.labelNode = null\n\t\tthis.original = null\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Write the line into the label when it shows something else.\n\t\t * Remembers the first text Nextcloud drew in each label node, to put back.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tapply() {\n\t\t\tconst node = document.querySelector('#header ' + LABEL_SELECTOR)\n\t\t\tif (!node || !this.label) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (node !== this.labelNode) {\n\t\t\t\tthis.labelNode = node\n\t\t\t\tthis.original = node.textContent\n\t\t\t}\n\t\t\tif (node.textContent.trim() === this.label) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Keep Nextcloud's own text node where there is one, so its renderer still holds the live node.\n\t\t\tconst text = node.childNodes.length === 1 && node.firstChild.nodeType === 3 ? node.firstChild : null\n\t\t\tif (text) {\n\t\t\t\ttext.data = this.label\n\t\t\t} else {\n\t\t\t\tnode.textContent = this.label\n\t\t\t}\n\t\t},\n\t},\n\n\trender() {\n\t\treturn null\n\t},\n}\n</script>\n"],"names":[],"mappings":"AAmBA,MAAM,cAAe,GAAE,+BAA6B;;AAEpD;;AAEC;AACA;;AAED,aAAe;CACd,IAAI,EAAE,qBAAqB;;AAE3B,CAAA,KAAK,EAAE;AACN;AACC;AACA;;;AAGD,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;EACD;;AAED,CAAA,IAAI,GAAG;EACN,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAK,EAAA;EACzD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,KAAK,GAAG;GACP,IAAI,CAAC,KAAK,GAAC;GACX;EACD;;AAED,CAAA,OAAO,GAAG;AACT,EAAA,MAAM,SAAS,QAAQ,CAAC,cAAc,CAAC,QAAQ,EAAA;EAC/C,IAAI,CAAC,MAAM,EAAE;GACZ,MAAK;AACN,GAAA;EACA,IAAI,CAAC,KAAK,GAAC;AACX,EAAA,IAAI,OAAO,gBAAiB,KAAI,UAAU,EAAE;GAC3C,IAAI,CAAC,WAAW,IAAI,gBAAgB,CAAC,MAAM,IAAI,CAAC,KAAK,EAAE,EAAA;AACvD,GAAA,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAA;AACtF,GAAA;EACA;;AAED,CAAA,aAAa,GAAG;EACf,IAAI,CAAC,QAAQ,EAAE,UAAU,GAAC;AAC1B,EAAA,IAAI,CAAC,QAAO,GAAI,KAAG;AACnB,EAAA,IAAI,IAAI,CAAC,SAAU,IAAG,IAAI,CAAC,aAAa,IAAK,IAAG,IAAI,CAAC,SAAS,CAAC,WAAW,EAAE;AAC3E,GAAA,IAAI,CAAC,SAAS,CAAC,WAAU,GAAI,IAAI,CAAC,SAAO;AAC1C,GAAA;AACA,EAAA,IAAI,CAAC,SAAQ,GAAI,KAAG;AACpB,EAAA,IAAI,CAAC,QAAO,GAAI,KAAG;EACnB;;AAED,CAAA,OAAO,EAAE;AACR;;;AAGC;;;AAGD,EAAA,KAAK,GAAG;GACP,MAAM,IAAG,GAAI,QAAQ,CAAC,aAAa,CAAC,UAAW,GAAE,cAAc,EAAA;GAC/D,IAAI,CAAC,IAAG,IAAK,CAAC,IAAI,CAAC,KAAK,EAAE;IACzB,MAAK;AACN,IAAA;GACA,IAAI,IAAK,KAAI,IAAI,CAAC,SAAS,EAAE;AAC5B,IAAA,IAAI,CAAC,SAAQ,GAAI,KAAG;IACpB,IAAI,CAAC,QAAS,GAAE,IAAI,CAAC,YAAU;AAChC,IAAA;AACA,GAAA,IAAI,IAAI,CAAC,WAAW,CAAC,IAAI,EAAG,KAAI,IAAI,CAAC,KAAK,EAAE;IAC3C,MAAK;AACN,IAAA;AACA;AACA,GAAA,MAAM,IAAK,GAAE,IAAI,CAAC,UAAU,CAAC,MAAK,KAAM,CAAE,IAAG,IAAI,CAAC,UAAU,CAAC,QAAO,KAAM,CAAE,GAAE,IAAI,CAAC,aAAa,KAAG;AACnG,GAAA,IAAI,IAAI,EAAE;AACT,IAAA,IAAI,CAAC,IAAG,GAAI,IAAI,CAAC,MAAI;IACpB,MAAK;IACN,IAAI,CAAC,WAAU,GAAI,IAAI,CAAC,MAAI;AAC7B,IAAA;GACA;EACD;;AAED,CAAA,MAAM,GAAG;AACR,EAAA,OAAO,IAAG;EACV;AACF;;;;"}
|
|
@@ -467,6 +467,31 @@ var script = {
|
|
|
467
467
|
required: true,
|
|
468
468
|
},
|
|
469
469
|
|
|
470
|
+
/**
|
|
471
|
+
* A breadcrumb under the title (manifest `config.breadcrumb`): one
|
|
472
|
+
* crumb `{ label, route?, params?, href?, icon?, separator? }` or a
|
|
473
|
+
* list; the title follows as the current crumb. Drawn as the detail
|
|
474
|
+
* header's breadcrumb row. Null (the default) draws none.
|
|
475
|
+
*
|
|
476
|
+
* @type {object|Array<object>|null}
|
|
477
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
478
|
+
*/
|
|
479
|
+
breadcrumb: {
|
|
480
|
+
type: [Object, Array],
|
|
481
|
+
default: null,
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
|
|
486
|
+
* such as "{total} open": `{total}` and `{shown}` are the list's counts.
|
|
487
|
+
*
|
|
488
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
489
|
+
*/
|
|
490
|
+
headerMeta: {
|
|
491
|
+
type: String,
|
|
492
|
+
default: '',
|
|
493
|
+
},
|
|
494
|
+
|
|
470
495
|
/** Optional description shown below the title */
|
|
471
496
|
description: {
|
|
472
497
|
type: String,
|
|
@@ -1090,7 +1115,7 @@ var script = {
|
|
|
1090
1115
|
* can open is an ordinary segment; one it cannot (no board columns, no
|
|
1091
1116
|
* map configured) is drawn disabled, named "<mode>: not available on
|
|
1092
1117
|
* this list". Fed from `pages[].config.viewSwitch`. Ignored without the
|
|
1093
|
-
* board look
|
|
1118
|
+
* board look; absent, the board look draws all four.
|
|
1094
1119
|
*
|
|
1095
1120
|
* @type {Array<'table' | 'cards' | 'board' | 'map'>}
|
|
1096
1121
|
* @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
|
|
@@ -2413,6 +2438,8 @@ var script = {
|
|
|
2413
2438
|
data() {
|
|
2414
2439
|
return {
|
|
2415
2440
|
currentViewMode: this.viewMode,
|
|
2441
|
+
/** Whether `?_view=` names a mode the page did not offer yet, to open once it does. */
|
|
2442
|
+
routeViewPending: false,
|
|
2416
2443
|
/** The column field the reader picked in the board's column switch; '' = the default. */
|
|
2417
2444
|
boardColumnFieldChoice: '',
|
|
2418
2445
|
/** The rows a reference column field is built from, by field. */
|
|
@@ -2976,11 +3003,14 @@ var script = {
|
|
|
2976
3003
|
* @spec openspec/changes/screens-view-switch-parity/specs/view-switch-board-look/spec.md#requirement-the-manifest-can-draw-segments-the-page-cannot-open
|
|
2977
3004
|
*/
|
|
2978
3005
|
viewSwitchDisabledModes() {
|
|
2979
|
-
if (!this.isBoardLook
|
|
3006
|
+
if (!this.isBoardLook) {
|
|
2980
3007
|
return []
|
|
2981
3008
|
}
|
|
3009
|
+
// The index boards draw all four segments, so the board look does
|
|
3010
|
+
// too unless the manifest names its own list.
|
|
3011
|
+
const listed = Array.isArray(this.viewSwitch) ? this.viewSwitch : ['table', 'cards', 'board', 'map'];
|
|
2982
3012
|
const offered = [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])];
|
|
2983
|
-
return
|
|
3013
|
+
return listed.filter((mode) => ['table', 'cards', 'board', 'map'].includes(mode) && !offered.includes(mode))
|
|
2984
3014
|
},
|
|
2985
3015
|
|
|
2986
3016
|
/**
|
|
@@ -3018,6 +3048,17 @@ var script = {
|
|
|
3018
3048
|
})
|
|
3019
3049
|
},
|
|
3020
3050
|
|
|
3051
|
+
/**
|
|
3052
|
+
* The modes `?_view=` can open: the toggle's modes plus the map when
|
|
3053
|
+
* the page offers it.
|
|
3054
|
+
*
|
|
3055
|
+
* @return {Array<string>} The modes.
|
|
3056
|
+
* @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
|
|
3057
|
+
*/
|
|
3058
|
+
routeViewModes() {
|
|
3059
|
+
return [...this.effectiveToggleModes, ...(this.showMapSegment ? ['map'] : [])]
|
|
3060
|
+
},
|
|
3061
|
+
|
|
3021
3062
|
/**
|
|
3022
3063
|
* The column fields the board offers (`board.columnFields`), for the
|
|
3023
3064
|
* column switch (screens-board-reference-columns).
|
|
@@ -4238,6 +4279,21 @@ var script = {
|
|
|
4238
4279
|
return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
|
|
4239
4280
|
},
|
|
4240
4281
|
|
|
4282
|
+
/**
|
|
4283
|
+
* Whether the table marks unread rows. The boards draw every row alike,
|
|
4284
|
+
* so under the board look only a page that offers the Unread lens
|
|
4285
|
+
* (`personalLenses`) marks them; the Nextcloud look always does.
|
|
4286
|
+
*
|
|
4287
|
+
* @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-unread-rows-are-marked-only-where-the-reader-asked-for-them
|
|
4288
|
+
* @return {boolean} True when unread rows read in bold with the dot.
|
|
4289
|
+
*/
|
|
4290
|
+
showUnreadRows() {
|
|
4291
|
+
if (!this.isBoardLook) {
|
|
4292
|
+
return true
|
|
4293
|
+
}
|
|
4294
|
+
return Array.isArray(this.personalLenses) && this.personalLenses.includes('unread')
|
|
4295
|
+
},
|
|
4296
|
+
|
|
4241
4297
|
/** Resolved schema OBJECT (for column generation / icons / labels). */
|
|
4242
4298
|
/**
|
|
4243
4299
|
* Whether the table shows its header filters. A self-fetching page
|
|
@@ -4860,8 +4916,9 @@ var script = {
|
|
|
4860
4916
|
}
|
|
4861
4917
|
// The board's header: Export reads "Download" with its icon and
|
|
4862
4918
|
// the Actions menu reads "Actions", without the manifest saying so.
|
|
4863
|
-
|
|
4864
|
-
|
|
4919
|
+
// A labelled Export keeps the icon too, unless it names its own.
|
|
4920
|
+
if (this.isBoardLook && button.action === 'export') {
|
|
4921
|
+
label = label || translate('nextcloud-vue', 'Download');
|
|
4865
4922
|
icon = icon || 'Download';
|
|
4866
4923
|
}
|
|
4867
4924
|
if (label === '' && button.action === 'actions-menu') {
|
|
@@ -5144,6 +5201,22 @@ var script = {
|
|
|
5144
5201
|
* @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-the-count-reads-in-the-users-language
|
|
5145
5202
|
* @return {string}
|
|
5146
5203
|
*/
|
|
5204
|
+
/**
|
|
5205
|
+
* The header meta note with the list's counts filled in.
|
|
5206
|
+
*
|
|
5207
|
+
* @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
|
|
5208
|
+
* @return {string}
|
|
5209
|
+
*/
|
|
5210
|
+
headerMetaText() {
|
|
5211
|
+
if (!this.headerMeta) {
|
|
5212
|
+
return ''
|
|
5213
|
+
}
|
|
5214
|
+
const total = this.effectivePagination?.total;
|
|
5215
|
+
return this.cnTranslate(this.headerMeta)
|
|
5216
|
+
.replace('{shown}', String(this.effectiveObjects.length))
|
|
5217
|
+
.replace('{total}', typeof total === 'number' ? String(total) : '')
|
|
5218
|
+
},
|
|
5219
|
+
|
|
5147
5220
|
headerDescription() {
|
|
5148
5221
|
const total = this.effectivePagination?.total;
|
|
5149
5222
|
// Board look: the count line is a template over the rows on this page
|
|
@@ -5334,6 +5407,12 @@ var script = {
|
|
|
5334
5407
|
this.applyViewModeFromRoute();
|
|
5335
5408
|
},
|
|
5336
5409
|
|
|
5410
|
+
routeViewModes() {
|
|
5411
|
+
if (this.routeViewPending) {
|
|
5412
|
+
this.applyViewModeFromRoute();
|
|
5413
|
+
}
|
|
5414
|
+
},
|
|
5415
|
+
|
|
5337
5416
|
boardFetchKey: {
|
|
5338
5417
|
immediate: true,
|
|
5339
5418
|
handler(key) {
|
|
@@ -5766,6 +5845,10 @@ var script = {
|
|
|
5766
5845
|
*/
|
|
5767
5846
|
async loadFilterCounts() {
|
|
5768
5847
|
const requestKey = this.countRequestKey;
|
|
5848
|
+
if (this.isNamedSource) {
|
|
5849
|
+
await this.loadNamedSourceCounts(requestKey);
|
|
5850
|
+
return
|
|
5851
|
+
}
|
|
5769
5852
|
const schema = this.exportSchemaSlug;
|
|
5770
5853
|
if (requestKey === '' || !this.register || !schema) {
|
|
5771
5854
|
this.tabCounts = null;
|
|
@@ -5797,6 +5880,46 @@ var script = {
|
|
|
5797
5880
|
this.savedViewCounts = viewCounts;
|
|
5798
5881
|
},
|
|
5799
5882
|
|
|
5883
|
+
/**
|
|
5884
|
+
* The quick-filter counts of a named source (`entitySource`): each tab
|
|
5885
|
+
* that asks is counted by the source's own `count(config)`, with the
|
|
5886
|
+
* tab's filter over `sourceConfig`. A source without `count` shows
|
|
5887
|
+
* none; a failed count leaves that tab without a number.
|
|
5888
|
+
*
|
|
5889
|
+
* @param {string} requestKey The `countRequestKey` this load answers.
|
|
5890
|
+
* @return {Promise<void>}
|
|
5891
|
+
* @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-a-named-source-counts-its-quick-filters
|
|
5892
|
+
*/
|
|
5893
|
+
async loadNamedSourceCounts(requestKey) {
|
|
5894
|
+
const count = this.namedSource ? this.namedSource.count : null;
|
|
5895
|
+
if (requestKey === '' || typeof count !== 'function') {
|
|
5896
|
+
this.tabCounts = null;
|
|
5897
|
+
this.savedViewCounts = null;
|
|
5898
|
+
return
|
|
5899
|
+
}
|
|
5900
|
+
const params = (this.$route && this.$route.params) || {};
|
|
5901
|
+
const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {};
|
|
5902
|
+
const tabCounts = {};
|
|
5903
|
+
await Promise.all(this.countEntries
|
|
5904
|
+
.filter((entry) => entry.key.startsWith('tab:'))
|
|
5905
|
+
.map(async (entry) => {
|
|
5906
|
+
const config = resolveFilterMap({ ...(this.sourceConfig || {}), ...(entry.filter || {}) }, params, ctx);
|
|
5907
|
+
try {
|
|
5908
|
+
const total = await count(config);
|
|
5909
|
+
if (Number.isFinite(total)) {
|
|
5910
|
+
tabCounts[Number(entry.key.slice(4))] = total;
|
|
5911
|
+
}
|
|
5912
|
+
} catch {
|
|
5913
|
+
// No number for this tab.
|
|
5914
|
+
}
|
|
5915
|
+
}));
|
|
5916
|
+
if (requestKey !== this.countRequestKey) {
|
|
5917
|
+
return
|
|
5918
|
+
}
|
|
5919
|
+
this.tabCounts = tabCounts;
|
|
5920
|
+
this.savedViewCounts = null;
|
|
5921
|
+
},
|
|
5922
|
+
|
|
5800
5923
|
// ── Split view (case-page-and-list-as-a-place) ──────────────────
|
|
5801
5924
|
|
|
5802
5925
|
/**
|
|
@@ -8061,6 +8184,7 @@ var script = {
|
|
|
8061
8184
|
* @param {string} mode 'table' or 'cards'
|
|
8062
8185
|
*/
|
|
8063
8186
|
onViewModeChange(mode) {
|
|
8187
|
+
this.routeViewPending = false;
|
|
8064
8188
|
this.currentViewMode = mode;
|
|
8065
8189
|
this.$emit('view-mode-change', mode);
|
|
8066
8190
|
},
|
|
@@ -8070,16 +8194,26 @@ var script = {
|
|
|
8070
8194
|
* offers it. The key starts with `_`, like `_search` and `_page`, so it
|
|
8071
8195
|
* is never read as a list filter. A menu entry's `query` can so open a
|
|
8072
8196
|
* list as its board, on the one page that lists the schema
|
|
8073
|
-
* (screens-board-reference-columns).
|
|
8197
|
+
* (screens-board-reference-columns). A mode the page offers only later
|
|
8198
|
+
* (the map, once rows with a geometry are in) opens when it arrives,
|
|
8199
|
+
* unless the reader picked a view first.
|
|
8074
8200
|
*
|
|
8075
8201
|
* @return {void}
|
|
8076
8202
|
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-the-address-can-open-a-list-in-a-view-mode
|
|
8203
|
+
* @spec openspec/changes/screens-index-board-gaps/specs/index-list-board-look/spec.md#requirement-the-address-opens-a-view-mode-that-arrives-later
|
|
8077
8204
|
*/
|
|
8078
8205
|
applyViewModeFromRoute() {
|
|
8079
8206
|
const wanted = this.$route?.query?._view;
|
|
8080
|
-
if (typeof wanted
|
|
8207
|
+
if (typeof wanted !== 'string' || wanted === '' || wanted === this.currentViewMode) {
|
|
8208
|
+
this.routeViewPending = false;
|
|
8209
|
+
return
|
|
8210
|
+
}
|
|
8211
|
+
if (this.routeViewModes.includes(wanted)) {
|
|
8081
8212
|
this.currentViewMode = wanted;
|
|
8213
|
+
this.routeViewPending = false;
|
|
8214
|
+
return
|
|
8082
8215
|
}
|
|
8216
|
+
this.routeViewPending = true;
|
|
8083
8217
|
},
|
|
8084
8218
|
|
|
8085
8219
|
/**
|