@conduction/nextcloud-vue 2.147.0 → 2.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +71 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +8 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +3 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +26 -15
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -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 +160 -126
- package/dist/esm/utils/builtInFormatters.js +53 -1
- package/dist/esm/utils/builtInFormatters.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 +1357 -1217
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +160 -126
- package/l10n/en.json +2 -1
- package/l10n/nl.json +2 -1
- package/package.json +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +73 -10
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
- package/src/components/CnStepper/CnStepper.vue +10 -2
- package/src/css/dialog.css +5 -2
- package/src/css/look-board-index.css +14 -0
- package/src/css/look-board.css +3 -0
- package/src/dialogs/CnAccessPurposeDialog.vue +27 -12
- package/src/schemas/app-manifest-v2.schema.json +4 -2
- package/src/utils/builtInFormatters.js +52 -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=6351bc64&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\"\n\t\t\tclass=\"cn-facts-widget__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-facts-error\">\n\t\t\t{{ errorLabel }}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\tdata-testid=\"cn-facts-retry\"\n\t\t\t\t@click=\"load\">\n\t\t\t\t{{ retryLabel }}\n\t\t\t</button>\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<div v-else-if=\"blocks.length === 0 && showsEmpty\" class=\"cn-facts-widget__empty\">\n\t\t\t<p class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</p>\n\t\t\t<button\n\t\t\t\tv-for=\"(entry, index) in emptyActions\"\n\t\t\t\t:key=\"`empty-action-${index}`\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\t\tdata-testid=\"cn-facts-empty-action\"\n\t\t\t\t@click=\"runAction(entry.action)\">\n\t\t\t\t<CnIcon v-if=\"entry.icon\" :name=\"entry.icon\" :size=\"16\" />\n\t\t\t\t{{ entry.label }}\n\t\t\t</button>\n\t\t</div>\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, 'cn-facts-widget__rows--stacked': stacked }\">\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:class=\"{ 'cn-facts-widget__row--inline': row.inline }\"\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<router-link\n\t\t\t\t\t\t\t\tv-else-if=\"row.to\"\n\t\t\t\t\t\t\t\t:to=\"row.to\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\">\n\t\t\t\t\t\t\t\t{{ shownValue(row) }}\n\t\t\t\t\t\t\t</router-link>\n\t\t\t\t\t\t\t<span v-else>{{ shownValue(row) }}</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:disabled=\"revealing === 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<span\n\t\t\t\t\t\t\t\tv-if=\"revealErrors[row.key]\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__reveal-error\"\n\t\t\t\t\t\t\t\trole=\"alert\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-error-${row.key}`\">{{ revealErrors[row.key] }}</span>\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}`, { 'cn-facts-widget__lead-tile--initials': !block.icon }]\"\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\n\t\t\t\t\tv-else-if=\"block.kind === 'meter'\"\n\t\t\t\t\tclass=\"cn-facts-widget__meter\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__meter--over': block.over }\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<div class=\"cn-facts-widget__meter-head\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-label\">{{ block.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-value\">{{ block.value }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-if=\"block.limit !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"meter\"\n\t\t\t\t\t\t:aria-label=\"block.label\"\n\t\t\t\t\t\t:aria-valuenow=\"block.amount\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t:aria-valuemax=\"block.limit\"\n\t\t\t\t\t\t:aria-valuetext=\"block.value\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'progress'\"\n\t\t\t\t\tclass=\"cn-facts-widget__progress\"\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\tv-if=\"block.percent !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\t\t:aria-label=\"block.label || null\"\n\t\t\t\t\t\t:aria-valuenow=\"block.percent\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\taria-valuemax=\"100\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__line\">{{ block.value }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pills'\" class=\"cn-facts-widget__pills\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-for=\"pill in block.pills\"\n\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t:label=\"pill.value\"\n\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${pill.key}`\" />\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<NcNoteCard\n\t\t\tv-if=\"notice && record\"\n\t\t\t:type=\"notice.type\"\n\t\t\tclass=\"cn-facts-widget__notice\"\n\t\t\tdata-testid=\"cn-facts-notice\">\n\t\t\t{{ notice.text }}\n\t\t</NcNoteCard>\n\t\t<div\n\t\t\tv-if=\"footnote && record\"\n\t\t\tclass=\"cn-facts-widget__footnote\"\n\t\t\tdata-testid=\"cn-facts-footnote\">\n\t\t\t{{ footnote }}\n\t\t</div>\n\t\t<button\n\t\t\tv-if=\"link && link.sidebarTab && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__link cn-facts-widget__link--button\"\n\t\t\tdata-testid=\"cn-facts-link\"\n\t\t\t@click=\"openSidebarTab(link.sidebarTab)\">\n\t\t\t<CnIcon v-if=\"link.icon\" :name=\"link.icon\" :size=\"18\" />\n\t\t\t{{ link.label }}\n\t\t</button>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-else-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\t:target=\"link.target || undefined\"\n\t\t\t:rel=\"link.target === '_blank' ? 'noopener noreferrer' : undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t<CnIcon v-if=\"link.icon\" :name=\"link.icon\" :size=\"18\" />\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\t<CnActionButtons\n\t\t\tv-if=\"drawnActions.length > 0\"\n\t\t\tclass=\"cn-facts-widget__actions\"\n\t\t\t:actions=\"drawnActions\"\n\t\t\tdata-testid=\"cn-facts-actions\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { formatFileSize } from '@nextcloud/files'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcLoadingIcon, NcNoteCard } 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 CnActionButtons from '../CnActionButtons/CnActionButtons.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { fetchEndpointSource, 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 { conditionalLine } from '../../utils/conditionalLine.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { fillTemplate, formatLongDate } from '../../utils/fillTemplate.js'\nimport { formatMoney, metricLocale, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\nimport { evaluateVisibleWhen, evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\n\n/** Event-bus channel of the page-level Refresh, which an api-call or a saved form also sends. */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * Replace every `@source.<field>` in a declaration with the field of the\n * record shown, deeply.\n *\n * @param {unknown} value The declaration.\n * @param {object} record The record shown.\n * @return {{value: unknown, unresolved: boolean}} The resolved copy, and whether a token named an empty field.\n * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n */\nfunction resolveSourceTokens(value, record) {\n\tlet unresolved = false\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\treturn node.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)\n\t\t\t\tif (read === undefined || read === null || read === '' || typeof read === 'object') {\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}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\treturn Object.fromEntries(Object.entries(node).map(([key, entry]) => [key, walk(entry)]))\n\t\t}\n\t\treturn node\n\t}\n\tconst resolved = walk(value)\n\treturn { value: resolved, unresolved }\n}\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\n/**\n * Two or more pills in a row become one `pills` block, drawn on one line.\n *\n * @param {Array<object>} blocks The blocks in order.\n * @return {Array<object>}\n * @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context\n */\nfunction groupPills(blocks) {\n\tconst out = []\n\tblocks.forEach((block) => {\n\t\tconst last = out[out.length - 1]\n\t\tif (block.kind !== 'pill') {\n\t\t\tout.push(block)\n\t\t} else if (last && last.kind === 'pills') {\n\t\t\tlast.pills.push(block)\n\t\t} else if (last && last.kind === 'pill') {\n\t\t\tout[out.length - 1] = { kind: 'pills', key: `pills-${last.key}`, pills: [last, block] }\n\t\t} else {\n\t\t\tout.push(block)\n\t\t}\n\t})\n\treturn out\n}\n\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text', 'meter', 'progress'])\n\n/** The schemes a facts link opens, besides a path on this server. */\nconst LINK_SCHEMES = ['http:', 'https:']\n\n/**\n * A value read as an absolute URL, or null when it is not one (a page path).\n *\n * @param {string} value The value.\n * @return {URL|null}\n */\nfunction absoluteUrl(value) {\n\ttry {\n\t\treturn new URL(value)\n\t} catch {\n\t\treturn null\n\t}\n}\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, CnActionButtons, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon, NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\t/** The detail page's newest change of the record (`recordLastChange`), read as `_lastChange.*`. */\n\t\tcnRecordLastChange: { 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\t/** The page's object sidebar channel; a `link.sidebarTab` opens one of its tabs. */\n\t\tobjectSidebarState: { default: null },\n\t\t/** The records the detail page reads beside the record (`@context.<name>`), for a `labelPath`. */\n\t\tcnDetailVisibilityRows: { 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?, inline?, 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`), `pill` or `meter`.\n\t\t * `countFrom` (`{ register, schema, filter? }`, `@objectId` / `@object.<field>` in the filter) shows how many records that list holds (\"Lopende zaken 43\"); the row is left out while the count cannot be read.\n\t\t * A `meter` shows the amount at `path` against the limit at `limitPath` (or a constant `limit`) as \"12 of 50\" with a bar; past the limit the bar turns red and the text says \"over the limit\"; without a positive limit it shows the amount and no bar.\n\t\t * A row with `inline: true` spans both columns as one line, the label, the value and its buttons side by side (a masked BSN with Show).\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, which unmasks the record's value; `reveal: { url, path? }` makes Show ask the server for the value at `url` (tokens resolved), read at `path` (else the entry's path), so a server that answers the mask can check and record the reveal). `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 * `route` (`{ name, params?, query? }`, `@source.<path>` in a param) on a `title` or a `row` links its value to that route; a param that does not fill leaves it plain.\n\t\t * `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.\n\t\t * `template` (\"{available} {unit}\") fills its `{path}` placeholders from the record instead of reading `path` (`{path:format}` formats one, \"{from:year}\"); it is empty when every placeholder is. `suffix` (\" kg\") is added to a value that is not empty, its words translated.\n\t\t * `labelPath` (\"@context.caseType.handlerLabel\") names a value on the record or the page's `@context` records that labels the row when it holds text; `label` is the fallback.\n\t\t * `emptyValue` (\"No\") is shown, translated, when the entry has no value, so the entry stays drawn whatever `hideEmpty` says (an override that was never set reads \"No\").\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 * - `actions` — action buttons under the facts in the `headerActions` shape (`api-call`, `open-form`, `navigate`, `open-modal`, ...; `confirm`, `variant`). `@source.<field>` names a field of the record shown, and an action whose token names an empty field is not drawn. `visibleWhen` reads the record shown; `adminOnly: true` draws the action for an instance admin only (visibility, never authorisation). An api-call or a saved form refreshes the page, and the facts read their source again.\n\t\t * - `fieldsFrom` — `@context.<name>.<path>`, a list of entries on a record the page reads beside this one (a case type's `requestFields`); when it holds entries they are drawn instead of `fields`.\n\t\t * - `rowsPath` — a path to a list of `{ label, value }` the record holds, drawn as rows after `fields` (a preview the server builds).\n\t\t * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar \"Advies ontvangen\"); nothing shows while the value is empty.\n\t\t * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.\n\t\t * - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten \"Na het ondertekenen ...\"); the text is translated.\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: \"3 oktober\"), `year` (\"2026\"), `list` (an array joined by \", \", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, \"1.5 MB\"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is \"20 jaar\") 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. `{ register, schema, filter, sort? }` without `id` reads the first object that matches `filter` in `sort` order (`{ field, dir }`): the latest count of a shift. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.\n\t\t * - `emptyWhen` — a path; when the record's value there is truthy the card shows `emptyText` (an endpoint that answers `empty: true`).\n\t\t * - `link` — `{ label, route? | href? }` at the foot. `{ label, icon?, sidebarTab }` instead opens the page's object sidebar on that tab (no link without a sidebar).\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). `1` stacks each label above its value in one column, for a narrow card with long labels (board DqHandhaving \"Afwijking en definitief\").\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\trevealedValues: {},\n\t\t\trevealErrors: {},\n\t\t\trevealing: '',\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\t/** The `countFrom` totals read so far, by `countKey`. */\n\t\t\tcounts: {},\n\t\t\t/** The outcome of each `actions[]` condition that needs a request, by action id. */\n\t\t\tremoteVisibility: {},\n\t\t\t/** Sequence of the source reads, so an older answer never replaces a newer one. */\n\t\t\tloadSeq: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The entries to draw: the list `fieldsFrom` names on the page's\n\t\t * `@context` records when it holds entries (a case type's\n\t\t * `requestFields`), else `fields`.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context\n\t\t */\n\t\tentries() {\n\t\t\tconst from = this.content?.fieldsFrom\n\t\t\tif (typeof from === 'string' && from.startsWith('@context.')) {\n\t\t\t\tconst rows = this.cnDetailVisibilityRows && typeof this.cnDetailVisibilityRows === 'object' && 'value' in this.cnDetailVisibilityRows\n\t\t\t\t\t? this.cnDetailVisibilityRows.value\n\t\t\t\t\t: this.cnDetailVisibilityRows\n\t\t\t\tconst named = readPath({ '@context': rows || {} }, from)\n\t\t\t\tconst list = Array.isArray(named) ? named.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)) : []\n\t\t\t\tif (list.length > 0) {\n\t\t\t\t\treturn list\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t},\n\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 * Whether the rows stack in one column, label above value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tstacked() {\n\t\t\treturn Number(this.content?.columns) === 1\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 entries' `countFrom` lists (`{ register, schema, filter? }`), each\n\t\t * once, keyed by what they count on this record.\n\t\t *\n\t\t * @return {string} A stable key of the lists to count.\n\t\t * @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list\n\t\t */\n\t\tcountJobsKey() {\n\t\t\tconst fields = this.entries\n\t\t\tconst keys = fields\n\t\t\t\t.filter((field) => field && field.countFrom && typeof field.countFrom === 'object' && field.countFrom.register && field.countFrom.schema)\n\t\t\t\t.map((field) => this.countKey(field.countFrom))\n\t\t\treturn JSON.stringify([...new Set(keys)])\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\tsourceKey() {\n\t\t\tif (this.endpointRequest) {\n\t\t\t\treturn JSON.stringify(this.endpointRequest)\n\t\t\t}\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The `endpointSource` request with its tokens resolved, or null without one.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\tendpointRequest() {\n\t\t\tconst config = this.content?.endpointSource\n\t\t\tif (!config || typeof config !== 'object' || typeof config.url !== 'string' || config.url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveEndpointRequest(config, this.tokenCtx)\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\trecord() {\n\t\t\tlet record = this.endpointRequest || this.resolvedSource ? this.fetched : this.objectData\n\t\t\tconst provided = this.cnRecordLastChange\n\t\t\tconst lastChange = provided && typeof provided === 'object' && 'value' in provided ? provided.value : null\n\t\t\tif (record && record === this.objectData && lastChange) {\n\t\t\t\trecord = { ...record, _lastChange: lastChange }\n\t\t\t}\n\t\t\tconst path = this.content?.source?.path\n\t\t\tif (this.resolvedSource && !this.endpointRequest && typeof path === 'string' && path !== '' && record) {\n\t\t\t\tconst inner = readPath(record, path)\n\t\t\t\trecord = inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : null\n\t\t\t}\n\t\t\tconst emptyWhen = this.content?.emptyWhen\n\t\t\tif (record && typeof emptyWhen === 'string' && emptyWhen !== '' && readPath(record, emptyWhen)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn record\n\t\t},\n\n\t\t/**\n\t\t * Whether the empty line shows: when the card declares facts or actions\n\t\t * for the empty case. A card that is only a link says nothing without one.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\tshowsEmpty() {\n\t\t\tconst fields = this.content?.fields\n\t\t\treturn (Array.isArray(fields) && fields.length > 0) || this.emptyActions.length > 0 || (typeof this.content?.emptyText === 'string' && this.content.emptyText !== '')\n\t\t},\n\n\t\t/**\n\t\t * The actions under the empty text (`emptyActions`), as drawn.\n\t\t *\n\t\t * @return {Array<{label: string, icon: string, action: object}>}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\temptyActions() {\n\t\t\tconst list = Array.isArray(this.content?.emptyActions) ? this.content.emptyActions : []\n\t\t\treturn list\n\t\t\t\t.map((action) => ({ view: this.actionView(action), action }))\n\t\t\t\t.filter((entry) => entry.view)\n\t\t\t\t.map((entry) => ({ ...entry.view, action: entry.action }))\n\t\t},\n\n\t\t/** @return {string} The line shown when the source could not be read. */\n\t\terrorLabel() {\n\t\t\treturn this.tr(this.content?.errorText || 'Could not load this information')\n\t\t},\n\n\t\t/** @return {string} The retry button's text. */\n\t\tretryLabel() {\n\t\t\treturn t('nextcloud-vue', 'Try again')\n\t\t},\n\n\t\t/**\n\t\t * The `actions[]` to draw: `@source.<field>` resolved against the record\n\t\t * shown, `adminOnly` and `visibleWhen` settled here, so the buttons do\n\t\t * not judge them again against the page's object.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tdrawnActions() {\n\t\t\tconst record = this.record\n\t\t\tconst actions = Array.isArray(this.content?.actions) ? this.content.actions : []\n\t\t\tif (!record || actions.length === 0) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst admin = this.viewerIsAdmin()\n\t\t\tconst out = []\n\t\t\tactions.forEach((action, index) => {\n\t\t\t\tif (!action || typeof action !== 'object' || (action.adminOnly === true && !admin)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst id = this.actionId(action, index)\n\t\t\t\tconst condition = action.visibleWhen\n\t\t\t\tif (condition && isLocallyDecidableVisibleWhen(condition) && !evaluateVisibleWhenLocal(condition, record)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (condition && !isLocallyDecidableVisibleWhen(condition) && this.remoteVisibility[id] !== true) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst { value, unresolved } = resolveSourceTokens(action, record)\n\t\t\t\tif (unresolved) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst { visibleWhen, adminOnly, ...drawn } = value\n\t\t\t\tout.push({ ...drawn, id })\n\t\t\t})\n\t\t\treturn out\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-draw-the-rows-the-source-answers\n\t\t * @spec openspec/changes/facts-from-object/specs/facts-widget/spec.md#requirement-an-entry-can-read-the-page-object-on-a-sourced-card\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tconst fields = this.entries\n\t\t\t// A source that finds nothing still leaves the entries that read the\n\t\t\t// page's own object (`from: \"object\"`).\n\t\t\tconst ownEntries = Boolean(this.objectData) && fields.some((field) => field && field.from === 'object')\n\t\t\tif (!sourceRecord && !ownEntries) {\n\t\t\t\treturn []\n\t\t\t}\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\tif (!record) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (field.when && !evaluateVisibleWhenLocal(field.when, record)) {\n\t\t\t\t\treturn\n\t\t\t\t}\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.labelOf(record, field), value, inline: field.inline === true, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (field.route && !row.href) {\n\t\t\t\t\t\trow.to = this.resolveRoute(field.route, record)\n\t\t\t\t\t}\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 === 'meter') {\n\t\t\t\t\tObject.assign(block, this.meterOf(record, field))\n\t\t\t\t}\n\t\t\t\tif (kind === 'progress') {\n\t\t\t\t\tObject.assign(block, this.progressOf(record, field))\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\t// A boolean looks its tone up as \"true\" / \"false\", as `map` reads it.\n\t\t\t\t\tconst hit = colours ? keys.map((candidate) => (typeof candidate === 'boolean' ? String(candidate) : candidate)).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\tthis.answeredRows(sourceRecord, hideEmpty).forEach((row) => {\n\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t} else {\n\t\t\t\t\tout.push({ kind: 'rows', key: 'rows-answered', rows: [row] })\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groupPills(out)\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null. A `sidebarTab` link needs the\n\t\t * page's object sidebar and is null without one.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string, sidebarTab?: string}|null}\n\t\t * @spec openspec/changes/facts-link-opens-sidebar-tab/specs/record-widgets/spec.md#requirement-a-facts-link-can-open-a-sidebar-tab\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 (typeof link.sidebarTab === 'string' && link.sidebarTab !== '') {\n\t\t\t\treturn this.objectSidebarState\n\t\t\t\t\t? { label: this.tr(link.label), sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }\n\t\t\t\t\t: 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.linkHref(link, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : 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/**\n\t\t * The card header's status pill, read from the record like a `pill`\n\t\t * entry, or null when it has no text.\n\t\t *\n\t\t * @return {{label: string, variant: string}|null}\n\t\t */\n\t\theaderPill() {\n\t\t\tconst pill = this.content?.headerPill\n\t\t\tif (!pill || typeof pill !== 'object' || !this.record) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(this.record, pill.path) : undefined\n\t\t\tconst mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : undefined\n\t\t\tconst text = mapped ?? pill.text ?? raw\n\t\t\tif (typeof text !== 'string' && typeof text !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.tr(String(text))\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst colours = pill.variantMap || pill.colorMap\n\t\t\tconst hit = colours && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(colours, raw) ? colours[raw] : pill.variant\n\t\t\treturn { label, variant: isBadgeVariant(hit) ? hit : 'default' }\n\t\t},\n\n\t\t/**\n\t\t * The note band under the facts: its text translated, its NcNoteCard\n\t\t * type from `variant` (info by default), or null without text.\n\t\t *\n\t\t * @return {{text: string, type: string}|null}\n\t\t */\n\t\tnotice() {\n\t\t\tconst notice = this.content?.notice\n\t\t\tconst text = notice && typeof notice.text === 'string' ? this.tr(notice.text) : ''\n\t\t\tif (text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }\n\t\t},\n\n\t\t/** @return {string} The translated footnote, or '' when none is set. */\n\t\tfootnote() {\n\t\t\tconst footnote = this.content?.footnote\n\t\t\treturn typeof footnote === 'string' && footnote.trim() !== '' ? this.tr(footnote) : ''\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\tcountJobsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadCounts()\n\t\t\t},\n\t\t},\n\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\t/**\n\t\t\t * Read the labels and settle the action conditions of the record shown.\n\t\t\t *\n\t\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t\t */\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t\tthis.evaluateRemoteVisibility()\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\n\t\theaderPill: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onPageRefresh)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onPageRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Open the page's object sidebar on one of its tabs.\n\t\t *\n\t\t * @param {string} tab The sidebar tab id.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/facts-link-opens-sidebar-tab/specs/record-widgets/spec.md#requirement-a-facts-link-can-open-a-sidebar-tab\n\t\t */\n\t\topenSidebarTab(tab) {\n\t\t\tthis.objectSidebarState.open = true\n\t\t\tthis.objectSidebarState.requestedTab = tab\n\t\t},\n\n\t\t/**\n\t\t * The page asked every widget to read again (a dialog that changed what\n\t\t * the source answers, the page's Refresh): read the source again.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\tonPageRefresh() {\n\t\t\tif (this.resolvedSource || this.endpointRequest) {\n\t\t\t\tthis.load(true)\n\t\t\t}\n\t\t},\n\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 * A progress entry: the percentage at `path` (0 to 100) drawn as a bar\n\t\t * when it is a number, and the sentence that says what it means\n\t\t * (`lines`, the first that applies; `conditionalLine`), else the\n\t\t * percentage itself.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, percent: (number|null)}}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-show-progress\n\t\t */\n\t\tprogressOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\tconst number = raw === null || raw === undefined || raw === '' ? NaN : Number(raw)\n\t\t\tconst barWhen = field.barWhen ? evaluateVisibleWhenLocal(field.barWhen, record) : true\n\t\t\tconst percent = Number.isFinite(number) && barWhen ? Math.max(0, Math.min(100, number)) : null\n\t\t\tconst line = field.lines ? conditionalLine(field.lines, record, (text) => this.tr(text)) : ''\n\t\t\treturn {\n\t\t\t\tlabel: this.tr(field.label || ''),\n\t\t\t\tvalue: line || (percent !== null ? `${percent}%` : ''),\n\t\t\t\tpercent,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A link's href: `{field}` tokens filled from the record (and the\n\t\t * `@source.<field>` grammar), a value that is not a URL put under\n\t\t * `hrefBase` (a page path in another app), and only http, https and\n\t\t * paths on this server opened. Anything else is no link.\n\t\t *\n\t\t * @param {{href: string, hrefBase?: string}} link The link declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The href, or ''.\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-link-can-point-into-another-app\n\t\t */\n\t\tlinkHref(link, record) {\n\t\t\tlet value = link.href.includes('{') ? fillTemplate(link.href, record) : this.resolveTemplate(link.href, record)\n\t\t\tvalue = String(value || '').trim()\n\t\t\tif (value === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst parsed = absoluteUrl(value)\n\t\t\tif (parsed) {\n\t\t\t\treturn LINK_SCHEMES.includes(parsed.protocol) ? parsed.href : ''\n\t\t\t}\n\t\t\tif (typeof link.hrefBase === 'string' && link.hrefBase !== '') {\n\t\t\t\tconst path = link.hrefBase.replace(/\\/+$/, '') + '/' + value.replace(/^\\/+/, '')\n\t\t\t\treturn generateUrl(path)\n\t\t\t}\n\t\t\treturn value.startsWith('/') && !value.startsWith('//') ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * A meter entry's parts: the amount at `path` against the limit at\n\t\t * `limitPath` (or a constant `limit`), both formatted with the entry's\n\t\t * `format`. Without a positive numeric limit there is no bar, only the\n\t\t * amount. Past the limit the text says so, so the state never rests on\n\t\t * the bar's colour alone.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, amount: number|null, limit: number|null, percent: number, over: boolean}}\n\t\t * @spec openspec/changes/facts-meter-row/specs/facts-meter-row/spec.md#requirement-a-facts-entry-can-be-a-meter\n\t\t */\n\t\tmeterOf(record, field) {\n\t\t\tconst amount = Number(readPath(record, field.path))\n\t\t\tconst rawLimit = typeof field.limitPath === 'string' && field.limitPath !== '' ? readPath(record, field.limitPath) : field.limit\n\t\t\tconst limit = rawLimit === null || rawLimit === undefined || rawLimit === '' ? NaN : Number(rawLimit)\n\t\t\tconst label = this.tr(field.label || '')\n\t\t\tconst value = this.valueOf(record, field)\n\t\t\tif (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) {\n\t\t\t\treturn { label, value, amount: Number.isFinite(amount) ? amount : null, limit: null, percent: 0, over: false }\n\t\t\t}\n\t\t\tconst over = amount > limit\n\t\t\tconst limitText = this.format(limit, field, record)\n\t\t\treturn {\n\t\t\t\tlabel,\n\t\t\t\tvalue: over\n\t\t\t\t\t? t('nextcloud-vue', '{value} of {limit}, over the limit', { value, limit: limitText })\n\t\t\t\t\t: t('nextcloud-vue', '{value} of {limit}', { value, limit: limitText }),\n\n\t\t\t\tamount,\n\t\t\t\tlimit,\n\t\t\t\tpercent: Math.max(0, Math.min(100, (amount / limit) * 100)),\n\t\t\t\tover,\n\t\t\t}\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t * @spec openspec/changes/facts-entry-empty-value/specs/facts-widget/spec.md#requirement-a-facts-entry-can-name-its-empty-value\n\t\t */\n\t\t/**\n\t\t * The key of one `countFrom` list on this record: register, schema and\n\t\t * the filter with its tokens resolved.\n\t\t *\n\t\t * @param {{register: string, schema: string, filter?: object}} countFrom The list.\n\t\t * @return {string} The key.\n\t\t */\n\t\tcountKey(countFrom) {\n\t\t\treturn JSON.stringify([countFrom.register, countFrom.schema, resolveDeepTokens(countFrom.filter || {}, this.tokenCtx)])\n\t\t},\n\n\t\t/**\n\t\t * Count every `countFrom` list with the list endpoint's total. A count\n\t\t * that cannot be read stays unset, so its row is left out.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list\n\t\t */\n\t\tasync loadCounts() {\n\t\t\tconst fields = this.entries\n\t\t\tconst seen = new Set()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst countFrom = field && field.countFrom\n\t\t\t\tif (!countFrom || typeof countFrom !== 'object' || !countFrom.register || !countFrom.schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst key = this.countKey(countFrom)\n\t\t\t\tif (seen.has(key)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tseen.add(key)\n\t\t\t\ttry {\n\t\t\t\t\tconst total = await fetchListTotal(countFrom.register, countFrom.schema, countFrom.filter || {}, this.tokenCtx)\n\t\t\t\t\tthis.counts = { ...this.counts, [key]: total }\n\t\t\t\t} catch {\n\t\t\t\t\tthis.counts = { ...this.counts, [key]: null }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tentryValue(record, field, kind) {\n\t\t\tconst value = this.bareEntryValue(record, field, kind)\n\t\t\tif (value === '') {\n\t\t\t\treturn typeof field.emptyValue === 'string' && field.emptyValue !== '' ? this.tr(field.emptyValue) : ''\n\t\t\t}\n\t\t\tif (typeof field.suffix !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\t// The words of a suffix are translated; its leading space stays as written.\n\t\t\tconst lead = field.suffix.match(/^\\s*/)[0]\n\t\t\treturn `${value}${lead}${this.tr(field.suffix.slice(lead.length))}`\n\t\t},\n\n\t\t/**\n\t\t * An entry's text before its `suffix`.\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/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t */\n\t\tbareEntryValue(record, field, kind) {\n\t\t\tif (field.countFrom && typeof field.countFrom === 'object') {\n\t\t\t\tconst total = this.counts[this.countKey(field.countFrom)]\n\t\t\t\treturn Number.isFinite(total) ? String(total) : ''\n\t\t\t}\n\t\t\tif (kind === 'progress') {\n\t\t\t\treturn this.progressOf(record, field).value\n\t\t\t}\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif (typeof field.template === 'string' && field.template !== '') {\n\t\t\t\treturn this.templateValue(record, field.template)\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 * Fill a template's `{path}` placeholders from the record: \"{available} {unit}\"\n\t\t * reads \"12 pcs\". Empty when every placeholder is empty.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string} template The template.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t */\n\t\ttemplateValue(record, template) {\n\t\t\tlet filled = false\n\t\t\tconst text = this.tr(template).replace(/\\{([\\w.@]+)(?::([\\w-]+))?\\}/g, (match, path, format) => {\n\t\t\t\tconst value = this.format(readPath(record, path), format ? { format } : {}, record)\n\t\t\t\tfilled = filled || value !== ''\n\t\t\t\treturn value\n\t\t\t})\n\t\t\treturn filled ? text.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * The rows the record answers at `rowsPath`: `{ label, value }` items\n\t\t * drawn after the declared fields.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {boolean} hideEmpty Whether an empty value drops its row.\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-draw-the-rows-the-source-answers\n\t\t */\n\t\tansweredRows(record, hideEmpty) {\n\t\t\tconst path = this.content?.rowsPath\n\t\t\tconst items = typeof path === 'string' && path !== '' ? readPath(record, path) : null\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn items\n\t\t\t\t.filter((item) => item && typeof item === 'object')\n\t\t\t\t.map((item, index) => ({ item, index, value: this.format(item.value, { format: item.format }, record) }))\n\t\t\t\t.filter(({ value }) => !(hideEmpty && value === ''))\n\t\t\t\t.map(({ item, index, value }) => ({\n\t\t\t\t\tkey: `answered-${index}`,\n\t\t\t\t\tlabel: typeof item.label === 'string' ? item.label : '',\n\t\t\t\t\tvalue,\n\t\t\t\t\t...this.rowExtras(item, { path: 'value' }, value),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether the viewer administers this instance, for `adminOnly` actions.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tviewerIsAdmin() {\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The id an `actions[]` entry is drawn and tested by.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @param {number} index Its place in `actions`.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tactionId(action, index) {\n\t\t\treturn typeof action.id === 'string' && action.id !== '' ? action.id : `action-${index}`\n\t\t},\n\n\t\t/**\n\t\t * Settle the `actions[]` conditions that need a request (an `endpoint` or\n\t\t * a `source`), against the record shown.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tasync evaluateRemoteVisibility() {\n\t\t\tconst record = this.record\n\t\t\tconst actions = Array.isArray(this.content?.actions) ? this.content.actions : []\n\t\t\tconst remote = actions\n\t\t\t\t.map((action, index) => ({ action, index }))\n\t\t\t\t.filter(({ action }) => action && action.visibleWhen && !isLocallyDecidableVisibleWhen(action.visibleWhen))\n\t\t\tif (!record || remote.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { ...this.tokenCtx, object: record }\n\t\t\tconst outcomes = await Promise.all(remote.map(({ action }) => evaluateVisibleWhen(action.visibleWhen, ctx)))\n\t\t\tconst next = {}\n\t\t\tremote.forEach(({ action, index }, at) => {\n\t\t\t\tnext[this.actionId(action, index)] = outcomes[at]\n\t\t\t})\n\t\t\tthis.remoteVisibility = next\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\tconst server = field.reveal && typeof field.reveal === 'object' && typeof field.reveal.url === 'string' && field.reveal.url !== ''\n\t\t\t\textras.reveal = (field.reveal === true || server) && value.length > 4\n\t\t\t\tif (server) {\n\t\t\t\t\textras.revealUrl = field.reveal.url\n\t\t\t\t\textras.revealPath = typeof field.reveal.path === 'string' && field.reveal.path !== '' ? field.reveal.path : (field.path || '')\n\t\t\t\t}\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\tasync reveal(row) {\n\t\t\tif (row.revealUrl) {\n\t\t\t\tconst value = await this.fetchRevealed(row)\n\t\t\t\tif (value === null) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.revealedValues = { ...this.revealedValues, [row.key]: value }\n\t\t\t}\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 * The text a row shows: its mask until revealed, then the value the\n\t\t * server answered (a server reveal) or the value the record holds.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server\n\t\t */\n\t\tshownValue(row) {\n\t\t\tif (!row.masked || !this.revealed[row.key]) {\n\t\t\t\treturn row.masked || row.value\n\t\t\t}\n\t\t\treturn Object.hasOwn(this.revealedValues, row.key) ? this.revealedValues[row.key] : row.value\n\t\t},\n\n\t\t/**\n\t\t * Ask the server for a masked value (`reveal.url`, tokens resolved,\n\t\t * read at `reveal.path`), never from a cache: each show is a request\n\t\t * the server can check and record.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<string|null>} The value, or null when it could not be read.\n\t\t * @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server\n\t\t */\n\t\tasync fetchRevealed(row) {\n\t\t\tthis.revealing = row.key\n\t\t\tthis.revealErrors = { ...this.revealErrors, [row.key]: '' }\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource({ url: row.revealUrl, responsePath: row.revealPath }, this.tokenCtx, { force: true })\n\t\t\t\tif (payload === null || payload === undefined || payload === '' || typeof payload === 'object') {\n\t\t\t\t\tthrow new Error('no value')\n\t\t\t\t}\n\t\t\t\treturn String(payload)\n\t\t\t} catch {\n\t\t\t\tthis.revealErrors = { ...this.revealErrors, [row.key]: t('nextcloud-vue', 'The value could not be shown.') }\n\t\t\t\treturn null\n\t\t\t} finally {\n\t\t\t\tthis.revealing = ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A row's label: the value at `labelPath` (the record, or the page's\n\t\t * `@context.<name>` records) when it holds text, else `label`, both\n\t\t * through the host translate. A case type can name its handler\n\t\t * (\"Regisseur\") while another keeps \"Behandelaar\".\n\t\t *\n\t\t * @param {object} record The record shown.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-label-from-path/specs/facts-widget/spec.md#requirement-a-facts-row-can-take-its-label-from-the-record-or-its-context\n\t\t */\n\t\tlabelOf(record, field) {\n\t\t\tif (typeof field.labelPath === 'string' && field.labelPath !== '') {\n\t\t\t\tconst rows = this.cnDetailVisibilityRows && typeof this.cnDetailVisibilityRows === 'object' && 'value' in this.cnDetailVisibilityRows\n\t\t\t\t\t? this.cnDetailVisibilityRows.value\n\t\t\t\t\t: this.cnDetailVisibilityRows\n\t\t\t\tconst named = readPath({ ...(record || {}), '@context': rows || {} }, field.labelPath)\n\t\t\t\tif (typeof named === 'string' && named.trim() !== '') {\n\t\t\t\t\treturn this.tr(named.trim())\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.tr(field.label || '')\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), pill: (object|null)}} `action` is `{ label, icon, run }`, `pill` is `{ label, variant }`.\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\tpill: this.headerPill,\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 = this.entries\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: by id or url, the first match of a query, or\n\t\t * an app endpoint.\n\t\t *\n\t\t * @param {boolean} [force] Read past the shared endpoint cache (a page refresh).\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tasync load(force = false) {\n\t\t\tconst seq = ++this.loadSeq\n\t\t\tconst source = this.resolvedSource\n\t\t\tconst endpoint = this.endpointRequest\n\t\t\tthis.error = false\n\t\t\tconst url = source && !endpoint ? this.sourceUrl(source) : ''\n\t\t\tif ((!endpoint || endpoint.blocked) && !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 data = endpoint\n\t\t\t\t\t? await fetchEndpointSource(this.content.endpointSource, this.tokenCtx, { force })\n\t\t\t\t\t: await this.readSource(source, url)\n\t\t\t\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t\t}\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\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.fetched = null\n\t\t\t\t\tthis.error = true\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.loading = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * GET a `source`: the object itself, or the first match of its `filter`.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @param {string} url Its URL.\n\t\t * @return {Promise<unknown>} The object read, or null.\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tasync readSource(source, url) {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\tconst params = this.sourceQuery(source)\n\t\t\tconst response = params ? await axios.get(full, { params }) : await axios.get(full)\n\t\t\tconst data = response && response.data\n\t\t\tif (!params) {\n\t\t\t\treturn data\n\t\t\t}\n\t\t\tconst first = data && Array.isArray(data.results) ? data.results[0] : null\n\t\t\treturn first ?? null\n\t\t},\n\n\t\t/**\n\t\t * The query of a source that names no `id`: its `filter`, its `sort` as\n\t\t * `_order`, and one result. Null for a source read by id or url.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tsourceQuery(source) {\n\t\t\tif (typeof source.url === 'string' || Object.hasOwn(source, 'id')) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst params = { ...(source.filter && typeof source.filter === 'object' ? source.filter : {}) }\n\t\t\tconst sort = source.sort\n\t\t\tif (sort && typeof sort === 'object' && typeof sort.field === 'string' && sort.field !== '') {\n\t\t\t\tparams[`_order[${sort.field}]`] = sort.dir === 'desc' || sort.direction === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tparams._limit = 1\n\t\t\treturn params\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\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 && this.sourceQuery(source)) {\n\t\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}`\n\t\t\t}\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 * @spec openspec/changes/facts-user-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-user-by-name\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\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: spec.itemFormat || '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 === 'year') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\treturn date === null ? '' : String(date.getFullYear())\n\t\t\t}\n\t\t\t// The month written out, the year only outside this year: \"3 oktober\".\n\t\t\tif (format === 'longdate') {\n\t\t\t\treturn formatLongDate(raw)\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\t// A number of months in words: whole years as years (\"20 jaar\"), else months.\n\t\t\tif (format === 'months') {\n\t\t\t\tconst months = Number(raw)\n\t\t\t\tif (typeof raw === 'object' || !Number.isInteger(months) || months < 0) {\n\t\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t\t}\n\t\t\t\treturn months > 0 && months % 12 === 0\n\t\t\t\t\t? n('nextcloud-vue', '%n year', '%n years', months / 12)\n\t\t\t\t\t: n('nextcloud-vue', '%n month', '%n months', months)\n\t\t\t}\n\t\t\tif (format === 'bytes') {\n\t\t\t\tconst bytes = Number(raw)\n\t\t\t\treturn Number.isFinite(bytes) && bytes >= 0 ? formatFileSize(bytes) : ''\n\t\t\t}\n\t\t\tif (format === 'user') {\n\t\t\t\treturn userDisplayName(raw)\n\t\t\t}\n\t\t\t// A Nextcloud group by its display name; the id until the name is known.\n\t\t\tif (format === 'group') {\n\t\t\t\tif (typeof raw === 'object') {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\tconst name = groupDisplayName(String(raw))\n\t\t\t\tif (name === null) {\n\t\t\t\t\tloadGroupDisplayName(String(raw))\n\t\t\t\t\treturn String(raw)\n\t\t\t\t}\n\t\t\t\treturn name\n\t\t\t}\n\t\t\tif (format === 'number') {\n\t\t\t\tconst number = Number(raw)\n\t\t\t\tif (typeof raw === 'object' || !Number.isFinite(number)) {\n\t\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t\t}\n\t\t\t\tconst digits = Number.isInteger(spec.decimals) && spec.decimals >= 0 ? { minimumFractionDigits: spec.decimals, maximumFractionDigits: spec.decimals } : {}\n\t\t\t\treturn new Intl.NumberFormat(metricLocale(), digits).format(number)\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__pills {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n/* `padding: 0`: Nextcloud's core stylesheet pads every `dl` 12px at the top\n and bottom, which put a side card's facts 24px under its title, against\n the board's 12 (DqZaak \"Behandeling\", DqZaakDocumenten). */\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tpadding: 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.cn-facts-widget__row--inline {\n\tdisplay: flex;\n\tgrid-column: 1 / -1;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-facts-widget__row--inline .cn-facts-widget__value {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding, a\n 130px-wide `dt`), which pushed the label to the right of its column, added\n 12px around both and widened the label column to 130px. The class is\n doubled so these resets outrank that rule, and a dossiq-style `:deep()`\n override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\twidth: auto;\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__meter {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tfont-size: 14px;\n}\n\n.cn-facts-widget__meter-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 16px;\n}\n\n.cn-facts-widget__meter-label {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter-value {\n\tfont-weight: 500;\n\ttext-align: end;\n}\n\n.cn-facts-widget__meter-track {\n\theight: 6px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-darker);\n\toverflow: hidden;\n}\n\n.cn-facts-widget__meter-bar {\n\tdisplay: block;\n\theight: 100%;\n\tborder-radius: inherit;\n\tbackground: var(--color-primary-element);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-value {\n\tcolor: var(--color-error-text);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-bar {\n\tbackground: var(--color-error);\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__reveal-error {\n\tfont-size: 13px;\n\tcolor: var(--color-text-error, var(--color-error));\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\tcolor: var(--color-main-text);\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 30px;\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/* Initials stand for a person: round, as an avatar. */\n.cn-facts-widget__lead-tile--initials {\n\tborder-radius: 50%;\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__notice {\n\tmargin: 4px 0 0;\n}\n\n/* A div, not a p: a theme sets every paragraph's size with !important. */\n.cn-facts-widget__footnote {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tline-height: 1.2;\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.cn-facts-widget__link--button {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tbackground: none;\n\tfont-family: inherit;\n\tcursor: pointer;\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/* One column, label above value: 2px under its label, 10px to the next\n label, so a long label never squeezes its value (DqHandhaving). */\n.cn-facts-widget__rows--stacked {\n\tgrid-template-columns: minmax(0, 1fr);\n\tgap: 2px 0;\n}\n\n.cn-facts-widget__rows--stacked .cn-facts-widget__value.cn-facts-widget__value {\n\tmargin-bottom: 8px;\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":["_openBlock","_createElementBlock","_createTextVNode","_toDisplayString","_createVNode","_Fragment","_renderList","_createBlock","_normalizeClass","_createElementVNode","_resolveDynamicComponent","_normalizeStyle"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnFactsWidget.vue3.js","sources":["../../../../src/components/CnFactsWidget/CnFactsWidget.vue?vue&type=template&id=48761b13&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\"\n\t\t\tclass=\"cn-facts-widget__muted\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-facts-error\">\n\t\t\t{{ errorLabel }}\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-facts-widget__inline-button\"\n\t\t\t\tdata-testid=\"cn-facts-retry\"\n\t\t\t\t@click=\"load\">\n\t\t\t\t{{ retryLabel }}\n\t\t\t</button>\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<div v-else-if=\"blocks.length === 0 && showsEmpty\" class=\"cn-facts-widget__empty\">\n\t\t\t<p class=\"cn-facts-widget__muted\" data-testid=\"cn-facts-empty\">\n\t\t\t\t{{ emptyLabel }}\n\t\t\t</p>\n\t\t\t<button\n\t\t\t\tv-for=\"(entry, index) in emptyActions\"\n\t\t\t\t:key=\"`empty-action-${index}`\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-facts-widget__action\"\n\t\t\t\tdata-testid=\"cn-facts-empty-action\"\n\t\t\t\t@click=\"runAction(entry.action)\">\n\t\t\t\t<CnIcon v-if=\"entry.icon\" :name=\"entry.icon\" :size=\"16\" />\n\t\t\t\t{{ entry.label }}\n\t\t\t</button>\n\t\t</div>\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, 'cn-facts-widget__rows--stacked': stacked }\">\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:class=\"{ 'cn-facts-widget__row--inline': row.inline }\"\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<router-link\n\t\t\t\t\t\t\t\tv-else-if=\"row.to\"\n\t\t\t\t\t\t\t\t:to=\"row.to\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__value-link\">\n\t\t\t\t\t\t\t\t{{ shownValue(row) }}\n\t\t\t\t\t\t\t</router-link>\n\t\t\t\t\t\t\t<span v-else>{{ shownValue(row) }}</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:disabled=\"revealing === 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<span\n\t\t\t\t\t\t\t\tv-if=\"revealErrors[row.key]\"\n\t\t\t\t\t\t\t\tclass=\"cn-facts-widget__reveal-error\"\n\t\t\t\t\t\t\t\trole=\"alert\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-fact-reveal-error-${row.key}`\">{{ revealErrors[row.key] }}</span>\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}`, { 'cn-facts-widget__lead-tile--initials': !block.icon }]\"\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\n\t\t\t\t\tv-else-if=\"block.kind === 'meter'\"\n\t\t\t\t\tclass=\"cn-facts-widget__meter\"\n\t\t\t\t\t:class=\"{ 'cn-facts-widget__meter--over': block.over }\"\n\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}`\">\n\t\t\t\t\t<div class=\"cn-facts-widget__meter-head\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-label\">{{ block.label }}</span>\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-value\">{{ block.value }}</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-if=\"block.limit !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"meter\"\n\t\t\t\t\t\t:aria-label=\"block.label\"\n\t\t\t\t\t\t:aria-valuenow=\"block.amount\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\t:aria-valuemax=\"block.limit\"\n\t\t\t\t\t\t:aria-valuetext=\"block.value\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tv-else-if=\"block.kind === 'progress'\"\n\t\t\t\t\tclass=\"cn-facts-widget__progress\"\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\tv-if=\"block.percent !== null\"\n\t\t\t\t\t\tclass=\"cn-facts-widget__meter-track\"\n\t\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\t\t:aria-label=\"block.label || null\"\n\t\t\t\t\t\t:aria-valuenow=\"block.percent\"\n\t\t\t\t\t\taria-valuemin=\"0\"\n\t\t\t\t\t\taria-valuemax=\"100\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${block.key}-bar`\">\n\t\t\t\t\t\t<span class=\"cn-facts-widget__meter-bar\" :style=\"{ width: `${block.percent}%` }\" />\n\t\t\t\t\t</span>\n\t\t\t\t\t<span class=\"cn-facts-widget__line\">{{ block.value }}</span>\n\t\t\t\t</div>\n\t\t\t\t<div v-else-if=\"block.kind === 'pills'\" class=\"cn-facts-widget__pills\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-for=\"pill in block.pills\"\n\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t:label=\"pill.value\"\n\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t:data-testid=\"`cn-fact-${pill.key}`\" />\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<NcNoteCard\n\t\t\tv-if=\"notice && record\"\n\t\t\t:type=\"notice.type\"\n\t\t\tclass=\"cn-facts-widget__notice\"\n\t\t\tdata-testid=\"cn-facts-notice\">\n\t\t\t{{ notice.text }}\n\t\t</NcNoteCard>\n\t\t<div\n\t\t\tv-if=\"footnote && record\"\n\t\t\tclass=\"cn-facts-widget__footnote\"\n\t\t\tdata-testid=\"cn-facts-footnote\">\n\t\t\t{{ footnote }}\n\t\t</div>\n\t\t<button\n\t\t\tv-if=\"link && link.sidebarTab && record\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-facts-widget__link cn-facts-widget__link--button\"\n\t\t\tdata-testid=\"cn-facts-link\"\n\t\t\t@click=\"openSidebarTab(link.sidebarTab)\">\n\t\t\t<CnIcon v-if=\"link.icon\" :name=\"link.icon\" :size=\"18\" />\n\t\t\t{{ link.label }}\n\t\t</button>\n\t\t<component\n\t\t\t:is=\"link.to ? 'router-link' : 'a'\"\n\t\t\tv-else-if=\"link && record\"\n\t\t\t:to=\"link.to || undefined\"\n\t\t\t:href=\"link.href || undefined\"\n\t\t\t:target=\"link.target || undefined\"\n\t\t\t:rel=\"link.target === '_blank' ? 'noopener noreferrer' : undefined\"\n\t\t\tclass=\"cn-facts-widget__link\"\n\t\t\tdata-testid=\"cn-facts-link\">\n\t\t\t<CnIcon v-if=\"link.icon\" :name=\"link.icon\" :size=\"18\" />\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\t<CnActionButtons\n\t\t\tv-if=\"drawnActions.length > 0\"\n\t\t\tclass=\"cn-facts-widget__actions\"\n\t\t\t:actions=\"drawnActions\"\n\t\t\tdata-testid=\"cn-facts-actions\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { emit, subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { formatFileSize } from '@nextcloud/files'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcLoadingIcon, NcNoteCard } 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 CnActionButtons from '../CnActionButtons/CnActionButtons.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { fetchEndpointSource, 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 { formatTermDuration } from '../../utils/builtInFormatters.js'\nimport { conditionalLine } from '../../utils/conditionalLine.js'\nimport { parseDateValue } from '../../utils/dateVariant.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { fillTemplate, formatLongDate } from '../../utils/fillTemplate.js'\nimport { formatMoney, metricLocale, unwrapAppConfig } from '../../utils/formatMetric.js'\nimport { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveFilterTokens.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\nimport { evaluateVisibleWhen, evaluateVisibleWhenLocal, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'\n\n/** Event-bus channel of the page-level Refresh, which an api-call or a saved form also sends. */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * Replace every `@source.<field>` in a declaration with the field of the\n * record shown, deeply.\n *\n * @param {unknown} value The declaration.\n * @param {object} record The record shown.\n * @return {{value: unknown, unresolved: boolean}} The resolved copy, and whether a token named an empty field.\n * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n */\nfunction resolveSourceTokens(value, record) {\n\tlet unresolved = false\n\tconst walk = (node) => {\n\t\tif (typeof node === 'string') {\n\t\t\treturn node.replace(/@source\\.([\\w.@]+)/g, (match, path) => {\n\t\t\t\tconst read = path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)\n\t\t\t\tif (read === undefined || read === null || read === '' || typeof read === 'object') {\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}\n\t\tif (Array.isArray(node)) {\n\t\t\treturn node.map(walk)\n\t\t}\n\t\tif (node && typeof node === 'object') {\n\t\t\treturn Object.fromEntries(Object.entries(node).map(([key, entry]) => [key, walk(entry)]))\n\t\t}\n\t\treturn node\n\t}\n\tconst resolved = walk(value)\n\treturn { value: resolved, unresolved }\n}\n\n/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */\n/**\n * Two or more pills in a row become one `pills` block, drawn on one line.\n *\n * @param {Array<object>} blocks The blocks in order.\n * @return {Array<object>}\n * @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context\n */\nfunction groupPills(blocks) {\n\tconst out = []\n\tblocks.forEach((block) => {\n\t\tconst last = out[out.length - 1]\n\t\tif (block.kind !== 'pill') {\n\t\t\tout.push(block)\n\t\t} else if (last && last.kind === 'pills') {\n\t\t\tlast.pills.push(block)\n\t\t} else if (last && last.kind === 'pill') {\n\t\t\tout[out.length - 1] = { kind: 'pills', key: `pills-${last.key}`, pills: [last, block] }\n\t\t} else {\n\t\t\tout.push(block)\n\t\t}\n\t})\n\treturn out\n}\n\nconst KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text', 'meter', 'progress'])\n\n/** The schemes a facts link opens, besides a path on this server. */\nconst LINK_SCHEMES = ['http:', 'https:']\n\n/**\n * A value read as an absolute URL, or null when it is not one (a page path).\n *\n * @param {string} value The value.\n * @return {URL|null}\n */\nfunction absoluteUrl(value) {\n\ttry {\n\t\treturn new URL(value)\n\t} catch {\n\t\treturn null\n\t}\n}\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, CnActionButtons, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon, NcNoteCard },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t\tcnObjectContext: { default: null },\n\t\t/** The detail page's newest change of the record (`recordLastChange`), read as `_lastChange.*`. */\n\t\tcnRecordLastChange: { 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\t/** The page's object sidebar channel; a `link.sidebarTab` opens one of its tabs. */\n\t\tobjectSidebarState: { default: null },\n\t\t/** The records the detail page reads beside the record (`@context.<name>`), for a `labelPath`. */\n\t\tcnDetailVisibilityRows: { 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?, inline?, 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`), `pill` or `meter`.\n\t\t * `countFrom` (`{ register, schema, filter? }`, `@objectId` / `@object.<field>` in the filter) shows how many records that list holds (\"Lopende zaken 43\"); the row is left out while the count cannot be read.\n\t\t * A `meter` shows the amount at `path` against the limit at `limitPath` (or a constant `limit`) as \"12 of 50\" with a bar; past the limit the bar turns red and the text says \"over the limit\"; without a positive limit it shows the amount and no bar.\n\t\t * A row with `inline: true` spans both columns as one line, the label, the value and its buttons side by side (a masked BSN with Show).\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, which unmasks the record's value; `reveal: { url, path? }` makes Show ask the server for the value at `url` (tokens resolved), read at `path` (else the entry's path), so a server that answers the mask can check and record the reveal). `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 * `route` (`{ name, params?, query? }`, `@source.<path>` in a param) on a `title` or a `row` links its value to that route; a param that does not fill leaves it plain.\n\t\t * `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.\n\t\t * `template` (\"{available} {unit}\") fills its `{path}` placeholders from the record instead of reading `path` (`{path:format}` formats one, \"{from:year}\"); it is empty when every placeholder is. `suffix` (\" kg\") is added to a value that is not empty, its words translated.\n\t\t * `labelPath` (\"@context.caseType.handlerLabel\") names a value on the record or the page's `@context` records that labels the row when it holds text; `label` is the fallback.\n\t\t * `emptyValue` (\"No\") is shown, translated, when the entry has no value, so the entry stays drawn whatever `hideEmpty` says (an override that was never set reads \"No\").\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 * - `actions` — action buttons under the facts in the `headerActions` shape (`api-call`, `open-form`, `navigate`, `open-modal`, ...; `confirm`, `variant`). `@source.<field>` names a field of the record shown, and an action whose token names an empty field is not drawn. `visibleWhen` reads the record shown; `adminOnly: true` draws the action for an instance admin only (visibility, never authorisation). An api-call or a saved form refreshes the page, and the facts read their source again.\n\t\t * - `fieldsFrom` — `@context.<name>.<path>`, a list of entries on a record the page reads beside this one (a case type's `requestFields`); when it holds entries they are drawn instead of `fields`.\n\t\t * - `rowsPath` — a path to a list of `{ label, value }` the record holds, drawn as rows after `fields` (a preview the server builds).\n\t\t * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar \"Advies ontvangen\"); nothing shows while the value is empty.\n\t\t * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.\n\t\t * - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten \"Na het ondertekenen ...\"); the text is translated.\n\t\t * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: \"3 oktober\"), `duration` (an ISO period in words, \"4 weken\"; `extendBy` names the extension period on the record: \"4 weken, te verdagen met 2\"), `year` (\"2026\"), `list` (an array joined by \", \", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, \"1.5 MB\"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is \"20 jaar\") 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. `{ register, schema, filter, sort? }` without `id` reads the first object that matches `filter` in `sort` order (`{ field, dir }`): the latest count of a shift. An entry with `from: \"object\"` reads the current object even then (a client card that also shows the ticket's channel).\n\t\t * - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.\n\t\t * - `emptyWhen` — a path; when the record's value there is truthy the card shows `emptyText` (an endpoint that answers `empty: true`).\n\t\t * - `link` — `{ label, route? | href? }` at the foot. `{ label, icon?, sidebarTab }` instead opens the page's object sidebar on that tab (no link without a sidebar).\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). `1` stacks each label above its value in one column, for a narrow card with long labels (board DqHandhaving \"Afwijking en definitief\").\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\trevealedValues: {},\n\t\t\trevealErrors: {},\n\t\t\trevealing: '',\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\t/** The `countFrom` totals read so far, by `countKey`. */\n\t\t\tcounts: {},\n\t\t\t/** The outcome of each `actions[]` condition that needs a request, by action id. */\n\t\t\tremoteVisibility: {},\n\t\t\t/** Sequence of the source reads, so an older answer never replaces a newer one. */\n\t\t\tloadSeq: 0,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The entries to draw: the list `fieldsFrom` names on the page's\n\t\t * `@context` records when it holds entries (a case type's\n\t\t * `requestFields`), else `fields`.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-fields-from-context/specs/facts-widget/spec.md#requirement-a-facts-card-can-take-its-entries-from-the-page-context\n\t\t */\n\t\tentries() {\n\t\t\tconst from = this.content?.fieldsFrom\n\t\t\tif (typeof from === 'string' && from.startsWith('@context.')) {\n\t\t\t\tconst rows = this.cnDetailVisibilityRows && typeof this.cnDetailVisibilityRows === 'object' && 'value' in this.cnDetailVisibilityRows\n\t\t\t\t\t? this.cnDetailVisibilityRows.value\n\t\t\t\t\t: this.cnDetailVisibilityRows\n\t\t\t\tconst named = readPath({ '@context': rows || {} }, from)\n\t\t\t\tconst list = Array.isArray(named) ? named.filter((entry) => entry && typeof entry === 'object' && !Array.isArray(entry)) : []\n\t\t\t\tif (list.length > 0) {\n\t\t\t\t\treturn list\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.isArray(this.content?.fields) ? this.content.fields : []\n\t\t},\n\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 * Whether the rows stack in one column, label above value.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tstacked() {\n\t\t\treturn Number(this.content?.columns) === 1\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 entries' `countFrom` lists (`{ register, schema, filter? }`), each\n\t\t * once, keyed by what they count on this record.\n\t\t *\n\t\t * @return {string} A stable key of the lists to count.\n\t\t * @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list\n\t\t */\n\t\tcountJobsKey() {\n\t\t\tconst fields = this.entries\n\t\t\tconst keys = fields\n\t\t\t\t.filter((field) => field && field.countFrom && typeof field.countFrom === 'object' && field.countFrom.register && field.countFrom.schema)\n\t\t\t\t.map((field) => this.countKey(field.countFrom))\n\t\t\treturn JSON.stringify([...new Set(keys)])\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\tsourceKey() {\n\t\t\tif (this.endpointRequest) {\n\t\t\t\treturn JSON.stringify(this.endpointRequest)\n\t\t\t}\n\t\t\treturn this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''\n\t\t},\n\n\t\t/**\n\t\t * The `endpointSource` request with its tokens resolved, or null without one.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\tendpointRequest() {\n\t\t\tconst config = this.content?.endpointSource\n\t\t\tif (!config || typeof config !== 'object' || typeof config.url !== 'string' || config.url === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn resolveEndpointRequest(config, this.tokenCtx)\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-read-an-app-endpoint\n\t\t */\n\t\trecord() {\n\t\t\tlet record = this.endpointRequest || this.resolvedSource ? this.fetched : this.objectData\n\t\t\tconst provided = this.cnRecordLastChange\n\t\t\tconst lastChange = provided && typeof provided === 'object' && 'value' in provided ? provided.value : null\n\t\t\tif (record && record === this.objectData && lastChange) {\n\t\t\t\trecord = { ...record, _lastChange: lastChange }\n\t\t\t}\n\t\t\tconst path = this.content?.source?.path\n\t\t\tif (this.resolvedSource && !this.endpointRequest && typeof path === 'string' && path !== '' && record) {\n\t\t\t\tconst inner = readPath(record, path)\n\t\t\t\trecord = inner && typeof inner === 'object' && !Array.isArray(inner) ? inner : null\n\t\t\t}\n\t\t\tconst emptyWhen = this.content?.emptyWhen\n\t\t\tif (record && typeof emptyWhen === 'string' && emptyWhen !== '' && readPath(record, emptyWhen)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn record\n\t\t},\n\n\t\t/**\n\t\t * Whether the empty line shows: when the card declares facts or actions\n\t\t * for the empty case. A card that is only a link says nothing without one.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\tshowsEmpty() {\n\t\t\tconst fields = this.content?.fields\n\t\t\treturn (Array.isArray(fields) && fields.length > 0) || this.emptyActions.length > 0 || (typeof this.content?.emptyText === 'string' && this.content.emptyText !== '')\n\t\t},\n\n\t\t/**\n\t\t * The actions under the empty text (`emptyActions`), as drawn.\n\t\t *\n\t\t * @return {Array<{label: string, icon: string, action: object}>}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\temptyActions() {\n\t\t\tconst list = Array.isArray(this.content?.emptyActions) ? this.content.emptyActions : []\n\t\t\treturn list\n\t\t\t\t.map((action) => ({ view: this.actionView(action), action }))\n\t\t\t\t.filter((entry) => entry.view)\n\t\t\t\t.map((entry) => ({ ...entry.view, action: entry.action }))\n\t\t},\n\n\t\t/** @return {string} The line shown when the source could not be read. */\n\t\terrorLabel() {\n\t\t\treturn this.tr(this.content?.errorText || 'Could not load this information')\n\t\t},\n\n\t\t/** @return {string} The retry button's text. */\n\t\tretryLabel() {\n\t\t\treturn t('nextcloud-vue', 'Try again')\n\t\t},\n\n\t\t/**\n\t\t * The `actions[]` to draw: `@source.<field>` resolved against the record\n\t\t * shown, `adminOnly` and `visibleWhen` settled here, so the buttons do\n\t\t * not judge them again against the page's object.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tdrawnActions() {\n\t\t\tconst record = this.record\n\t\t\tconst actions = Array.isArray(this.content?.actions) ? this.content.actions : []\n\t\t\tif (!record || actions.length === 0) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst admin = this.viewerIsAdmin()\n\t\t\tconst out = []\n\t\t\tactions.forEach((action, index) => {\n\t\t\t\tif (!action || typeof action !== 'object' || (action.adminOnly === true && !admin)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst id = this.actionId(action, index)\n\t\t\t\tconst condition = action.visibleWhen\n\t\t\t\tif (condition && isLocallyDecidableVisibleWhen(condition) && !evaluateVisibleWhenLocal(condition, record)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (condition && !isLocallyDecidableVisibleWhen(condition) && this.remoteVisibility[id] !== true) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst { value, unresolved } = resolveSourceTokens(action, record)\n\t\t\t\tif (unresolved) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst { visibleWhen, adminOnly, ...drawn } = value\n\t\t\t\tout.push({ ...drawn, id })\n\t\t\t})\n\t\t\treturn out\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-draw-the-rows-the-source-answers\n\t\t * @spec openspec/changes/facts-from-object/specs/facts-widget/spec.md#requirement-an-entry-can-read-the-page-object-on-a-sourced-card\n\t\t */\n\t\tblocks() {\n\t\t\tconst sourceRecord = this.record\n\t\t\tconst fields = this.entries\n\t\t\t// A source that finds nothing still leaves the entries that read the\n\t\t\t// page's own object (`from: \"object\"`).\n\t\t\tconst ownEntries = Boolean(this.objectData) && fields.some((field) => field && field.from === 'object')\n\t\t\tif (!sourceRecord && !ownEntries) {\n\t\t\t\treturn []\n\t\t\t}\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\tif (!record) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (field.when && !evaluateVisibleWhenLocal(field.when, record)) {\n\t\t\t\t\treturn\n\t\t\t\t}\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.labelOf(record, field), value, inline: field.inline === true, ...this.rowExtras(record, field, value) }\n\t\t\t\t\tif (field.route && !row.href) {\n\t\t\t\t\t\trow.to = this.resolveRoute(field.route, record)\n\t\t\t\t\t}\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 === 'meter') {\n\t\t\t\t\tObject.assign(block, this.meterOf(record, field))\n\t\t\t\t}\n\t\t\t\tif (kind === 'progress') {\n\t\t\t\t\tObject.assign(block, this.progressOf(record, field))\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\t// A boolean looks its tone up as \"true\" / \"false\", as `map` reads it.\n\t\t\t\t\tconst hit = colours ? keys.map((candidate) => (typeof candidate === 'boolean' ? String(candidate) : candidate)).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\tthis.answeredRows(sourceRecord, hideEmpty).forEach((row) => {\n\t\t\t\tconst last = out[out.length - 1]\n\t\t\t\tif (last && last.kind === 'rows') {\n\t\t\t\t\tlast.rows.push(row)\n\t\t\t\t} else {\n\t\t\t\t\tout.push({ kind: 'rows', key: 'rows-answered', rows: [row] })\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groupPills(out)\n\t\t},\n\n\t\t/**\n\t\t * The foot link, resolved, or null. A `sidebarTab` link needs the\n\t\t * page's object sidebar and is null without one.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string, sidebarTab?: string}|null}\n\t\t * @spec openspec/changes/facts-link-opens-sidebar-tab/specs/record-widgets/spec.md#requirement-a-facts-link-can-open-a-sidebar-tab\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 (typeof link.sidebarTab === 'string' && link.sidebarTab !== '') {\n\t\t\t\treturn this.objectSidebarState\n\t\t\t\t\t? { label: this.tr(link.label), sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }\n\t\t\t\t\t: 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.linkHref(link, this.record)\n\t\t\t\treturn href ? { label: this.tr(link.label), href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : 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/**\n\t\t * The card header's status pill, read from the record like a `pill`\n\t\t * entry, or null when it has no text.\n\t\t *\n\t\t * @return {{label: string, variant: string}|null}\n\t\t */\n\t\theaderPill() {\n\t\t\tconst pill = this.content?.headerPill\n\t\t\tif (!pill || typeof pill !== 'object' || !this.record) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(this.record, pill.path) : undefined\n\t\t\tconst mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : undefined\n\t\t\tconst text = mapped ?? pill.text ?? raw\n\t\t\tif (typeof text !== 'string' && typeof text !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst label = this.tr(String(text))\n\t\t\tif (label === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst colours = pill.variantMap || pill.colorMap\n\t\t\tconst hit = colours && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(colours, raw) ? colours[raw] : pill.variant\n\t\t\treturn { label, variant: isBadgeVariant(hit) ? hit : 'default' }\n\t\t},\n\n\t\t/**\n\t\t * The note band under the facts: its text translated, its NcNoteCard\n\t\t * type from `variant` (info by default), or null without text.\n\t\t *\n\t\t * @return {{text: string, type: string}|null}\n\t\t */\n\t\tnotice() {\n\t\t\tconst notice = this.content?.notice\n\t\t\tconst text = notice && typeof notice.text === 'string' ? this.tr(notice.text) : ''\n\t\t\tif (text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }\n\t\t},\n\n\t\t/** @return {string} The translated footnote, or '' when none is set. */\n\t\tfootnote() {\n\t\t\tconst footnote = this.content?.footnote\n\t\t\treturn typeof footnote === 'string' && footnote.trim() !== '' ? this.tr(footnote) : ''\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\tcountJobsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadCounts()\n\t\t\t},\n\t\t},\n\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\t/**\n\t\t\t * Read the labels and settle the action conditions of the record shown.\n\t\t\t *\n\t\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t\t */\n\t\t\thandler() {\n\t\t\t\tthis.loadLabels()\n\t\t\t\tthis.evaluateRemoteVisibility()\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\n\t\theaderPill: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.emitHeader()\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this.onPageRefresh)\n\t},\n\n\tbeforeUnmount() {\n\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this.onPageRefresh)\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Open the page's object sidebar on one of its tabs.\n\t\t *\n\t\t * @param {string} tab The sidebar tab id.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/facts-link-opens-sidebar-tab/specs/record-widgets/spec.md#requirement-a-facts-link-can-open-a-sidebar-tab\n\t\t */\n\t\topenSidebarTab(tab) {\n\t\t\tthis.objectSidebarState.open = true\n\t\t\tthis.objectSidebarState.requestedTab = tab\n\t\t},\n\n\t\t/**\n\t\t * The page asked every widget to read again (a dialog that changed what\n\t\t * the source answers, the page's Refresh): read the source again.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-act-when-it-is-empty\n\t\t */\n\t\tonPageRefresh() {\n\t\t\tif (this.resolvedSource || this.endpointRequest) {\n\t\t\t\tthis.load(true)\n\t\t\t}\n\t\t},\n\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 * A progress entry: the percentage at `path` (0 to 100) drawn as a bar\n\t\t * when it is a number, and the sentence that says what it means\n\t\t * (`lines`, the first that applies; `conditionalLine`), else the\n\t\t * percentage itself.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, percent: (number|null)}}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-card-can-show-progress\n\t\t */\n\t\tprogressOf(record, field) {\n\t\t\tconst raw = readPath(record, field.path)\n\t\t\tconst number = raw === null || raw === undefined || raw === '' ? NaN : Number(raw)\n\t\t\tconst barWhen = field.barWhen ? evaluateVisibleWhenLocal(field.barWhen, record) : true\n\t\t\tconst percent = Number.isFinite(number) && barWhen ? Math.max(0, Math.min(100, number)) : null\n\t\t\tconst line = field.lines ? conditionalLine(field.lines, record, (text) => this.tr(text)) : ''\n\t\t\treturn {\n\t\t\t\tlabel: this.tr(field.label || ''),\n\t\t\t\tvalue: line || (percent !== null ? `${percent}%` : ''),\n\t\t\t\tpercent,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A link's href: `{field}` tokens filled from the record (and the\n\t\t * `@source.<field>` grammar), a value that is not a URL put under\n\t\t * `hrefBase` (a page path in another app), and only http, https and\n\t\t * paths on this server opened. Anything else is no link.\n\t\t *\n\t\t * @param {{href: string, hrefBase?: string}} link The link declaration.\n\t\t * @param {object|null} record The object shown.\n\t\t * @return {string} The href, or ''.\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-facts-link-can-point-into-another-app\n\t\t */\n\t\tlinkHref(link, record) {\n\t\t\tlet value = link.href.includes('{') ? fillTemplate(link.href, record) : this.resolveTemplate(link.href, record)\n\t\t\tvalue = String(value || '').trim()\n\t\t\tif (value === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst parsed = absoluteUrl(value)\n\t\t\tif (parsed) {\n\t\t\t\treturn LINK_SCHEMES.includes(parsed.protocol) ? parsed.href : ''\n\t\t\t}\n\t\t\tif (typeof link.hrefBase === 'string' && link.hrefBase !== '') {\n\t\t\t\tconst path = link.hrefBase.replace(/\\/+$/, '') + '/' + value.replace(/^\\/+/, '')\n\t\t\t\treturn generateUrl(path)\n\t\t\t}\n\t\t\treturn value.startsWith('/') && !value.startsWith('//') ? value : ''\n\t\t},\n\n\t\t/**\n\t\t * A meter entry's parts: the amount at `path` against the limit at\n\t\t * `limitPath` (or a constant `limit`), both formatted with the entry's\n\t\t * `format`. Without a positive numeric limit there is no bar, only the\n\t\t * amount. Past the limit the text says so, so the state never rests on\n\t\t * the bar's colour alone.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {object} field The entry.\n\t\t * @return {{label: string, value: string, amount: number|null, limit: number|null, percent: number, over: boolean}}\n\t\t * @spec openspec/changes/facts-meter-row/specs/facts-meter-row/spec.md#requirement-a-facts-entry-can-be-a-meter\n\t\t */\n\t\tmeterOf(record, field) {\n\t\t\tconst amount = Number(readPath(record, field.path))\n\t\t\tconst rawLimit = typeof field.limitPath === 'string' && field.limitPath !== '' ? readPath(record, field.limitPath) : field.limit\n\t\t\tconst limit = rawLimit === null || rawLimit === undefined || rawLimit === '' ? NaN : Number(rawLimit)\n\t\t\tconst label = this.tr(field.label || '')\n\t\t\tconst value = this.valueOf(record, field)\n\t\t\tif (!Number.isFinite(amount) || !Number.isFinite(limit) || limit <= 0) {\n\t\t\t\treturn { label, value, amount: Number.isFinite(amount) ? amount : null, limit: null, percent: 0, over: false }\n\t\t\t}\n\t\t\tconst over = amount > limit\n\t\t\tconst limitText = this.format(limit, field, record)\n\t\t\treturn {\n\t\t\t\tlabel,\n\t\t\t\tvalue: over\n\t\t\t\t\t? t('nextcloud-vue', '{value} of {limit}, over the limit', { value, limit: limitText })\n\t\t\t\t\t: t('nextcloud-vue', '{value} of {limit}', { value, limit: limitText }),\n\n\t\t\t\tamount,\n\t\t\t\tlimit,\n\t\t\t\tpercent: Math.max(0, Math.min(100, (amount / limit) * 100)),\n\t\t\t\tover,\n\t\t\t}\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t * @spec openspec/changes/facts-entry-empty-value/specs/facts-widget/spec.md#requirement-a-facts-entry-can-name-its-empty-value\n\t\t */\n\t\t/**\n\t\t * The key of one `countFrom` list on this record: register, schema and\n\t\t * the filter with its tokens resolved.\n\t\t *\n\t\t * @param {{register: string, schema: string, filter?: object}} countFrom The list.\n\t\t * @return {string} The key.\n\t\t */\n\t\tcountKey(countFrom) {\n\t\t\treturn JSON.stringify([countFrom.register, countFrom.schema, resolveDeepTokens(countFrom.filter || {}, this.tokenCtx)])\n\t\t},\n\n\t\t/**\n\t\t * Count every `countFrom` list with the list endpoint's total. A count\n\t\t * that cannot be read stays unset, so its row is left out.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-entry-count/specs/facts-widget/spec.md#requirement-a-facts-entry-can-count-a-related-list\n\t\t */\n\t\tasync loadCounts() {\n\t\t\tconst fields = this.entries\n\t\t\tconst seen = new Set()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst countFrom = field && field.countFrom\n\t\t\t\tif (!countFrom || typeof countFrom !== 'object' || !countFrom.register || !countFrom.schema) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst key = this.countKey(countFrom)\n\t\t\t\tif (seen.has(key)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tseen.add(key)\n\t\t\t\ttry {\n\t\t\t\t\tconst total = await fetchListTotal(countFrom.register, countFrom.schema, countFrom.filter || {}, this.tokenCtx)\n\t\t\t\t\tthis.counts = { ...this.counts, [key]: total }\n\t\t\t\t} catch {\n\t\t\t\t\tthis.counts = { ...this.counts, [key]: null }\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tentryValue(record, field, kind) {\n\t\t\tconst value = this.bareEntryValue(record, field, kind)\n\t\t\tif (value === '') {\n\t\t\t\treturn typeof field.emptyValue === 'string' && field.emptyValue !== '' ? this.tr(field.emptyValue) : ''\n\t\t\t}\n\t\t\tif (typeof field.suffix !== 'string') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\t// The words of a suffix are translated; its leading space stays as written.\n\t\t\tconst lead = field.suffix.match(/^\\s*/)[0]\n\t\t\treturn `${value}${lead}${this.tr(field.suffix.slice(lead.length))}`\n\t\t},\n\n\t\t/**\n\t\t * An entry's text before its `suffix`.\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/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t */\n\t\tbareEntryValue(record, field, kind) {\n\t\t\tif (field.countFrom && typeof field.countFrom === 'object') {\n\t\t\t\tconst total = this.counts[this.countKey(field.countFrom)]\n\t\t\t\treturn Number.isFinite(total) ? String(total) : ''\n\t\t\t}\n\t\t\tif (kind === 'progress') {\n\t\t\t\treturn this.progressOf(record, field).value\n\t\t\t}\n\t\t\tif (kind === 'lead') {\n\t\t\t\treturn this.partsValue(record, field.title)\n\t\t\t}\n\t\t\tif (typeof field.template === 'string' && field.template !== '') {\n\t\t\t\treturn this.templateValue(record, field.template)\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 * Fill a template's `{path}` placeholders from the record: \"{available} {unit}\"\n\t\t * reads \"12 pcs\". Empty when every placeholder is empty.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {string} template The template.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\n\t\t */\n\t\ttemplateValue(record, template) {\n\t\t\tlet filled = false\n\t\t\tconst text = this.tr(template).replace(/\\{([\\w.@]+)(?::([\\w-]+))?\\}/g, (match, path, format) => {\n\t\t\t\tconst value = this.format(readPath(record, path), format ? { format } : {}, record)\n\t\t\t\tfilled = filled || value !== ''\n\t\t\t\treturn value\n\t\t\t})\n\t\t\treturn filled ? text.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * The rows the record answers at `rowsPath`: `{ label, value }` items\n\t\t * drawn after the declared fields.\n\t\t *\n\t\t * @param {object} record The object.\n\t\t * @param {boolean} hideEmpty Whether an empty value drops its row.\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-draw-the-rows-the-source-answers\n\t\t */\n\t\tansweredRows(record, hideEmpty) {\n\t\t\tconst path = this.content?.rowsPath\n\t\t\tconst items = typeof path === 'string' && path !== '' ? readPath(record, path) : null\n\t\t\tif (!Array.isArray(items)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn items\n\t\t\t\t.filter((item) => item && typeof item === 'object')\n\t\t\t\t.map((item, index) => ({ item, index, value: this.format(item.value, { format: item.format }, record) }))\n\t\t\t\t.filter(({ value }) => !(hideEmpty && value === ''))\n\t\t\t\t.map(({ item, index, value }) => ({\n\t\t\t\t\tkey: `answered-${index}`,\n\t\t\t\t\tlabel: typeof item.label === 'string' ? item.label : '',\n\t\t\t\t\tvalue,\n\t\t\t\t\t...this.rowExtras(item, { path: 'value' }, value),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether the viewer administers this instance, for `adminOnly` actions.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tviewerIsAdmin() {\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The id an `actions[]` entry is drawn and tested by.\n\t\t *\n\t\t * @param {object} action The declaration.\n\t\t * @param {number} index Its place in `actions`.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tactionId(action, index) {\n\t\t\treturn typeof action.id === 'string' && action.id !== '' ? action.id : `action-${index}`\n\t\t},\n\n\t\t/**\n\t\t * Settle the `actions[]` conditions that need a request (an `endpoint` or\n\t\t * a `source`), against the record shown.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-carry-declared-actions\n\t\t */\n\t\tasync evaluateRemoteVisibility() {\n\t\t\tconst record = this.record\n\t\t\tconst actions = Array.isArray(this.content?.actions) ? this.content.actions : []\n\t\t\tconst remote = actions\n\t\t\t\t.map((action, index) => ({ action, index }))\n\t\t\t\t.filter(({ action }) => action && action.visibleWhen && !isLocallyDecidableVisibleWhen(action.visibleWhen))\n\t\t\tif (!record || remote.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { ...this.tokenCtx, object: record }\n\t\t\tconst outcomes = await Promise.all(remote.map(({ action }) => evaluateVisibleWhen(action.visibleWhen, ctx)))\n\t\t\tconst next = {}\n\t\t\tremote.forEach(({ action, index }, at) => {\n\t\t\t\tnext[this.actionId(action, index)] = outcomes[at]\n\t\t\t})\n\t\t\tthis.remoteVisibility = next\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\tconst server = field.reveal && typeof field.reveal === 'object' && typeof field.reveal.url === 'string' && field.reveal.url !== ''\n\t\t\t\textras.reveal = (field.reveal === true || server) && value.length > 4\n\t\t\t\tif (server) {\n\t\t\t\t\textras.revealUrl = field.reveal.url\n\t\t\t\t\textras.revealPath = typeof field.reveal.path === 'string' && field.reveal.path !== '' ? field.reveal.path : (field.path || '')\n\t\t\t\t}\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\tasync reveal(row) {\n\t\t\tif (row.revealUrl) {\n\t\t\t\tconst value = await this.fetchRevealed(row)\n\t\t\t\tif (value === null) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.revealedValues = { ...this.revealedValues, [row.key]: value }\n\t\t\t}\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 * The text a row shows: its mask until revealed, then the value the\n\t\t * server answered (a server reveal) or the value the record holds.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server\n\t\t */\n\t\tshownValue(row) {\n\t\t\tif (!row.masked || !this.revealed[row.key]) {\n\t\t\t\treturn row.masked || row.value\n\t\t\t}\n\t\t\treturn Object.hasOwn(this.revealedValues, row.key) ? this.revealedValues[row.key] : row.value\n\t\t},\n\n\t\t/**\n\t\t * Ask the server for a masked value (`reveal.url`, tokens resolved,\n\t\t * read at `reveal.path`), never from a cache: each show is a request\n\t\t * the server can check and record.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<string|null>} The value, or null when it could not be read.\n\t\t * @spec openspec/changes/facts-reveal-from-server/specs/facts-widget/spec.md#requirement-a-masked-fact-can-be-revealed-by-the-server\n\t\t */\n\t\tasync fetchRevealed(row) {\n\t\t\tthis.revealing = row.key\n\t\t\tthis.revealErrors = { ...this.revealErrors, [row.key]: '' }\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource({ url: row.revealUrl, responsePath: row.revealPath }, this.tokenCtx, { force: true })\n\t\t\t\tif (payload === null || payload === undefined || payload === '' || typeof payload === 'object') {\n\t\t\t\t\tthrow new Error('no value')\n\t\t\t\t}\n\t\t\t\treturn String(payload)\n\t\t\t} catch {\n\t\t\t\tthis.revealErrors = { ...this.revealErrors, [row.key]: t('nextcloud-vue', 'The value could not be shown.') }\n\t\t\t\treturn null\n\t\t\t} finally {\n\t\t\t\tthis.revealing = ''\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A row's label: the value at `labelPath` (the record, or the page's\n\t\t * `@context.<name>` records) when it holds text, else `label`, both\n\t\t * through the host translate. A case type can name its handler\n\t\t * (\"Regisseur\") while another keeps \"Behandelaar\".\n\t\t *\n\t\t * @param {object} record The record shown.\n\t\t * @param {object} field The entry.\n\t\t * @return {string}\n\t\t * @spec openspec/changes/facts-label-from-path/specs/facts-widget/spec.md#requirement-a-facts-row-can-take-its-label-from-the-record-or-its-context\n\t\t */\n\t\tlabelOf(record, field) {\n\t\t\tif (typeof field.labelPath === 'string' && field.labelPath !== '') {\n\t\t\t\tconst rows = this.cnDetailVisibilityRows && typeof this.cnDetailVisibilityRows === 'object' && 'value' in this.cnDetailVisibilityRows\n\t\t\t\t\t? this.cnDetailVisibilityRows.value\n\t\t\t\t\t: this.cnDetailVisibilityRows\n\t\t\t\tconst named = readPath({ ...(record || {}), '@context': rows || {} }, field.labelPath)\n\t\t\t\tif (typeof named === 'string' && named.trim() !== '') {\n\t\t\t\t\treturn this.tr(named.trim())\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.tr(field.label || '')\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), pill: (object|null)}} `action` is `{ label, icon, run }`, `pill` is `{ label, variant }`.\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\tpill: this.headerPill,\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 = this.entries\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: by id or url, the first match of a query, or\n\t\t * an app endpoint.\n\t\t *\n\t\t * @param {boolean} [force] Read past the shared endpoint cache (a page refresh).\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tasync load(force = false) {\n\t\t\tconst seq = ++this.loadSeq\n\t\t\tconst source = this.resolvedSource\n\t\t\tconst endpoint = this.endpointRequest\n\t\t\tthis.error = false\n\t\t\tconst url = source && !endpoint ? this.sourceUrl(source) : ''\n\t\t\tif ((!endpoint || endpoint.blocked) && !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 data = endpoint\n\t\t\t\t\t? await fetchEndpointSource(this.content.endpointSource, this.tokenCtx, { force })\n\t\t\t\t\t: await this.readSource(source, url)\n\t\t\t\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null\n\t\t\t\t}\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\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.fetched = null\n\t\t\t\t\tthis.error = true\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (seq === this.loadSeq) {\n\t\t\t\t\tthis.loading = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * GET a `source`: the object itself, or the first match of its `filter`.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @param {string} url Its URL.\n\t\t * @return {Promise<unknown>} The object read, or null.\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tasync readSource(source, url) {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url\n\t\t\tconst params = this.sourceQuery(source)\n\t\t\tconst response = params ? await axios.get(full, { params }) : await axios.get(full)\n\t\t\tconst data = response && response.data\n\t\t\tif (!params) {\n\t\t\t\treturn data\n\t\t\t}\n\t\t\tconst first = data && Array.isArray(data.results) ? data.results[0] : null\n\t\t\treturn first ?? null\n\t\t},\n\n\t\t/**\n\t\t * The query of a source that names no `id`: its `filter`, its `sort` as\n\t\t * `_order`, and one result. Null for a source read by id or url.\n\t\t *\n\t\t * @param {object} source The resolved source.\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\n\t\t */\n\t\tsourceQuery(source) {\n\t\t\tif (typeof source.url === 'string' || Object.hasOwn(source, 'id')) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst params = { ...(source.filter && typeof source.filter === 'object' ? source.filter : {}) }\n\t\t\tconst sort = source.sort\n\t\t\tif (sort && typeof sort === 'object' && typeof sort.field === 'string' && sort.field !== '') {\n\t\t\t\tparams[`_order[${sort.field}]`] = sort.dir === 'desc' || sort.direction === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tparams._limit = 1\n\t\t\treturn params\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 * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-facts-can-show-the-first-match-of-a-query\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 && this.sourceQuery(source)) {\n\t\t\t\treturn `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}`\n\t\t\t}\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 * @spec openspec/changes/facts-user-format/specs/facts-widget/spec.md#requirement-a-facts-entry-can-show-a-user-by-name\n\t\t * @spec openspec/changes/facts-actions-and-sources/specs/facts-widget/spec.md#requirement-a-facts-entry-can-fill-a-template\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: spec.itemFormat || '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 === 'year') {\n\t\t\t\tconst date = parseDateValue(raw)\n\t\t\t\treturn date === null ? '' : String(date.getFullYear())\n\t\t\t}\n\t\t\t// A term in words, with the extension at `extendBy` on the record: \"4 weken, te verdagen met 2\".\n\t\t\tif (format === 'duration') {\n\t\t\t\treturn formatTermDuration(raw, typeof spec.extendBy === 'string' && record ? readPath(record, spec.extendBy) : null)\n\t\t\t}\n\t\t\t// The month written out, the year only outside this year: \"3 oktober\".\n\t\t\tif (format === 'longdate') {\n\t\t\t\treturn formatLongDate(raw)\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\t// A number of months in words: whole years as years (\"20 jaar\"), else months.\n\t\t\tif (format === 'months') {\n\t\t\t\tconst months = Number(raw)\n\t\t\t\tif (typeof raw === 'object' || !Number.isInteger(months) || months < 0) {\n\t\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t\t}\n\t\t\t\treturn months > 0 && months % 12 === 0\n\t\t\t\t\t? n('nextcloud-vue', '%n year', '%n years', months / 12)\n\t\t\t\t\t: n('nextcloud-vue', '%n month', '%n months', months)\n\t\t\t}\n\t\t\tif (format === 'bytes') {\n\t\t\t\tconst bytes = Number(raw)\n\t\t\t\treturn Number.isFinite(bytes) && bytes >= 0 ? formatFileSize(bytes) : ''\n\t\t\t}\n\t\t\tif (format === 'user') {\n\t\t\t\treturn userDisplayName(raw)\n\t\t\t}\n\t\t\t// A Nextcloud group by its display name; the id until the name is known.\n\t\t\tif (format === 'group') {\n\t\t\t\tif (typeof raw === 'object') {\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\tconst name = groupDisplayName(String(raw))\n\t\t\t\tif (name === null) {\n\t\t\t\t\tloadGroupDisplayName(String(raw))\n\t\t\t\t\treturn String(raw)\n\t\t\t\t}\n\t\t\t\treturn name\n\t\t\t}\n\t\t\tif (format === 'number') {\n\t\t\t\tconst number = Number(raw)\n\t\t\t\tif (typeof raw === 'object' || !Number.isFinite(number)) {\n\t\t\t\t\treturn typeof raw === 'object' ? '' : String(raw)\n\t\t\t\t}\n\t\t\t\tconst digits = Number.isInteger(spec.decimals) && spec.decimals >= 0 ? { minimumFractionDigits: spec.decimals, maximumFractionDigits: spec.decimals } : {}\n\t\t\t\treturn new Intl.NumberFormat(metricLocale(), digits).format(number)\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__pills {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n}\n\n/* `padding: 0`: Nextcloud's core stylesheet pads every `dl` 12px at the top\n and bottom, which put a side card's facts 24px under its title, against\n the board's 12 (DqZaak \"Behandeling\", DqZaakDocumenten). */\n.cn-facts-widget__rows {\n\tdisplay: grid;\n\tgrid-template-columns: max-content 1fr;\n\tgap: 8px 16px;\n\tmargin: 0;\n\tpadding: 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.cn-facts-widget__row--inline {\n\tdisplay: flex;\n\tgrid-column: 1 / -1;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n.cn-facts-widget__row--inline .cn-facts-widget__value {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 10px;\n}\n\n/* Nextcloud core styles `dt` and `dd` (text-align end, 12px padding, a\n 130px-wide `dt`), which pushed the label to the right of its column, added\n 12px around both and widened the label column to 130px. The class is\n doubled so these resets outrank that rule, and a dossiq-style `:deep()`\n override in an app is not needed. */\n.cn-facts-widget__label.cn-facts-widget__label {\n\twidth: auto;\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__meter {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n\tfont-size: 14px;\n}\n\n.cn-facts-widget__meter-head {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 16px;\n}\n\n.cn-facts-widget__meter-label {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-facts-widget__meter-value {\n\tfont-weight: 500;\n\ttext-align: end;\n}\n\n.cn-facts-widget__meter-track {\n\theight: 6px;\n\tborder-radius: 3px;\n\tbackground: var(--color-background-darker);\n\toverflow: hidden;\n}\n\n.cn-facts-widget__meter-bar {\n\tdisplay: block;\n\theight: 100%;\n\tborder-radius: inherit;\n\tbackground: var(--color-primary-element);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-value {\n\tcolor: var(--color-error-text);\n}\n\n.cn-facts-widget__meter--over .cn-facts-widget__meter-bar {\n\tbackground: var(--color-error);\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__reveal-error {\n\tfont-size: 13px;\n\tcolor: var(--color-text-error, var(--color-error));\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\tcolor: var(--color-main-text);\n\tfont-size: 13px;\n\tfont-weight: 600;\n\tgap: 4px;\n\tmargin-inline-start: 8px;\n\tmin-height: 30px;\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/* Initials stand for a person: round, as an avatar. */\n.cn-facts-widget__lead-tile--initials {\n\tborder-radius: 50%;\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__notice {\n\tmargin: 4px 0 0;\n}\n\n/* A div, not a p: a theme sets every paragraph's size with !important. */\n.cn-facts-widget__footnote {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tline-height: 1.2;\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.cn-facts-widget__link--button {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 0;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tbackground: none;\n\tfont-family: inherit;\n\tcursor: pointer;\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/* One column, label above value: 2px under its label, 10px to the next\n label, so a long label never squeezes its value (DqHandhaving). */\n.cn-facts-widget__rows--stacked {\n\tgrid-template-columns: minmax(0, 1fr);\n\tgap: 2px 0;\n}\n\n.cn-facts-widget__rows--stacked .cn-facts-widget__value.cn-facts-widget__value {\n\tmargin-bottom: 8px;\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":["_openBlock","_createElementBlock","_createTextVNode","_toDisplayString","_createVNode","_Fragment","_renderList","_createBlock","_normalizeClass","_createElementVNode","_resolveDynamicComponent","_normalizeStyle"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import script from './CnObjectDataWidget.vue2.js';
|
|
|
2
2
|
import { render } from './CnObjectDataWidget.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-344df010";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -10,10 +10,12 @@ import Pencil from 'vue-material-design-icons/Pencil.vue';
|
|
|
10
10
|
import __sfc_main$1 from '../CnCellRenderer/CnGroupNameCell.vue.js';
|
|
11
11
|
import __sfc_main$4 from '../CnFormDialog/CnFormDialog.vue.js';
|
|
12
12
|
import { normalizeLook } from '../../composables/useLook.js';
|
|
13
|
+
import { formatTermDuration } from '../../utils/builtInFormatters.js';
|
|
13
14
|
import { isDottedKey, spreadDottedFields, foldDottedFields } from '../../utils/dottedFields.js';
|
|
14
15
|
import { groupDisplayName, searchNextcloudGroups, loadGroupDisplayName } from '../../utils/groupAutocomplete.js';
|
|
15
16
|
import { humaniseKey } from '../../utils/humaniseKey.js';
|
|
16
17
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js';
|
|
18
|
+
import { readPath } from '../../utils/readPath.js';
|
|
17
19
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js';
|
|
18
20
|
import { saveEdited } from '../../utils/saveEdited.js';
|
|
19
21
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js';
|
|
@@ -676,6 +678,12 @@ var script = {
|
|
|
676
678
|
// label, beside a list that already read "Ingediend".
|
|
677
679
|
const enumLabel = this.enumDisplayLabel(field, raw);
|
|
678
680
|
const empty = this.emptyValueFor(field, raw);
|
|
681
|
+
if (empty === null && field.format === 'duration' && raw !== null && raw !== undefined && raw !== '') {
|
|
682
|
+
// A term in words, with the extension at `extendBy` when the object holds one.
|
|
683
|
+
const extension = typeof field.extendBy === 'string' ? readPath(this.objectData || {}, field.extendBy) : null;
|
|
684
|
+
values[field.key] = formatTermDuration(raw, extension);
|
|
685
|
+
continue
|
|
686
|
+
}
|
|
679
687
|
values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}));
|
|
680
688
|
}
|
|
681
689
|
return values
|