@conduction/nextcloud-vue 2.76.0 → 2.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
  6. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  7. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
  8. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  10. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  11. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  12. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  13. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  14. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  24. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  27. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  29. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  31. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  33. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +98 -13
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  38. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  39. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  43. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  45. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  46. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  47. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  48. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  49. package/dist/esm/composables/useListView.js +4 -0
  50. package/dist/esm/composables/useListView.js.map +1 -1
  51. package/dist/esm/index.js +2 -1
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/l10n/en.json.js +25 -1
  54. package/dist/esm/l10n/en.json.js.map +1 -1
  55. package/dist/esm/l10n/nl.json.js +25 -1
  56. package/dist/esm/l10n/nl.json.js.map +1 -1
  57. package/dist/esm/nextcloud-vue.css +288 -74
  58. package/dist/esm/store/plugins/interactions.js +20 -10
  59. package/dist/esm/store/plugins/interactions.js.map +1 -1
  60. package/dist/esm/store/useObjectStore.js +17 -0
  61. package/dist/esm/store/useObjectStore.js.map +1 -1
  62. package/dist/esm/utils/builtInFormatters.js +15 -7
  63. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  64. package/dist/esm/utils/groupAutocomplete.js +63 -1
  65. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  66. package/dist/esm/utils/lensAvailability.js +90 -0
  67. package/dist/esm/utils/lensAvailability.js.map +1 -0
  68. package/dist/esm/utils/personalLenses.js +13 -6
  69. package/dist/esm/utils/personalLenses.js.map +1 -1
  70. package/dist/esm/utils/recordInteractions.js +31 -13
  71. package/dist/esm/utils/recordInteractions.js.map +1 -1
  72. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  73. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  74. package/dist/esm/utils/schema.js +1 -1
  75. package/dist/esm/utils/schema.js.map +1 -1
  76. package/dist/esm/utils/sentinelTokens.js +4 -4
  77. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  78. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  79. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  80. package/dist/esm/utils/widgetVisibility.js +46 -1
  81. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  82. package/dist/nextcloud-vue.cjs.js +3873 -2674
  83. package/dist/nextcloud-vue.cjs.js.map +2 -2
  84. package/dist/nextcloud-vue.css +288 -74
  85. package/l10n/en.json +25 -1
  86. package/l10n/nl.json +25 -1
  87. package/package.json +1 -1
  88. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  89. package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
  90. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  91. package/src/components/CnDataTable/CnDataTable.md +9 -0
  92. package/src/components/CnDataTable/CnDataTable.vue +80 -20
  93. package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
  94. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  95. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  96. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  97. package/src/components/CnIndexPage/CnIndexPage.md +36 -0
  98. package/src/components/CnIndexPage/CnIndexPage.vue +106 -19
  99. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  100. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  101. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  102. package/src/composables/useListView.js +4 -0
  103. package/src/css/look-board-detail.css +40 -2
  104. package/src/css/table.css +82 -17
  105. package/src/index.js +2 -1
  106. package/src/schemas/app-manifest-v2.schema.json +11 -7
  107. package/src/store/plugins/interactions.js +20 -10
  108. package/src/store/useObjectStore.js +17 -0
  109. package/src/utils/builtInFormatters.js +15 -7
  110. package/src/utils/groupAutocomplete.js +75 -0
  111. package/src/utils/index.js +1 -1
  112. package/src/utils/lensAvailability.js +86 -0
  113. package/src/utils/personalLenses.js +13 -6
  114. package/src/utils/recordInteractions.js +31 -13
  115. package/src/utils/resolveFilterTokens.js +45 -2
  116. package/src/utils/schema.js +1 -1
  117. package/src/utils/sentinelTokens.js +4 -4
  118. package/src/utils/validateManifestV2.compiled.js +2 -2
  119. package/src/utils/widgetVisibility.js +45 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CnCellRenderer.vue3.js","sources":["../../../../src/components/CnCellRenderer/CnCellRenderer.vue?vue&type=template&id=37d7b88f&lang.js"],"sourcesContent":["<template>\n\t<span class=\"cn-cell-renderer\" :class=\"cellClass\">\n\t\t<!-- Consumer-registered cell widget (cnCellWidgets[column.widget]) -->\n\t\t<component\n\t\t\t:is=\"widgetComponent\"\n\t\t\tv-if=\"widgetComponent\"\n\t\t\t:value=\"value\"\n\t\t\t:row=\"row\"\n\t\t\t:property=\"property\"\n\t\t\t:formatted=\"formattedValue\"\n\t\t\tv-bind=\"widgetProps\" />\n\n\t\t<!-- Built-in \"badge\" widget — renders the (possibly formatter-shaped) value as a status pill -->\n\t\t<template v-else-if=\"widget === 'badge'\">\n\t\t\t<CnStatusBadge v-if=\"hasValue\"\n\t\t\t\t:label=\"String(formattedValue)\"\n\t\t\t\t:variant=\"badgeVariant\"\n\t\t\t\t:colorMap=\"badgeColorMap\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"fkResolve\" widget — resolves a reference uuid to the related\n\t\t object's display label via the object store (per-schema caching).\n\t\t Config: widgetProps { register, schema, labelField }. -->\n\t\t<template v-else-if=\"widget === 'fkResolve'\">\n\t\t\t<CnFkResolveCell\n\t\t\t\tv-if=\"hasValue\"\n\t\t\t\t:value=\"value\"\n\t\t\t\t:register=\"(widgetProps && widgetProps.register) || ''\"\n\t\t\t\t:schema=\"(widgetProps && widgetProps.schema) || ''\"\n\t\t\t\t:labelField=\"(widgetProps && widgetProps.labelField) || 'name'\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"refLabel\" widget — shows the label of the referenced object(s)\n\t\t from the batch CnIndexPage resolved for the column\n\t\t (widgetProps { labels: { id: label|null }, route? }). An id that is not\n\t\t in `labels` yet shows a placeholder; one that resolved to null shows the\n\t\t id in mono. With widgetProps.route (a manifest page id) each label links\n\t\t to that page for the referenced id. -->\n\t\t<template v-else-if=\"widget === 'refLabel'\">\n\t\t\t<span v-if=\"!refLabelItems.length\" class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__ref-labels\" data-testid=\"cn-cell-ref-labels\">\n\t\t\t\t<template v-for=\"(item, i) in refLabelItems\" :key=\"item.id\">\n\t\t\t\t\t<span v-if=\"i > 0\">, </span>\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"item.to ? 'router-link' : 'span'\"\n\t\t\t\t\t\t:to=\"item.to || undefined\"\n\t\t\t\t\t\t:class=\"[item.to ? 'cn-cell-renderer__link' : '', item.state === 'missing' ? 'cn-cell-renderer__mono' : '']\"\n\t\t\t\t\t\t:title=\"item.state === 'label' ? item.id : undefined\">\n\t\t\t\t\t\t{{ item.text }}\n\t\t\t\t\t</component>\n\t\t\t\t</template>\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"link\" widget — renders the (possibly formatter-shaped) value\n\t\t as a router-link (when widgetProps.route is a manifest page id, or\n\t\t widgetProps.routeMap picks one per row by widgetProps.routeField) or\n\t\t an external anchor (when widgetProps.href is set). Falls back to\n\t\t plain text + a once-per-session console.warn when neither resolves. -->\n\t\t<template v-else-if=\"widget === 'link'\">\n\t\t\t<router-link\n\t\t\t\tv-if=\"linkRoute\"\n\t\t\t\t:to=\"linkRoute\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</router-link>\n\t\t\t<a\n\t\t\t\tv-else-if=\"linkHref\"\n\t\t\t\t:href=\"linkHref\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</a>\n\t\t\t<span v-else :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"avatar\" widget: a person as an avatar with the name beside\n\t\t it. A user id (widgetProps.userField, or widgetProps.user for a cell\n\t\t that holds the id itself) gets the Nextcloud avatar; a free name\n\t\t gets its initials. The picture is decoration: the name is the text. -->\n\t\t<template v-else-if=\"widget === 'avatar'\">\n\t\t\t<span v-if=\"avatarName || avatarUserId\" class=\"cn-cell-renderer__avatar\">\n\t\t\t\t<span v-if=\"avatarUserId\" class=\"cn-cell-renderer__avatar-picture\" aria-hidden=\"true\">\n\t\t\t\t\t<NcAvatar\n\t\t\t\t\t\t:user=\"avatarUserId\"\n\t\t\t\t\t\t:displayName=\"avatarName || avatarUserId\"\n\t\t\t\t\t\t:size=\"avatarSize\"\n\t\t\t\t\t\t:disableMenu=\"true\"\n\t\t\t\t\t\t:disableTooltip=\"true\" />\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else\n\t\t\t\t\tclass=\"cn-cell-renderer__avatar-initials\"\n\t\t\t\t\t:style=\"{ width: avatarSize + 'px', height: avatarSize + 'px' }\"\n\t\t\t\t\taria-hidden=\"true\">{{ avatarInitials }}</span>\n\t\t\t\t<span class=\"cn-cell-renderer__avatar-name\" :title=\"avatarName || avatarUserId\">{{ avatarName || avatarUserId }}</span>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"date\" widget: a date whose colour follows\n\t\t widgetProps.variantWhen, rules on the number of days until it\n\t\t (0 = today, negative = overdue). -->\n\t\t<template v-else-if=\"widget === 'date'\">\n\t\t\t<time\n\t\t\t\tv-if=\"dateTimestamp\"\n\t\t\t\tclass=\"cn-cell-renderer__date\"\n\t\t\t\t:class=\"dateVariant ? 'cn-cell-renderer__date--' + dateVariant : null\"\n\t\t\t\t:datetime=\"dateTimestamp.toISOString()\"\n\t\t\t\t:title=\"rawTitle\">{{ dateLabel }}</time>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Explicit column formatter — overrides the type-aware paths below -->\n\t\t<template v-else-if=\"hasFormatter\">\n\t\t\t<span :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Declarative swatch — a colour dot (from a sibling row field) + text -->\n\t\t<template v-else-if=\"isSwatch\">\n\t\t\t<span class=\"cn-cell-renderer__swatch-wrap\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"swatchColor\"\n\t\t\t\t\tclass=\"cn-cell-renderer__swatch-dot\"\n\t\t\t\t\t:style=\"{ backgroundColor: swatchColor }\"\n\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t<span v-if=\"hasValue\" :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Declarative built-in format (currency / duration / number / percent) -->\n\t\t<template v-else-if=\"hasBuiltinFormat\">\n\t\t\t<span :title=\"rawTitle\" class=\"cn-cell-renderer--number\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Date / date-time: dynamic NcDateTime (relative time, absolute on hover) -->\n\t\t<template v-else-if=\"isDate\">\n\t\t\t<NcDateTime v-if=\"dateTimestamp\" :timestamp=\"dateTimestamp\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- URI / URL: external link -->\n\t\t<template v-else-if=\"isUri\">\n\t\t\t<a\n\t\t\t\tv-if=\"uriHref\"\n\t\t\t\t:href=\"uriHref\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</a>\n\t\t\t<span v-else-if=\"hasValue\" :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Boolean: icon -->\n\t\t<template v-else-if=\"propertyType === 'boolean'\">\n\t\t\t<CheckBold v-if=\"value\" :size=\"16\" class=\"cn-cell-renderer__icon cn-cell-renderer__icon--success\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Enum: status badge (auto-coloured from the property's optional colorMap) -->\n\t\t<template v-else-if=\"isEnum\">\n\t\t\t<CnStatusBadge v-if=\"value\"\n\t\t\t\t:label=\"enumLabel\"\n\t\t\t\t:colorKey=\"String(value)\"\n\t\t\t\t:colorMap=\"enumColorMap\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Array: comma-joined or count -->\n\t\t<template v-else-if=\"propertyType === 'array'\">\n\t\t\t<span v-if=\"!value || !value.length\" class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t<span v-else :title=\"Array.isArray(value) ? value.join(', ') : ''\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Default: formatted text -->\n\t\t<template v-else>\n\t\t\t<span :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { NcAvatar, NcDateTime } from '@nextcloud/vue'\nimport CheckBold from 'vue-material-design-icons/CheckBold.vue'\nimport CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'\nimport { parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\nimport { objectFieldValue } from '../../utils/objectName.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { formatValue } from '../../utils/schema.js'\nimport { CnStatusBadge } from '../CnStatusBadge/index.js'\n\n/** A uuid in any version, the shape OpenRegister ids take. */\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * Module-level set of column keys already warned about for a\n * `widget:\"link\"` declaration with no resolvable target — guarantees\n * one warning per (page, column-key) rather than per row × render.\n */\nconst WARNED_LINK_KEYS = new Set()\n\n/**\n * CnCellRenderer — Type-aware cell renderer for schema-driven tables.\n *\n * Renders a single cell value based on its schema property definition.\n * Booleans render as icons, enums as status badges, dates as formatted strings,\n * and everything else as truncated text via `formatValue()`.\n *\n * ```vue\n * <CnCellRenderer :value=\"row.status\" :property=\"schema.properties.status\" />\n * ```\n */\nexport default {\n\tname: 'CnCellRenderer',\n\n\tcomponents: {\n\t\tCnStatusBadge,\n\t\tCnFkResolveCell,\n\t\tCheckBold,\n\t\tNcAvatar,\n\t\tNcDateTime,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Cell-formatter registry, provided by CnAppRoot (`cnFormatters`).\n\t\t * Map of formatter-id → `(value, row, property) => string|number`.\n\t\t * Defaults to an empty object so standalone use (no CnAppRoot\n\t\t * ancestor) is unaffected.\n\t\t */\n\t\tcnFormatters: { default: () => ({}) },\n\t\t/**\n\t\t * Cell-widget registry, provided by CnAppRoot (`cnCellWidgets`).\n\t\t * Map of widget-id → Vue component. Resolves a column's `widget` id;\n\t\t * the built-in `\"badge\"` is handled inline (no registry entry needed).\n\t\t * Defaults to an empty object.\n\t\t */\n\t\tcnCellWidgets: { default: () => ({}) },\n\t\t/**\n\t\t * Display-layer translation function, provided by CnAppRoot\n\t\t * (`cnTranslate: this.translate`, bound to the host app's id). Applied\n\t\t * to the enum badge label only — the underlying value is untouched.\n\t\t * Defaults to identity so standalone use is unaffected.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The raw cell value */\n\t\tvalue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Schema property definition: { type, format, enum, items, title } */\n\t\tproperty: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional cell-formatter id (e.g. `currency`, `automationTrigger`).\n\t\t * When set and resolvable in the injected `cnFormatters` registry,\n\t\t * the cell renders `cnFormatters[formatter](value, row, property,\n\t\t * formatterOptions)` as text — an explicit override of the type-aware\n\t\t * rendering below.\n\t\t */\n\t\tformatter: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative options map passed as the formatter's fourth argument\n\t\t * (e.g. `{ currency: 'USD' }` for the built-in `currency` formatter,\n\t\t * or `{ negative, zero, positive }` phrases for `conditionalPhrase`).\n\t\t * Undefined-safe: three-argument formatters simply ignore it.\n\t\t */\n\t\tformatterOptions: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional cell-widget id (e.g. `badge`, or a consumer-registered\n\t\t * name). When it resolves in `cnCellWidgets` the cell renders that\n\t\t * component with `{ value, row, property, formatted, ...widgetProps }`;\n\t\t * the built-in id `\"badge\"` renders `CnStatusBadge` and the built-in\n\t\t * id `\"fkResolve\"` renders `CnFkResolveCell` (uuid → related object\n\t\t * label, config via `widgetProps { register, schema, labelField }`),\n\t\t * and the built-in id `\"refLabel\"` shows labels already resolved in a\n\t\t * batch (`widgetProps { labels, route? }`, set by CnIndexPage for a\n\t\t * column with `labelField`).\n\t\t * The built-in id `\"avatar\"` renders a person as an avatar with the\n\t\t * name beside it (`widgetProps { userField?, user?, nameField?, size? }`;\n\t\t * a free name gets its initials), and the built-in id `\"date\"` renders\n\t\t * a date whose colour follows `widgetProps.variantWhen`, rules on the\n\t\t * number of days until the date (`[{ op: \"lt\", value: 0, variant:\n\t\t * \"error\" }, { op: \"lte\", value: 5, variant: \"warning\" }]`).\n\t\t * Takes precedence over `formatter`/the type-aware rendering, but the\n\t\t * value handed to the widget is the formatter-shaped `formatted` when\n\t\t * `formatter` is also set.\n\t\t */\n\t\twidget: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Extra props spread onto the resolved cell-widget component. The\n\t\t * built-in `\"link\"` reads its target from here: `route` (a manifest\n\t\t * page id), `params` (route param to row field), `href` (URL with\n\t\t * `{field}` placeholders), and `routeField` + `routeMap` to pick the\n\t\t * page per row from a sibling field (`{ person: 'ContactDetail' }`),\n\t\t * with `route` as the fallback for a value the map does not hold.\n\t\t */\n\t\twidgetProps: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional declarative cell-format spec — a no-code alternative to a\n\t\t * registry `formatter`. Recognised `style` values:\n\t\t *\n\t\t * - `'currency'` → `Intl.NumberFormat` currency (e.g. `€ 1.234,56`),\n\t\t * honouring `currency` (ISO code, default `'EUR'`) and `decimals`\n\t\t * (default 2).\n\t\t * - `'number'` / `'percent'` → localized number (percent appends `%`),\n\t\t * honouring `decimals` (default 0).\n\t\t * - `'duration'` → a seconds value rendered compact (`1u 23m`, `45m 10s`,\n\t\t * `12s`); pass `unit: 'milliseconds'` / `'minutes'` / `'hours'` when the\n\t\t * raw value is not in seconds. Sub-second millisecond values render as\n\t\t * `245ms` rather than rounding down to `0s`.\n\t\t * - `'swatch'` → a colour dot read from a sibling row field named by\n\t\t * `colorField` (the value renders as the cell text beside it).\n\t\t *\n\t\t * `prefix` / `suffix` are prepended/appended to the numeric styles.\n\t\t * Resolved AFTER `formatter` / `widget` (those win), but BEFORE the\n\t\t * type-aware rendering, so a manifest column can opt into currency or a\n\t\t * colour swatch without registering a function.\n\t\t *\n\t\t * @type {{style?: 'currency'|'number'|'percent'|'duration'|'swatch', currency?: string, decimals?: number, unit?: 'milliseconds'|'seconds'|'minutes'|'hours', prefix?: string, suffix?: string, colorField?: string}}\n\t\t */\n\t\tformat: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The full row object — passed so a formatter can be a function of\n\t\t * the whole record (e.g. \"days since `@self.updated`\"), not just\n\t\t * this one cell value.\n\t\t */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Maximum string length before truncation */\n\t\ttruncate: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/**\n\t\t * Row identifier field — used by the built-in `widget:\"link\"` when\n\t\t * the manifest doesn't specify an explicit `widgetProps.params`\n\t\t * map. Defaults to `'id'` so router-link param resolution works\n\t\t * with the manifest convention (`/x/:id`).\n\t\t */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\tpropertyType() {\n\t\t\treturn this.property?.type || (typeof this.value)\n\t\t},\n\n\t\tisEnum() {\n\t\t\treturn !!(this.property?.enum && this.property.enum.length > 0)\n\t\t},\n\n\t\t/**\n\t\t * Badge text for an enum cell. Schema enum values are English source\n\t\t * codes, so a Dutch session otherwise reads `submitted` in a table whose\n\t\t * headers are Dutch. Only the label is translated — the cell's value,\n\t\t * sorting and filtering keep using the raw code.\n\t\t */\n\t\tenumLabel() {\n\t\t\tconst raw = String(this.value)\n\t\t\tconst labels = (this.property && (this.property.enumLabels || this.property['x-enum-labels'])) || {}\n\t\t\treturn this.cnTranslate(labels[raw] || raw)\n\t\t},\n\n\t\t/** True when the property is a date / date-time (rendered via NcDateTime). */\n\t\tisDate() {\n\t\t\treturn this.property?.format === 'date-time' || this.property?.format === 'date'\n\t\t},\n\n\t\t/**\n\t\t * The cell value as a Date for NcDateTime, or `null` when absent or\n\t\t * unparseable (the cell then falls back to a dash / plain text).\n\t\t *\n\t\t * @return {Date|null}\n\t\t */\n\t\tdateTimestamp() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst date = new Date(this.value)\n\t\t\treturn Number.isNaN(date.getTime()) ? null : date\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the Nextcloud user id whose avatar to show, or ''\n\t\t * for a free name. Read from the row field `widgetProps.userField`,\n\t\t * or from the cell value itself when `widgetProps.user` is true.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarUserId() {\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tlet id = ''\n\t\t\tif (typeof props.userField === 'string' && props.userField !== '') {\n\t\t\t\tid = objectFieldValue(this.row, props.userField)\n\t\t\t} else if (props.user === true) {\n\t\t\t\tid = this.value\n\t\t\t}\n\t\t\treturn (typeof id === 'string' || typeof id === 'number') ? String(id) : ''\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the name shown beside the picture. Read from the\n\t\t * row field `widgetProps.nameField` when set, else the cell value.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarName() {\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tif (typeof props.nameField === 'string' && props.nameField !== '') {\n\t\t\t\tconst name = objectFieldValue(this.row, props.nameField)\n\t\t\t\tif (typeof name === 'string' && name !== '') {\n\t\t\t\t\treturn name\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!this.hasValue || typeof this.value === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn String(this.hasFormatter ? this.formattedValue : this.value)\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the initials of a free name, at most two letters\n\t\t * (first and last word).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarInitials() {\n\t\t\tconst words = this.avatarName.trim().split(/\\s+/).filter((word) => word !== '')\n\t\t\tif (words.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = Array.from(words[0])[0] || ''\n\t\t\tconst last = words.length > 1 ? (Array.from(words[words.length - 1])[0] || '') : ''\n\t\t\treturn (first + last).toLocaleUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the picture size in pixels (`widgetProps.size`, default 24).\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarSize() {\n\t\t\tconst size = Number(this.widgetProps && this.widgetProps.size)\n\t\t\treturn Number.isFinite(size) && size > 0 ? size : 24\n\t\t},\n\n\t\t/**\n\t\t * `date` widget: the variant of the first `widgetProps.variantWhen`\n\t\t * rule the date matches, or '' for none.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tdateVariant() {\n\t\t\tconst rules = this.widgetProps && this.widgetProps.variantWhen\n\t\t\tconst variant = resolveDateVariant(this.value, rules)\n\t\t\treturn variant === 'default' ? '' : variant\n\t\t},\n\n\t\t/**\n\t\t * `date` widget: the date written out in the user's locale.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes\n\t\t */\n\t\tdateLabel() {\n\t\t\tconst date = parseDateValue(this.value)\n\t\t\tif (date === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// `widgetProps.showTime: true` adds the time of day (\"5 Oct 2026,\n\t\t\t// 08:30\"); `widgetProps.timeOnly: true` shows just the time\n\t\t\t// (\"08:30\"), for a timetable row. Default: the date alone.\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tconst options = props.timeOnly === true\n\t\t\t\t? { hour: '2-digit', minute: '2-digit' }\n\t\t\t\t: { day: 'numeric', month: 'short', year: 'numeric', ...(props.showTime === true ? { hour: '2-digit', minute: '2-digit' } : {}) }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(date)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(date)\n\t\t\t}\n\t\t},\n\n\t\t/** True when the property is a URI / URL (rendered as an external link). */\n\t\tisUri() {\n\t\t\treturn this.property?.format === 'uri' || this.property?.format === 'url'\n\t\t},\n\n\t\t/**\n\t\t * Safe external href for a URI/URL cell, or `null` when the value is\n\t\t * absent or fails `safeHref` validation (e.g. an unsafe scheme).\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\turiHref() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn safeHref(String(this.value))\n\t\t},\n\n\t\t/** True when the cell has a renderable value (not null/undefined/empty string). */\n\t\thasValue() {\n\t\t\treturn this.value !== null && this.value !== undefined && this.value !== ''\n\t\t},\n\n\t\t/**\n\t\t * Resolved cell-widget component for this column, or `null`. A column's\n\t\t * `widget` id resolves against the injected `cnCellWidgets` registry;\n\t\t * the built-in `\"badge\"` is NOT resolved here (handled inline in the\n\t\t * template) so apps can still override `\"badge\"` via the registry.\n\t\t *\n\t\t * @return {import('vue').Component|null}\n\t\t */\n\t\twidgetComponent() {\n\t\t\tif (!this.widget) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst c = this.cnCellWidgets && this.cnCellWidgets[this.widget]\n\t\t\treturn c || null\n\t\t},\n\n\t\t/** Variant for the built-in `badge` widget — `widgetProps.variant` or `'default'`. */\n\t\tbadgeVariant() {\n\t\t\treturn (this.widgetProps && this.widgetProps.variant) || 'default'\n\t\t},\n\n\t\t/**\n\t\t * Color map for the built-in `badge` widget — `widgetProps.colorMap`,\n\t\t * a `{ value: variant }` map (e.g. `{ placed: 'primary', delivered: 'success' }`)\n\t\t * that CnStatusBadge resolves per label. Null when unset (falls back to\n\t\t * `badgeVariant`). Lets a manifest colour a status column without code.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tbadgeColorMap() {\n\t\t\treturn (this.widgetProps && this.widgetProps.colorMap) || null\n\t\t},\n\n\t\t/**\n\t\t * Color map for the auto-rendered enum badge — read from the schema\n\t\t * property's optional `colorMap` (or `x-color-map`). Null when unset, so\n\t\t * enum cells stay the default grey unless the schema opts into colours.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tenumColorMap() {\n\t\t\treturn (this.property && (this.property.colorMap || this.property['x-color-map'])) || null\n\t\t},\n\n\t\t/**\n\t\t * The manifest page id the built-in `widget:\"link\"` routes this row\n\t\t * to, or null.\n\t\t *\n\t\t * `widgetProps.routeField` names a sibling field on the row, and\n\t\t * `widgetProps.routeMap` maps that field's values to page ids, so one\n\t\t * column can link a person to `ContactDetail` and an organisation to\n\t\t * `OrganisationDetail` off `initiatorType`. A row whose value has no\n\t\t * entry in the map falls back to the fixed `widgetProps.route`, and\n\t\t * without one the cell renders as text. The map is the whole\n\t\t * vocabulary: a row value is only ever used as a KEY into it, never\n\t\t * as a route name itself, so row data cannot send a link to a page\n\t\t * the manifest did not name.\n\t\t *\n\t\t * @return {string|null} The page id, or null when none resolves.\n\t\t */\n\t\tlinkRouteName() {\n\t\t\tconst wp = this.widgetProps || {}\n\t\t\tconst map = wp.routeMap\n\t\t\tif (typeof wp.routeField === 'string' && wp.routeField !== '' && map && typeof map === 'object') {\n\t\t\t\tconst key = this.row ? this.row[wp.routeField] : undefined\n\t\t\t\tif (key !== undefined && key !== null && Object.hasOwn(map, String(key))) {\n\t\t\t\t\tconst mapped = map[String(key)]\n\t\t\t\t\tif (typeof mapped === 'string' && mapped !== '') {\n\t\t\t\t\t\treturn mapped\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn (typeof wp.route === 'string' && wp.route !== '') ? wp.route : null\n\t\t},\n\n\t\t/**\n\t\t * Items of the built-in `widget:\"refLabel\"`: one per referenced id with\n\t\t * its text, state (`label`, `loading` or `missing`) and optional link.\n\t\t *\n\t\t * @return {Array<{id: string, text: string, state: string, to: (object|null)}>}\n\t\t */\n\t\trefLabelItems() {\n\t\t\tif (this.widget !== 'refLabel') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst wp = this.widgetProps || {}\n\t\t\tconst labels = wp.labels || {}\n\t\t\tconst raw = this.value\n\t\t\tconst list = Array.isArray(raw) ? raw : [raw]\n\t\t\treturn list\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '')\n\t\t\t\t.map((id) => {\n\t\t\t\t\tconst key = String(id)\n\t\t\t\t\tconst known = Object.hasOwn(labels, key)\n\t\t\t\t\tconst label = known ? labels[key] : undefined\n\t\t\t\t\tconst state = !known ? 'loading' : (label ? 'label' : 'missing')\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: key,\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\ttext: state === 'label' ? label : (state === 'loading' ? '…' : key),\n\t\t\t\t\t\tto: (state === 'label' && typeof wp.route === 'string' && wp.route !== '') ? { name: wp.route, params: { id: key } } : null,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Resolved router-link target for the built-in `widget:\"link\"`. When\n\t\t * a page id resolves (`widgetProps.route`, or the row's entry in\n\t\t * `widgetProps.routeMap`, see `linkRouteName`), returns\n\t\t * `{ name: route, params }`. Param map is `widgetProps.params`\n\t\t * (a map of route-param-name → row-field-name); when omitted,\n\t\t * defaults to `{ id: row[rowKey] }`. Returns null when no page id\n\t\t * resolves (the template falls through to `linkHref` or plain\n\t\t * text).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tlinkRoute() {\n\t\t\tif (this.widget !== 'link') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst route = this.linkRouteName\n\t\t\tif (!route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst paramMap = (this.widgetProps && this.widgetProps.params)\n\t\t\t\t|| { id: this.rowKey || 'id' }\n\t\t\tconst params = {}\n\t\t\tfor (const [routeParam, rowField] of Object.entries(paramMap)) {\n\t\t\t\tparams[routeParam] = this.row && this.row[rowField]\n\t\t\t}\n\t\t\treturn { name: route, params }\n\t\t},\n\n\t\t/**\n\t\t * Resolved external href for the built-in `widget:\"link\"` when\n\t\t * `widgetProps.href` is set. `{key}` placeholders in the href\n\t\t * are substituted from the row (`\"/x/{id}\"` + `row.id === \"42\"`\n\t\t * → `\"/x/42\"`). The final computed value is validated with\n\t\t * `safeHref` so row-injected values cannot introduce unsafe\n\t\t * schemes: a `javascript:` or `data:` result renders as plain text\n\t\t * instead of a link. Returns null when `href` isn't set.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tlinkHref() {\n\t\t\tif (this.widget !== 'link') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst href = this.widgetProps && this.widgetProps.href\n\t\t\tif (!href) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = String(href).replace(/\\{(\\w+)\\}/g, (_, key) => this.row && this.row[key] !== null && this.row[key] !== undefined ? String(this.row[key]) : '')\n\t\t\t// The safeHref retrofit (18700fd94) put this check here and a later\n\t\t\t// lint pass (e5ea00d51) dropped it, leaving the docblock above\n\t\t\t// promising a check the code no longer made: a template of `{url}`\n\t\t\t// put a row's `javascript:` value straight into `:href`. safeHref\n\t\t\t// answers '#' for anything unsafe; that is not a link worth\n\t\t\t// rendering, so fall through to plain text.\n\t\t\tconst safe = safeHref(resolved)\n\t\t\treturn safe === '#' ? null : safe\n\t\t},\n\n\t\t/**\n\t\t * Resolved formatter function for this cell, or `null`. A column's\n\t\t * `formatter` id resolves against the injected `cnFormatters` registry.\n\t\t *\n\t\t * @return {((value: unknown, row: object, property: object, options: object) => unknown)|null}\n\t\t */\n\t\tformatterFn() {\n\t\t\tconst fn = this.formatter && this.cnFormatters && this.cnFormatters[this.formatter]\n\t\t\treturn typeof fn === 'function' ? fn : null\n\t\t},\n\n\t\t/** True when an explicit formatter is in play for this cell. */\n\t\thasFormatter() {\n\t\t\treturn this.formatterFn !== null\n\t\t},\n\n\t\t/**\n\t\t * The declarative format `style`, or null when no (recognised) `format`\n\t\t * spec is set. Drives the built-in currency / duration / number / percent\n\t\t * / swatch rendering paths.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tformatStyle() {\n\t\t\tconst s = this.format && this.format.style\n\t\t\treturn typeof s === 'string' ? s : null\n\t\t},\n\n\t\t/** True when a built-in NUMERIC format (currency/number/percent/duration) applies. */\n\t\thasBuiltinFormat() {\n\t\t\treturn ['currency', 'number', 'percent', 'duration'].includes(this.formatStyle)\n\t\t},\n\n\t\t/** True when this cell renders as a colour swatch (`format.style:\"swatch\"`). */\n\t\tisSwatch() {\n\t\t\treturn this.formatStyle === 'swatch'\n\t\t},\n\n\t\t/**\n\t\t * The swatch colour — read from the sibling row field named by\n\t\t * `format.colorField` (default `'color'`). Returns null when no usable\n\t\t * colour string is present (the dot is then omitted).\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tswatchColor() {\n\t\t\tif (!this.isSwatch) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = (this.format && this.format.colorField) || 'color'\n\t\t\tconst c = this.row && this.row[field]\n\t\t\treturn (typeof c === 'string' && c.trim() !== '') ? c : null\n\t\t},\n\n\t\tformattedValue() {\n\t\t\tif (this.formatterFn) {\n\t\t\t\ttry {\n\t\t\t\t\treturn this.formatterFn(this.value, this.row, this.property, this.formatterOptions || undefined)\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnCellRenderer] formatter \"${this.formatter}\" threw; falling back`, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.hasBuiltinFormat) {\n\t\t\t\treturn this.applyBuiltinFormat()\n\t\t\t}\n\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t},\n\n\t\trawTitle() {\n\t\t\t// Show full value on hover if it was truncated\n\t\t\tconst raw = this.value\n\t\t\tif (typeof raw === 'string' && raw.length > this.truncate) {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the cell draws a uuid, and so takes the monospace uuid\n\t\t * style. The property's `format: \"uuid\"` alone used to decide this,\n\t\t * so a reference that a formatter or a widget had resolved to its\n\t\t * name was still drawn as code. Now the value has to be a uuid AND\n\t\t * nothing resolves it: a resolving widget or formatter, or a value\n\t\t * that is not a uuid, renders in the normal text style.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-resolved-reference-is-not-drawn-as-a-uuid\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsUuid() {\n\t\t\tif (this.property?.format !== 'uuid') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.widgetComponent || this.widget || this.hasFormatter) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn typeof this.value === 'string' && UUID_PATTERN.test(this.value)\n\t\t},\n\n\t\tcellClass() {\n\t\t\tconst classes = []\n\t\t\tif (this.propertyType === 'boolean') {\n\t\t\t\tclasses.push('cn-cell-renderer--boolean')\n\t\t\t}\n\t\t\tif (this.isEnum) {\n\t\t\t\tclasses.push('cn-cell-renderer--enum')\n\t\t\t}\n\t\t\tif (this.property?.format === 'date-time' || this.property?.format === 'date') {\n\t\t\t\tclasses.push('cn-cell-renderer--date')\n\t\t\t}\n\t\t\tif (this.showsUuid) {\n\t\t\t\tclasses.push('cn-cell-renderer--uuid')\n\t\t\t}\n\t\t\tif (this.propertyType === 'integer' || this.propertyType === 'number') {\n\t\t\t\tclasses.push('cn-cell-renderer--number')\n\t\t\t}\n\t\t\treturn classes\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// Warn once per column-property-name when `widget:\"link\"` resolves\n\t\t// to neither a `route` nor an `href` — silent fallback hides\n\t\t// manifest mistakes, an every-row warn is too noisy.\n\t\t// A column with a `routeMap` has declared its targets; a row whose\n\t\t// value is not in the map (an empty requester, say) is data, not a\n\t\t// manifest mistake, so it renders as text without the warning. The\n\t\t// same holds for a declared `href` whose resolved value safeHref\n\t\t// refused: the manifest is fine, the row value is not.\n\t\tif (\n\t\t\tthis.widget === 'link'\n\t\t\t&& !this.linkRoute\n\t\t\t&& !this.widgetProps?.href\n\t\t\t&& this.widgetProps?.fallback !== 'silent'\n\t\t\t&& !(this.widgetProps?.routeMap && typeof this.widgetProps.routeMap === 'object')\n\t\t) {\n\t\t\tconst key = this.property?.title || String(this.value).slice(0, 20)\n\t\t\tif (!WARNED_LINK_KEYS.has(key)) {\n\t\t\t\tWARNED_LINK_KEYS.add(key)\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnCellRenderer] widget:\"link\" on \"${key}\" has no resolvable target — set widgetProps.route (page id) or widgetProps.href (URL with optional {field} placeholders); cell falls back to plain text.`)\n\t\t\t}\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Render the cell value per the declarative `format` spec (currency /\n\t\t * number / percent / duration). Mirrors CnStatWidget's formatting so a\n\t\t * KPI tile and a table column read identically. Non-numeric values fall\n\t\t * back to `formatValue`; an empty value renders an em-dash.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tapplyBuiltinFormat() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tconst fmt = this.format || {}\n\t\t\tif (fmt.style === 'duration') {\n\t\t\t\treturn this.formatDuration()\n\t\t\t}\n\t\t\tconst num = Number(this.value)\n\t\t\tif (!Number.isFinite(num)) {\n\t\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t\t}\n\t\t\tconst decimals = Number.isFinite(fmt.decimals)\n\t\t\t\t? fmt.decimals\n\t\t\t\t: (fmt.style === 'currency' ? 2 : 0)\n\t\t\tlet body\n\t\t\tif (fmt.style === 'currency') {\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(fmt.currency),\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num)\n\t\t\t} else if (fmt.style === 'percent') {\n\t\t\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num) + '%'\n\t\t\t} else {\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num)\n\t\t\t}\n\t\t\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n\t\t},\n\n\t\t/**\n\t\t * Render a numeric duration compactly (`1u 23m`, `45m 10s`, `12s`). The\n\t\t * raw value is seconds unless `format.unit` is `'milliseconds'` /\n\t\t * `'minutes'` / `'hours'`. The hour suffix is `u` (uur) to read naturally\n\t\t * under NL theming while staying digit-led for other locales.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tformatDuration() {\n\t\t\tconst fmt = this.format || {}\n\t\t\tlet secs = Number(this.value)\n\t\t\tif (!Number.isFinite(secs)) {\n\t\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t\t}\n\t\t\tif (fmt.unit === 'milliseconds') {\n\t\t\t\t// Sub-second durations are the common case for a millisecond\n\t\t\t\t// field (request/step timings), and they would all collapse to\n\t\t\t\t// the `0s` floor below — so render them in ms directly.\n\t\t\t\tconst ms = Math.round(secs)\n\t\t\t\tif (Math.abs(ms) < 1000) {\n\t\t\t\t\treturn `${fmt.prefix || ''}${ms}ms${fmt.suffix || ''}`\n\t\t\t\t}\n\t\t\t\tsecs = ms / 1000\n\t\t\t} else if (fmt.unit === 'minutes') {\n\t\t\t\tsecs *= 60\n\t\t\t} else if (fmt.unit === 'hours') {\n\t\t\t\tsecs *= 3600\n\t\t\t}\n\t\t\tsecs = Math.round(secs)\n\t\t\tconst sign = secs < 0 ? '-' : ''\n\t\t\tsecs = Math.abs(secs)\n\t\t\tconst h = Math.floor(secs / 3600)\n\t\t\tconst m = Math.floor((secs % 3600) / 60)\n\t\t\tconst s = secs % 60\n\t\t\tconst parts = []\n\t\t\tif (h > 0) {\n\t\t\t\tparts.push(`${h}u`)\n\t\t\t}\n\t\t\tif (m > 0) {\n\t\t\t\tparts.push(`${m}m`)\n\t\t\t}\n\t\t\tif (s > 0 && h === 0) {\n\t\t\t\tparts.push(`${s}s`)\n\t\t\t}\n\t\t\tif (parts.length === 0) {\n\t\t\t\tparts.push('0s')\n\t\t\t}\n\t\t\treturn `${fmt.prefix || ''}${sign}${parts.join(' ')}${fmt.suffix || ''}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-cell-renderer__mono {\n\tfont-family: monospace;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer--uuid {\n\tfont-family: monospace;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer--number {\n\tfont-variant-numeric: tabular-nums;\n}\n\n.cn-cell-renderer--date {\n\twhite-space: nowrap;\n}\n\n.cn-cell-renderer__dash {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer__avatar {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tmax-width: 100%;\n\tvertical-align: middle;\n}\n\n.cn-cell-renderer__avatar-picture {\n\tdisplay: inline-flex;\n\tflex: none;\n}\n\n.cn-cell-renderer__avatar-initials {\n\tdisplay: inline-flex;\n\tflex: none;\n\talign-items: center;\n\tjustify-content: center;\n\tborder-radius: 50%;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-size: 0.75em;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n\n.cn-cell-renderer__avatar-name {\n\tmin-width: 0;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n/* A date that matched a rule is also set in a heavier weight, so the signal\n does not rest on colour alone. */\n.cn-cell-renderer__date--success,\n.cn-cell-renderer__date--warning,\n.cn-cell-renderer__date--error {\n\tfont-weight: 600;\n}\n\n.cn-cell-renderer__date--success {\n\tcolor: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-cell-renderer__date--warning {\n\tcolor: var(--color-text-warning, var(--color-warning-text));\n}\n\n.cn-cell-renderer__date--error {\n\tcolor: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-cell-renderer__icon--success {\n\tcolor: var(--color-success);\n}\n\n.cn-cell-renderer__swatch-wrap {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-cell-renderer__swatch-dot {\n\tdisplay: inline-block;\n\twidth: 12px;\n\theight: 12px;\n\tborder-radius: 50%;\n\tflex-shrink: 0;\n\tborder: 1px solid var(--color-border-dark);\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createBlock","_resolveDynamicComponent","_mergeProps","_Fragment","_openBlock","_renderList","_createTextVNode","_toDisplayString","_createVNode","_normalizeStyle","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnCellRenderer.vue3.js","sources":["../../../../src/components/CnCellRenderer/CnCellRenderer.vue?vue&type=template&id=37d7b88f&lang.js"],"sourcesContent":["<template>\n\t<span class=\"cn-cell-renderer\" :class=\"cellClass\">\n\t\t<!-- Consumer-registered cell widget (cnCellWidgets[column.widget]) -->\n\t\t<component\n\t\t\t:is=\"widgetComponent\"\n\t\t\tv-if=\"widgetComponent\"\n\t\t\t:value=\"value\"\n\t\t\t:row=\"row\"\n\t\t\t:property=\"property\"\n\t\t\t:formatted=\"formattedValue\"\n\t\t\tv-bind=\"widgetProps\" />\n\n\t\t<!-- Built-in \"group\" widget: a Nextcloud group id (or a list of them)\n\t\t shown as the group's display name, cached per id. Used on its own\n\t\t for a property marked `referenceType: nextcloud-group` /\n\t\t `format: nc-group` unless the column has a formatter. -->\n\t\t<template v-else-if=\"isGroupCell\">\n\t\t\t<span v-if=\"!groupIds.length\" class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__groups\">\n\t\t\t\t<template v-for=\"(gid, i) in groupIds\" :key=\"gid\">\n\t\t\t\t\t<span v-if=\"i > 0\">, </span>\n\t\t\t\t\t<CnGroupNameCell :gid=\"gid\" />\n\t\t\t\t</template>\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"badge\" widget — renders the (possibly formatter-shaped) value as a status pill -->\n\t\t<template v-else-if=\"widget === 'badge'\">\n\t\t\t<CnStatusBadge v-if=\"hasValue\"\n\t\t\t\t:label=\"String(formattedValue)\"\n\t\t\t\t:variant=\"badgeVariant\"\n\t\t\t\t:colorMap=\"badgeColorMap\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"fkResolve\" widget — resolves a reference uuid to the related\n\t\t object's display label via the object store (per-schema caching).\n\t\t Config: widgetProps { register, schema, labelField }. -->\n\t\t<template v-else-if=\"widget === 'fkResolve'\">\n\t\t\t<CnFkResolveCell\n\t\t\t\tv-if=\"hasValue\"\n\t\t\t\t:value=\"value\"\n\t\t\t\t:register=\"(widgetProps && widgetProps.register) || ''\"\n\t\t\t\t:schema=\"(widgetProps && widgetProps.schema) || ''\"\n\t\t\t\t:labelField=\"(widgetProps && widgetProps.labelField) || 'name'\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"refLabel\" widget — shows the label of the referenced object(s)\n\t\t from the batch CnIndexPage resolved for the column\n\t\t (widgetProps { labels: { id: label|null }, route? }). An id that is not\n\t\t in `labels` yet shows a placeholder; one that resolved to null shows the\n\t\t id in mono. With widgetProps.route (a manifest page id) each label links\n\t\t to that page for the referenced id. -->\n\t\t<template v-else-if=\"widget === 'refLabel'\">\n\t\t\t<span v-if=\"!refLabelItems.length\" class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__ref-labels\" data-testid=\"cn-cell-ref-labels\">\n\t\t\t\t<template v-for=\"(item, i) in refLabelItems\" :key=\"item.id\">\n\t\t\t\t\t<span v-if=\"i > 0\">, </span>\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"item.to ? 'router-link' : 'span'\"\n\t\t\t\t\t\t:to=\"item.to || undefined\"\n\t\t\t\t\t\t:class=\"[item.to ? 'cn-cell-renderer__link' : '', item.state === 'missing' ? 'cn-cell-renderer__mono' : '']\"\n\t\t\t\t\t\t:title=\"item.state === 'label' ? item.id : undefined\">\n\t\t\t\t\t\t{{ item.text }}\n\t\t\t\t\t</component>\n\t\t\t\t</template>\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"link\" widget — renders the (possibly formatter-shaped) value\n\t\t as a router-link (when widgetProps.route is a manifest page id, or\n\t\t widgetProps.routeMap picks one per row by widgetProps.routeField) or\n\t\t an external anchor (when widgetProps.href is set). Falls back to\n\t\t plain text + a once-per-session console.warn when neither resolves. -->\n\t\t<template v-else-if=\"widget === 'link'\">\n\t\t\t<router-link\n\t\t\t\tv-if=\"linkRoute\"\n\t\t\t\t:to=\"linkRoute\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</router-link>\n\t\t\t<a\n\t\t\t\tv-else-if=\"linkHref\"\n\t\t\t\t:href=\"linkHref\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</a>\n\t\t\t<span v-else :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"avatar\" widget: a person as an avatar with the name beside\n\t\t it. A user id (widgetProps.userField, or widgetProps.user for a cell\n\t\t that holds the id itself) gets the Nextcloud avatar; a free name\n\t\t gets its initials. The picture is decoration: the name is the text. -->\n\t\t<template v-else-if=\"widget === 'avatar'\">\n\t\t\t<span v-if=\"avatarName || avatarUserId\" class=\"cn-cell-renderer__avatar\">\n\t\t\t\t<span v-if=\"avatarUserId\" class=\"cn-cell-renderer__avatar-picture\" aria-hidden=\"true\">\n\t\t\t\t\t<NcAvatar\n\t\t\t\t\t\t:user=\"avatarUserId\"\n\t\t\t\t\t\t:displayName=\"avatarName || avatarUserId\"\n\t\t\t\t\t\t:size=\"avatarSize\"\n\t\t\t\t\t\t:disableMenu=\"true\"\n\t\t\t\t\t\t:disableTooltip=\"true\" />\n\t\t\t\t</span>\n\t\t\t\t<span\n\t\t\t\t\tv-else\n\t\t\t\t\tclass=\"cn-cell-renderer__avatar-initials\"\n\t\t\t\t\t:style=\"{ width: avatarSize + 'px', height: avatarSize + 'px' }\"\n\t\t\t\t\taria-hidden=\"true\">{{ avatarInitials }}</span>\n\t\t\t\t<span class=\"cn-cell-renderer__avatar-name\" :title=\"avatarName || avatarUserId\">{{ avatarName || avatarUserId }}</span>\n\t\t\t</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Built-in \"date\" widget: a date whose colour follows\n\t\t widgetProps.variantWhen, rules on the number of days until it\n\t\t (0 = today, negative = overdue). -->\n\t\t<template v-else-if=\"widget === 'date'\">\n\t\t\t<time\n\t\t\t\tv-if=\"dateTimestamp\"\n\t\t\t\tclass=\"cn-cell-renderer__date\"\n\t\t\t\t:class=\"dateVariant ? 'cn-cell-renderer__date--' + dateVariant : null\"\n\t\t\t\t:datetime=\"dateTimestamp.toISOString()\"\n\t\t\t\t:title=\"rawTitle\">{{ dateLabel }}</time>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Explicit column formatter — overrides the type-aware paths below -->\n\t\t<template v-else-if=\"hasFormatter\">\n\t\t\t<span :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Declarative swatch — a colour dot (from a sibling row field) + text -->\n\t\t<template v-else-if=\"isSwatch\">\n\t\t\t<span class=\"cn-cell-renderer__swatch-wrap\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"swatchColor\"\n\t\t\t\t\tclass=\"cn-cell-renderer__swatch-dot\"\n\t\t\t\t\t:style=\"{ backgroundColor: swatchColor }\"\n\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t<span v-if=\"hasValue\" :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Declarative built-in format (currency / duration / number / percent) -->\n\t\t<template v-else-if=\"hasBuiltinFormat\">\n\t\t\t<span :title=\"rawTitle\" class=\"cn-cell-renderer--number\">{{ formattedValue }}</span>\n\t\t</template>\n\n\t\t<!-- Date / date-time: dynamic NcDateTime (relative time, absolute on hover) -->\n\t\t<template v-else-if=\"isDate\">\n\t\t\t<NcDateTime v-if=\"dateTimestamp\" :timestamp=\"dateTimestamp\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- URI / URL: external link -->\n\t\t<template v-else-if=\"isUri\">\n\t\t\t<a\n\t\t\t\tv-if=\"uriHref\"\n\t\t\t\t:href=\"uriHref\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\trel=\"noopener\"\n\t\t\t\tclass=\"cn-cell-renderer__link\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</a>\n\t\t\t<span v-else-if=\"hasValue\" :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Boolean: icon -->\n\t\t<template v-else-if=\"propertyType === 'boolean'\">\n\t\t\t<CheckBold v-if=\"value\" :size=\"16\" class=\"cn-cell-renderer__icon cn-cell-renderer__icon--success\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Enum: status badge (auto-coloured from the property's optional colorMap) -->\n\t\t<template v-else-if=\"isEnum\">\n\t\t\t<CnStatusBadge v-if=\"value\"\n\t\t\t\t:label=\"enumLabel\"\n\t\t\t\t:colorKey=\"String(value)\"\n\t\t\t\t:colorMap=\"enumColorMap\" />\n\t\t\t<span v-else class=\"cn-cell-renderer__dash\">—</span>\n\t\t</template>\n\n\t\t<!-- Array: comma-joined or count -->\n\t\t<template v-else-if=\"propertyType === 'array'\">\n\t\t\t<span v-if=\"!value || !value.length\" class=\"cn-cell-renderer__dash\">—</span>\n\t\t\t<span v-else :title=\"Array.isArray(value) ? value.join(', ') : ''\">\n\t\t\t\t{{ formattedValue }}\n\t\t\t</span>\n\t\t</template>\n\n\t\t<!-- Default: formatted text -->\n\t\t<template v-else>\n\t\t\t<span :title=\"rawTitle\">{{ formattedValue }}</span>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { getCanonicalLocale } from '@nextcloud/l10n'\nimport { NcAvatar, NcDateTime } from '@nextcloud/vue'\nimport CheckBold from 'vue-material-design-icons/CheckBold.vue'\nimport CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'\nimport CnGroupNameCell from './CnGroupNameCell.vue'\nimport { parseDateValue, resolveDateVariant } from '../../utils/dateVariant.js'\nimport { safeCurrencyCode } from '../../utils/formatMetric.js'\nimport { objectFieldValue } from '../../utils/objectName.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { formatValue, isGroupProp } from '../../utils/schema.js'\nimport { CnStatusBadge } from '../CnStatusBadge/index.js'\n\n/** A uuid in any version, the shape OpenRegister ids take. */\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/**\n * Module-level set of column keys already warned about for a\n * `widget:\"link\"` declaration with no resolvable target — guarantees\n * one warning per (page, column-key) rather than per row × render.\n */\nconst WARNED_LINK_KEYS = new Set()\n\n/**\n * CnCellRenderer — Type-aware cell renderer for schema-driven tables.\n *\n * Renders a single cell value based on its schema property definition.\n * Booleans render as icons, enums as status badges, dates as formatted strings,\n * and everything else as truncated text via `formatValue()`.\n *\n * ```vue\n * <CnCellRenderer :value=\"row.status\" :property=\"schema.properties.status\" />\n * ```\n */\nexport default {\n\tname: 'CnCellRenderer',\n\n\tcomponents: {\n\t\tCnStatusBadge,\n\t\tCnFkResolveCell,\n\t\tCnGroupNameCell,\n\t\tCheckBold,\n\t\tNcAvatar,\n\t\tNcDateTime,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Cell-formatter registry, provided by CnAppRoot (`cnFormatters`).\n\t\t * Map of formatter-id → `(value, row, property) => string|number`.\n\t\t * Defaults to an empty object so standalone use (no CnAppRoot\n\t\t * ancestor) is unaffected.\n\t\t */\n\t\tcnFormatters: { default: () => ({}) },\n\t\t/**\n\t\t * Cell-widget registry, provided by CnAppRoot (`cnCellWidgets`).\n\t\t * Map of widget-id → Vue component. Resolves a column's `widget` id;\n\t\t * the built-in `\"badge\"` is handled inline (no registry entry needed).\n\t\t * Defaults to an empty object.\n\t\t */\n\t\tcnCellWidgets: { default: () => ({}) },\n\t\t/**\n\t\t * Display-layer translation function, provided by CnAppRoot\n\t\t * (`cnTranslate: this.translate`, bound to the host app's id). Applied\n\t\t * to the enum badge label only — the underlying value is untouched.\n\t\t * Defaults to identity so standalone use is unaffected.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** The raw cell value */\n\t\tvalue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Schema property definition: { type, format, enum, items, title } */\n\t\tproperty: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional cell-formatter id (e.g. `currency`, `automationTrigger`).\n\t\t * When set and resolvable in the injected `cnFormatters` registry,\n\t\t * the cell renders `cnFormatters[formatter](value, row, property,\n\t\t * formatterOptions)` as text — an explicit override of the type-aware\n\t\t * rendering below.\n\t\t */\n\t\tformatter: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative options map passed as the formatter's fourth argument\n\t\t * (e.g. `{ currency: 'USD' }` for the built-in `currency` formatter,\n\t\t * or `{ negative, zero, positive }` phrases for `conditionalPhrase`).\n\t\t * Undefined-safe: three-argument formatters simply ignore it.\n\t\t */\n\t\tformatterOptions: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional cell-widget id (e.g. `badge`, or a consumer-registered\n\t\t * name). When it resolves in `cnCellWidgets` the cell renders that\n\t\t * component with `{ value, row, property, formatted, ...widgetProps }`;\n\t\t * the built-in id `\"badge\"` renders `CnStatusBadge` and the built-in\n\t\t * id `\"fkResolve\"` renders `CnFkResolveCell` (uuid → related object\n\t\t * label, config via `widgetProps { register, schema, labelField }`),\n\t\t * and the built-in id `\"refLabel\"` shows labels already resolved in a\n\t\t * batch (`widgetProps { labels, route? }`, set by CnIndexPage for a\n\t\t * column with `labelField`).\n\t\t * The built-in id `\"avatar\"` renders a person as an avatar with the\n\t\t * name beside it (`widgetProps { userField?, user?, nameField?, size? }`;\n\t\t * a free name gets its initials), and the built-in id `\"date\"` renders\n\t\t * a date whose colour follows `widgetProps.variantWhen`, rules on the\n\t\t * number of days until the date (`[{ op: \"lt\", value: 0, variant:\n\t\t * \"error\" }, { op: \"lte\", value: 5, variant: \"warning\" }]`).\n\t\t * The built-in id `\"group\"` shows a Nextcloud group id (or a list of\n\t\t * them) as the group's display name; a column whose property is\n\t\t * marked `referenceType: \"nextcloud-group\"` gets it without asking.\n\t\t * Takes precedence over `formatter`/the type-aware rendering, but the\n\t\t * value handed to the widget is the formatter-shaped `formatted` when\n\t\t * `formatter` is also set.\n\t\t */\n\t\twidget: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Extra props spread onto the resolved cell-widget component. The\n\t\t * built-in `\"link\"` reads its target from here: `route` (a manifest\n\t\t * page id), `params` (route param to row field), `href` (URL with\n\t\t * `{field}` placeholders), and `routeField` + `routeMap` to pick the\n\t\t * page per row from a sibling field (`{ person: 'ContactDetail' }`),\n\t\t * with `route` as the fallback for a value the map does not hold.\n\t\t */\n\t\twidgetProps: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Optional declarative cell-format spec — a no-code alternative to a\n\t\t * registry `formatter`. Recognised `style` values:\n\t\t *\n\t\t * - `'currency'` → `Intl.NumberFormat` currency (e.g. `€ 1.234,56`),\n\t\t * honouring `currency` (ISO code, default `'EUR'`) and `decimals`\n\t\t * (default 2).\n\t\t * - `'number'` / `'percent'` → localized number (percent appends `%`),\n\t\t * honouring `decimals` (default 0).\n\t\t * - `'duration'` → a seconds value rendered compact (`1u 23m`, `45m 10s`,\n\t\t * `12s`); pass `unit: 'milliseconds'` / `'minutes'` / `'hours'` when the\n\t\t * raw value is not in seconds. Sub-second millisecond values render as\n\t\t * `245ms` rather than rounding down to `0s`.\n\t\t * - `'swatch'` → a colour dot read from a sibling row field named by\n\t\t * `colorField` (the value renders as the cell text beside it).\n\t\t *\n\t\t * `prefix` / `suffix` are prepended/appended to the numeric styles.\n\t\t * Resolved AFTER `formatter` / `widget` (those win), but BEFORE the\n\t\t * type-aware rendering, so a manifest column can opt into currency or a\n\t\t * colour swatch without registering a function.\n\t\t *\n\t\t * @type {{style?: 'currency'|'number'|'percent'|'duration'|'swatch', currency?: string, decimals?: number, unit?: 'milliseconds'|'seconds'|'minutes'|'hours', prefix?: string, suffix?: string, colorField?: string}}\n\t\t */\n\t\tformat: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The full row object — passed so a formatter can be a function of\n\t\t * the whole record (e.g. \"days since `@self.updated`\"), not just\n\t\t * this one cell value.\n\t\t */\n\t\trow: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Maximum string length before truncation */\n\t\ttruncate: {\n\t\t\ttype: Number,\n\t\t\tdefault: 100,\n\t\t},\n\n\t\t/**\n\t\t * Row identifier field — used by the built-in `widget:\"link\"` when\n\t\t * the manifest doesn't specify an explicit `widgetProps.params`\n\t\t * map. Defaults to `'id'` so router-link param resolution works\n\t\t * with the manifest convention (`/x/:id`).\n\t\t */\n\t\trowKey: {\n\t\t\ttype: String,\n\t\t\tdefault: 'id',\n\t\t},\n\t},\n\n\tcomputed: {\n\t\tpropertyType() {\n\t\t\treturn this.property?.type || (typeof this.value)\n\t\t},\n\n\t\tisEnum() {\n\t\t\treturn !!(this.property?.enum && this.property.enum.length > 0)\n\t\t},\n\n\t\t/**\n\t\t * Badge text for an enum cell. Schema enum values are English source\n\t\t * codes, so a Dutch session otherwise reads `submitted` in a table whose\n\t\t * headers are Dutch. Only the label is translated — the cell's value,\n\t\t * sorting and filtering keep using the raw code.\n\t\t */\n\t\tenumLabel() {\n\t\t\tconst raw = String(this.value)\n\t\t\tconst labels = (this.property && (this.property.enumLabels || this.property['x-enum-labels'])) || {}\n\t\t\treturn this.cnTranslate(labels[raw] || raw)\n\t\t},\n\n\t\t/** True when the property is a date / date-time (rendered via NcDateTime). */\n\t\tisDate() {\n\t\t\treturn this.property?.format === 'date-time' || this.property?.format === 'date'\n\t\t},\n\n\t\t/**\n\t\t * The cell value as a Date for NcDateTime, or `null` when absent or\n\t\t * unparseable (the cell then falls back to a dash / plain text).\n\t\t *\n\t\t * @return {Date|null}\n\t\t */\n\t\tdateTimestamp() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst date = new Date(this.value)\n\t\t\treturn Number.isNaN(date.getTime()) ? null : date\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the Nextcloud user id whose avatar to show, or ''\n\t\t * for a free name. Read from the row field `widgetProps.userField`,\n\t\t * or from the cell value itself when `widgetProps.user` is true.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarUserId() {\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tlet id = ''\n\t\t\tif (typeof props.userField === 'string' && props.userField !== '') {\n\t\t\t\tid = objectFieldValue(this.row, props.userField)\n\t\t\t} else if (props.user === true) {\n\t\t\t\tid = this.value\n\t\t\t}\n\t\t\treturn (typeof id === 'string' || typeof id === 'number') ? String(id) : ''\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the name shown beside the picture. Read from the\n\t\t * row field `widgetProps.nameField` when set, else the cell value.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarName() {\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tif (typeof props.nameField === 'string' && props.nameField !== '') {\n\t\t\t\tconst name = objectFieldValue(this.row, props.nameField)\n\t\t\t\tif (typeof name === 'string' && name !== '') {\n\t\t\t\t\treturn name\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!this.hasValue || typeof this.value === 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn String(this.hasFormatter ? this.formattedValue : this.value)\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the initials of a free name, at most two letters\n\t\t * (first and last word).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarInitials() {\n\t\t\tconst words = this.avatarName.trim().split(/\\s+/).filter((word) => word !== '')\n\t\t\tif (words.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = Array.from(words[0])[0] || ''\n\t\t\tconst last = words.length > 1 ? (Array.from(words[words.length - 1])[0] || '') : ''\n\t\t\treturn (first + last).toLocaleUpperCase()\n\t\t},\n\n\t\t/**\n\t\t * `avatar` widget: the picture size in pixels (`widgetProps.size`, default 24).\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tavatarSize() {\n\t\t\tconst size = Number(this.widgetProps && this.widgetProps.size)\n\t\t\treturn Number.isFinite(size) && size > 0 ? size : 24\n\t\t},\n\n\t\t/**\n\t\t * `date` widget: the variant of the first `widgetProps.variantWhen`\n\t\t * rule the date matches, or '' for none.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t */\n\t\tdateVariant() {\n\t\t\tconst rules = this.widgetProps && this.widgetProps.variantWhen\n\t\t\tconst variant = resolveDateVariant(this.value, rules)\n\t\t\treturn variant === 'default' ? '' : variant\n\t\t},\n\n\t\t/**\n\t\t * `date` widget: the date written out in the user's locale.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes\n\t\t */\n\t\tdateLabel() {\n\t\t\tconst date = parseDateValue(this.value)\n\t\t\tif (date === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\t// `widgetProps.showTime: true` adds the time of day (\"5 Oct 2026,\n\t\t\t// 08:30\"); `widgetProps.timeOnly: true` shows just the time\n\t\t\t// (\"08:30\"), for a timetable row. Default: the date alone.\n\t\t\tconst props = this.widgetProps || {}\n\t\t\tconst options = props.timeOnly === true\n\t\t\t\t? { hour: '2-digit', minute: '2-digit' }\n\t\t\t\t: { day: 'numeric', month: 'short', year: 'numeric', ...(props.showTime === true ? { hour: '2-digit', minute: '2-digit' } : {}) }\n\t\t\tlet locale\n\t\t\ttry {\n\t\t\t\tlocale = getCanonicalLocale()\n\t\t\t} catch {\n\t\t\t\tlocale = undefined\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Intl.DateTimeFormat(locale || undefined, options).format(date)\n\t\t\t} catch {\n\t\t\t\treturn new Intl.DateTimeFormat(undefined, options).format(date)\n\t\t\t}\n\t\t},\n\n\t\t/** True when the property is a URI / URL (rendered as an external link). */\n\t\tisUri() {\n\t\t\treturn this.property?.format === 'uri' || this.property?.format === 'url'\n\t\t},\n\n\t\t/**\n\t\t * Safe external href for a URI/URL cell, or `null` when the value is\n\t\t * absent or fails `safeHref` validation (e.g. an unsafe scheme).\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\turiHref() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn safeHref(String(this.value))\n\t\t},\n\n\t\t/** True when the cell has a renderable value (not null/undefined/empty string). */\n\t\thasValue() {\n\t\t\treturn this.value !== null && this.value !== undefined && this.value !== ''\n\t\t},\n\n\t\t/**\n\t\t * Resolved cell-widget component for this column, or `null`. A column's\n\t\t * `widget` id resolves against the injected `cnCellWidgets` registry;\n\t\t * the built-in `\"badge\"` is NOT resolved here (handled inline in the\n\t\t * template) so apps can still override `\"badge\"` via the registry.\n\t\t *\n\t\t * @return {import('vue').Component|null}\n\t\t */\n\t\twidgetComponent() {\n\t\t\tif (!this.widget) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst c = this.cnCellWidgets && this.cnCellWidgets[this.widget]\n\t\t\treturn c || null\n\t\t},\n\n\t\t/**\n\t\t * Whether this cell shows Nextcloud group names: the column asks for\n\t\t * `widget: \"group\"`, or it names no widget and no formatter and its\n\t\t * property is a group (or a list of groups).\n\t\t *\n\t\t * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name\n\t\t * @return {boolean}\n\t\t */\n\t\tisGroupCell() {\n\t\t\tif (this.widget === 'group') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (this.widget || this.hasFormatter) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst p = this.property\n\t\t\tif (!p || typeof p !== 'object') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn isGroupProp(p) || (p.type === 'array' && isGroupProp(p.items))\n\t\t},\n\n\t\t/**\n\t\t * The group ids in this cell, empty ones dropped.\n\t\t *\n\t\t * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name\n\t\t * @return {string[]}\n\t\t */\n\t\tgroupIds() {\n\t\t\tconst raw = this.value\n\t\t\tconst list = Array.isArray(raw) ? raw : [raw]\n\t\t\treturn list\n\t\t\t\t.filter((gid) => (typeof gid === 'string' && gid !== '') || typeof gid === 'number')\n\t\t\t\t.map((gid) => String(gid))\n\t\t},\n\n\t\t/** Variant for the built-in `badge` widget — `widgetProps.variant` or `'default'`. */\n\t\tbadgeVariant() {\n\t\t\treturn (this.widgetProps && this.widgetProps.variant) || 'default'\n\t\t},\n\n\t\t/**\n\t\t * Color map for the built-in `badge` widget — `widgetProps.colorMap`,\n\t\t * a `{ value: variant }` map (e.g. `{ placed: 'primary', delivered: 'success' }`)\n\t\t * that CnStatusBadge resolves per label. Null when unset (falls back to\n\t\t * `badgeVariant`). Lets a manifest colour a status column without code.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tbadgeColorMap() {\n\t\t\treturn (this.widgetProps && this.widgetProps.colorMap) || null\n\t\t},\n\n\t\t/**\n\t\t * Color map for the auto-rendered enum badge — read from the schema\n\t\t * property's optional `colorMap` (or `x-color-map`). Null when unset, so\n\t\t * enum cells stay the default grey unless the schema opts into colours.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tenumColorMap() {\n\t\t\treturn (this.property && (this.property.colorMap || this.property['x-color-map'])) || null\n\t\t},\n\n\t\t/**\n\t\t * The manifest page id the built-in `widget:\"link\"` routes this row\n\t\t * to, or null.\n\t\t *\n\t\t * `widgetProps.routeField` names a sibling field on the row, and\n\t\t * `widgetProps.routeMap` maps that field's values to page ids, so one\n\t\t * column can link a person to `ContactDetail` and an organisation to\n\t\t * `OrganisationDetail` off `initiatorType`. A row whose value has no\n\t\t * entry in the map falls back to the fixed `widgetProps.route`, and\n\t\t * without one the cell renders as text. The map is the whole\n\t\t * vocabulary: a row value is only ever used as a KEY into it, never\n\t\t * as a route name itself, so row data cannot send a link to a page\n\t\t * the manifest did not name.\n\t\t *\n\t\t * @return {string|null} The page id, or null when none resolves.\n\t\t */\n\t\tlinkRouteName() {\n\t\t\tconst wp = this.widgetProps || {}\n\t\t\tconst map = wp.routeMap\n\t\t\tif (typeof wp.routeField === 'string' && wp.routeField !== '' && map && typeof map === 'object') {\n\t\t\t\tconst key = this.row ? this.row[wp.routeField] : undefined\n\t\t\t\tif (key !== undefined && key !== null && Object.hasOwn(map, String(key))) {\n\t\t\t\t\tconst mapped = map[String(key)]\n\t\t\t\t\tif (typeof mapped === 'string' && mapped !== '') {\n\t\t\t\t\t\treturn mapped\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn (typeof wp.route === 'string' && wp.route !== '') ? wp.route : null\n\t\t},\n\n\t\t/**\n\t\t * Items of the built-in `widget:\"refLabel\"`: one per referenced id with\n\t\t * its text, state (`label`, `loading` or `missing`) and optional link.\n\t\t *\n\t\t * @return {Array<{id: string, text: string, state: string, to: (object|null)}>}\n\t\t */\n\t\trefLabelItems() {\n\t\t\tif (this.widget !== 'refLabel') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst wp = this.widgetProps || {}\n\t\t\tconst labels = wp.labels || {}\n\t\t\tconst raw = this.value\n\t\t\tconst list = Array.isArray(raw) ? raw : [raw]\n\t\t\treturn list\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '')\n\t\t\t\t.map((id) => {\n\t\t\t\t\tconst key = String(id)\n\t\t\t\t\tconst known = Object.hasOwn(labels, key)\n\t\t\t\t\tconst label = known ? labels[key] : undefined\n\t\t\t\t\tconst state = !known ? 'loading' : (label ? 'label' : 'missing')\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: key,\n\t\t\t\t\t\tstate,\n\t\t\t\t\t\ttext: state === 'label' ? label : (state === 'loading' ? '…' : key),\n\t\t\t\t\t\tto: (state === 'label' && typeof wp.route === 'string' && wp.route !== '') ? { name: wp.route, params: { id: key } } : null,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Resolved router-link target for the built-in `widget:\"link\"`. When\n\t\t * a page id resolves (`widgetProps.route`, or the row's entry in\n\t\t * `widgetProps.routeMap`, see `linkRouteName`), returns\n\t\t * `{ name: route, params }`. Param map is `widgetProps.params`\n\t\t * (a map of route-param-name → row-field-name); when omitted,\n\t\t * defaults to `{ id: row[rowKey] }`. Returns null when no page id\n\t\t * resolves (the template falls through to `linkHref` or plain\n\t\t * text).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tlinkRoute() {\n\t\t\tif (this.widget !== 'link') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst route = this.linkRouteName\n\t\t\tif (!route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst paramMap = (this.widgetProps && this.widgetProps.params)\n\t\t\t\t|| { id: this.rowKey || 'id' }\n\t\t\tconst params = {}\n\t\t\tfor (const [routeParam, rowField] of Object.entries(paramMap)) {\n\t\t\t\tparams[routeParam] = this.row && this.row[rowField]\n\t\t\t}\n\t\t\treturn { name: route, params }\n\t\t},\n\n\t\t/**\n\t\t * Resolved external href for the built-in `widget:\"link\"` when\n\t\t * `widgetProps.href` is set. `{key}` placeholders in the href\n\t\t * are substituted from the row (`\"/x/{id}\"` + `row.id === \"42\"`\n\t\t * → `\"/x/42\"`). The final computed value is validated with\n\t\t * `safeHref` so row-injected values cannot introduce unsafe\n\t\t * schemes: a `javascript:` or `data:` result renders as plain text\n\t\t * instead of a link. Returns null when `href` isn't set.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tlinkHref() {\n\t\t\tif (this.widget !== 'link') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst href = this.widgetProps && this.widgetProps.href\n\t\t\tif (!href) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst resolved = String(href).replace(/\\{(\\w+)\\}/g, (_, key) => this.row && this.row[key] !== null && this.row[key] !== undefined ? String(this.row[key]) : '')\n\t\t\t// The safeHref retrofit (18700fd94) put this check here and a later\n\t\t\t// lint pass (e5ea00d51) dropped it, leaving the docblock above\n\t\t\t// promising a check the code no longer made: a template of `{url}`\n\t\t\t// put a row's `javascript:` value straight into `:href`. safeHref\n\t\t\t// answers '#' for anything unsafe; that is not a link worth\n\t\t\t// rendering, so fall through to plain text.\n\t\t\tconst safe = safeHref(resolved)\n\t\t\treturn safe === '#' ? null : safe\n\t\t},\n\n\t\t/**\n\t\t * Resolved formatter function for this cell, or `null`. A column's\n\t\t * `formatter` id resolves against the injected `cnFormatters` registry.\n\t\t *\n\t\t * @return {((value: unknown, row: object, property: object, options: object) => unknown)|null}\n\t\t */\n\t\tformatterFn() {\n\t\t\tconst fn = this.formatter && this.cnFormatters && this.cnFormatters[this.formatter]\n\t\t\treturn typeof fn === 'function' ? fn : null\n\t\t},\n\n\t\t/** True when an explicit formatter is in play for this cell. */\n\t\thasFormatter() {\n\t\t\treturn this.formatterFn !== null\n\t\t},\n\n\t\t/**\n\t\t * The declarative format `style`, or null when no (recognised) `format`\n\t\t * spec is set. Drives the built-in currency / duration / number / percent\n\t\t * / swatch rendering paths.\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tformatStyle() {\n\t\t\tconst s = this.format && this.format.style\n\t\t\treturn typeof s === 'string' ? s : null\n\t\t},\n\n\t\t/** True when a built-in NUMERIC format (currency/number/percent/duration) applies. */\n\t\thasBuiltinFormat() {\n\t\t\treturn ['currency', 'number', 'percent', 'duration'].includes(this.formatStyle)\n\t\t},\n\n\t\t/** True when this cell renders as a colour swatch (`format.style:\"swatch\"`). */\n\t\tisSwatch() {\n\t\t\treturn this.formatStyle === 'swatch'\n\t\t},\n\n\t\t/**\n\t\t * The swatch colour — read from the sibling row field named by\n\t\t * `format.colorField` (default `'color'`). Returns null when no usable\n\t\t * colour string is present (the dot is then omitted).\n\t\t *\n\t\t * @return {string|null}\n\t\t */\n\t\tswatchColor() {\n\t\t\tif (!this.isSwatch) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst field = (this.format && this.format.colorField) || 'color'\n\t\t\tconst c = this.row && this.row[field]\n\t\t\treturn (typeof c === 'string' && c.trim() !== '') ? c : null\n\t\t},\n\n\t\tformattedValue() {\n\t\t\tif (this.formatterFn) {\n\t\t\t\ttry {\n\t\t\t\t\treturn this.formatterFn(this.value, this.row, this.property, this.formatterOptions || undefined)\n\t\t\t\t} catch (e) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnCellRenderer] formatter \"${this.formatter}\" threw; falling back`, e)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (this.hasBuiltinFormat) {\n\t\t\t\treturn this.applyBuiltinFormat()\n\t\t\t}\n\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t},\n\n\t\trawTitle() {\n\t\t\t// Show full value on hover if it was truncated\n\t\t\tconst raw = this.value\n\t\t\tif (typeof raw === 'string' && raw.length > this.truncate) {\n\t\t\t\treturn raw\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the cell draws a uuid, and so takes the monospace uuid\n\t\t * style. The property's `format: \"uuid\"` alone used to decide this,\n\t\t * so a reference that a formatter or a widget had resolved to its\n\t\t * name was still drawn as code. Now the value has to be a uuid AND\n\t\t * nothing resolves it: a resolving widget or formatter, or a value\n\t\t * that is not a uuid, renders in the normal text style.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-resolved-reference-is-not-drawn-as-a-uuid\n\t\t * @return {boolean}\n\t\t */\n\t\tshowsUuid() {\n\t\t\tif (this.property?.format !== 'uuid') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.widgetComponent || this.widget || this.hasFormatter) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn typeof this.value === 'string' && UUID_PATTERN.test(this.value)\n\t\t},\n\n\t\tcellClass() {\n\t\t\tconst classes = []\n\t\t\tif (this.propertyType === 'boolean') {\n\t\t\t\tclasses.push('cn-cell-renderer--boolean')\n\t\t\t}\n\t\t\tif (this.isEnum) {\n\t\t\t\tclasses.push('cn-cell-renderer--enum')\n\t\t\t}\n\t\t\tif (this.property?.format === 'date-time' || this.property?.format === 'date') {\n\t\t\t\tclasses.push('cn-cell-renderer--date')\n\t\t\t}\n\t\t\tif (this.showsUuid) {\n\t\t\t\tclasses.push('cn-cell-renderer--uuid')\n\t\t\t}\n\t\t\tif (this.propertyType === 'integer' || this.propertyType === 'number') {\n\t\t\t\tclasses.push('cn-cell-renderer--number')\n\t\t\t}\n\t\t\treturn classes\n\t\t},\n\t},\n\n\tmounted() {\n\t\t// Warn once per column-property-name when `widget:\"link\"` resolves\n\t\t// to neither a `route` nor an `href` — silent fallback hides\n\t\t// manifest mistakes, an every-row warn is too noisy.\n\t\t// A column with a `routeMap` has declared its targets; a row whose\n\t\t// value is not in the map (an empty requester, say) is data, not a\n\t\t// manifest mistake, so it renders as text without the warning. The\n\t\t// same holds for a declared `href` whose resolved value safeHref\n\t\t// refused: the manifest is fine, the row value is not.\n\t\tif (\n\t\t\tthis.widget === 'link'\n\t\t\t&& !this.linkRoute\n\t\t\t&& !this.widgetProps?.href\n\t\t\t&& this.widgetProps?.fallback !== 'silent'\n\t\t\t&& !(this.widgetProps?.routeMap && typeof this.widgetProps.routeMap === 'object')\n\t\t) {\n\t\t\tconst key = this.property?.title || String(this.value).slice(0, 20)\n\t\t\tif (!WARNED_LINK_KEYS.has(key)) {\n\t\t\t\tWARNED_LINK_KEYS.add(key)\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnCellRenderer] widget:\"link\" on \"${key}\" has no resolvable target — set widgetProps.route (page id) or widgetProps.href (URL with optional {field} placeholders); cell falls back to plain text.`)\n\t\t\t}\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Render the cell value per the declarative `format` spec (currency /\n\t\t * number / percent / duration). Mirrors CnStatWidget's formatting so a\n\t\t * KPI tile and a table column read identically. Non-numeric values fall\n\t\t * back to `formatValue`; an empty value renders an em-dash.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tapplyBuiltinFormat() {\n\t\t\tif (!this.hasValue) {\n\t\t\t\treturn '—'\n\t\t\t}\n\t\t\tconst fmt = this.format || {}\n\t\t\tif (fmt.style === 'duration') {\n\t\t\t\treturn this.formatDuration()\n\t\t\t}\n\t\t\tconst num = Number(this.value)\n\t\t\tif (!Number.isFinite(num)) {\n\t\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t\t}\n\t\t\tconst decimals = Number.isFinite(fmt.decimals)\n\t\t\t\t? fmt.decimals\n\t\t\t\t: (fmt.style === 'currency' ? 2 : 0)\n\t\t\tlet body\n\t\t\tif (fmt.style === 'currency') {\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tstyle: 'currency',\n\t\t\t\t\tcurrency: safeCurrencyCode(fmt.currency),\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num)\n\t\t\t} else if (fmt.style === 'percent') {\n\t\t\t\t// Values are stored as the literal percent (83.3), not a 0–1 ratio.\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num) + '%'\n\t\t\t} else {\n\t\t\t\tbody = new Intl.NumberFormat(undefined, {\n\t\t\t\t\tminimumFractionDigits: decimals,\n\t\t\t\t\tmaximumFractionDigits: decimals,\n\t\t\t\t}).format(num)\n\t\t\t}\n\t\t\treturn `${fmt.prefix || ''}${body}${fmt.suffix || ''}`\n\t\t},\n\n\t\t/**\n\t\t * Render a numeric duration compactly (`1u 23m`, `45m 10s`, `12s`). The\n\t\t * raw value is seconds unless `format.unit` is `'milliseconds'` /\n\t\t * `'minutes'` / `'hours'`. The hour suffix is `u` (uur) to read naturally\n\t\t * under NL theming while staying digit-led for other locales.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tformatDuration() {\n\t\t\tconst fmt = this.format || {}\n\t\t\tlet secs = Number(this.value)\n\t\t\tif (!Number.isFinite(secs)) {\n\t\t\t\treturn formatValue(this.value, this.property, { truncate: this.truncate })\n\t\t\t}\n\t\t\tif (fmt.unit === 'milliseconds') {\n\t\t\t\t// Sub-second durations are the common case for a millisecond\n\t\t\t\t// field (request/step timings), and they would all collapse to\n\t\t\t\t// the `0s` floor below — so render them in ms directly.\n\t\t\t\tconst ms = Math.round(secs)\n\t\t\t\tif (Math.abs(ms) < 1000) {\n\t\t\t\t\treturn `${fmt.prefix || ''}${ms}ms${fmt.suffix || ''}`\n\t\t\t\t}\n\t\t\t\tsecs = ms / 1000\n\t\t\t} else if (fmt.unit === 'minutes') {\n\t\t\t\tsecs *= 60\n\t\t\t} else if (fmt.unit === 'hours') {\n\t\t\t\tsecs *= 3600\n\t\t\t}\n\t\t\tsecs = Math.round(secs)\n\t\t\tconst sign = secs < 0 ? '-' : ''\n\t\t\tsecs = Math.abs(secs)\n\t\t\tconst h = Math.floor(secs / 3600)\n\t\t\tconst m = Math.floor((secs % 3600) / 60)\n\t\t\tconst s = secs % 60\n\t\t\tconst parts = []\n\t\t\tif (h > 0) {\n\t\t\t\tparts.push(`${h}u`)\n\t\t\t}\n\t\t\tif (m > 0) {\n\t\t\t\tparts.push(`${m}m`)\n\t\t\t}\n\t\t\tif (s > 0 && h === 0) {\n\t\t\t\tparts.push(`${s}s`)\n\t\t\t}\n\t\t\tif (parts.length === 0) {\n\t\t\t\tparts.push('0s')\n\t\t\t}\n\t\t\treturn `${fmt.prefix || ''}${sign}${parts.join(' ')}${fmt.suffix || ''}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-cell-renderer__mono {\n\tfont-family: monospace;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer--uuid {\n\tfont-family: monospace;\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer--number {\n\tfont-variant-numeric: tabular-nums;\n}\n\n.cn-cell-renderer--date {\n\twhite-space: nowrap;\n}\n\n.cn-cell-renderer__dash {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-cell-renderer__avatar {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tmax-width: 100%;\n\tvertical-align: middle;\n}\n\n.cn-cell-renderer__avatar-picture {\n\tdisplay: inline-flex;\n\tflex: none;\n}\n\n.cn-cell-renderer__avatar-initials {\n\tdisplay: inline-flex;\n\tflex: none;\n\talign-items: center;\n\tjustify-content: center;\n\tborder-radius: 50%;\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-size: 0.75em;\n\tfont-weight: 700;\n\tline-height: 1;\n}\n\n.cn-cell-renderer__avatar-name {\n\tmin-width: 0;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n}\n\n/* A date that matched a rule is also set in a heavier weight, so the signal\n does not rest on colour alone. */\n.cn-cell-renderer__date--success,\n.cn-cell-renderer__date--warning,\n.cn-cell-renderer__date--error {\n\tfont-weight: 600;\n}\n\n.cn-cell-renderer__date--success {\n\tcolor: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-cell-renderer__date--warning {\n\tcolor: var(--color-text-warning, var(--color-warning-text));\n}\n\n.cn-cell-renderer__date--error {\n\tcolor: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-cell-renderer__icon--success {\n\tcolor: var(--color-success);\n}\n\n.cn-cell-renderer__swatch-wrap {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n}\n\n.cn-cell-renderer__swatch-dot {\n\tdisplay: inline-block;\n\twidth: 12px;\n\theight: 12px;\n\tborder-radius: 50%;\n\tflex-shrink: 0;\n\tborder: 1px solid var(--color-border-dark);\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createBlock","_resolveDynamicComponent","_mergeProps","_Fragment","_openBlock","_renderList","_createVNode","_createTextVNode","_toDisplayString","_normalizeStyle","_createElementVNode"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import script from './CnGroupNameCell.vue2.js';
2
+ import { render } from './CnGroupNameCell.vue3.js';
3
+
4
+ script.render = render;
5
+ script.__file = "src/components/CnCellRenderer/CnGroupNameCell.vue";
6
+ const __sfc_main = script;
7
+
8
+ export { __sfc_main as default };
9
+ //# sourceMappingURL=CnGroupNameCell.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnGroupNameCell.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,50 @@
1
+ import { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js';
2
+
3
+ /**
4
+ * CnGroupNameCell renders a Nextcloud group id as its display name.
5
+ *
6
+ * ```vue
7
+ * <CnGroupNameCell gid="toezicht" />
8
+ * ```
9
+ *
10
+ * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name
11
+ */
12
+ var script = {
13
+ name: 'CnGroupNameCell',
14
+
15
+ props: {
16
+ /** The group id to show. */
17
+ gid: {
18
+ type: String,
19
+ required: true,
20
+ },
21
+ },
22
+
23
+ computed: {
24
+ /**
25
+ * The cached display name, or the id while it loads or when unknown.
26
+ *
27
+ * @return {string}
28
+ */
29
+ label() {
30
+ return groupDisplayName(this.gid) || this.gid
31
+ },
32
+ },
33
+
34
+ watch: {
35
+ gid: {
36
+ immediate: true,
37
+ /**
38
+ * Ask for the name of a new id (cached, so once per id per page).
39
+ *
40
+ * @param {string} gid The group id.
41
+ */
42
+ handler(gid) {
43
+ loadGroupDisplayName(gid);
44
+ },
45
+ },
46
+ },
47
+ };
48
+
49
+ export { script as default };
50
+ //# sourceMappingURL=CnGroupNameCell.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnGroupNameCell.vue2.js","sources":["../../../../src/components/CnCellRenderer/CnGroupNameCell.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n\n CnGroupNameCell: one Nextcloud group id shown as the group's display name.\n The name is looked up once per page (utils/groupAutocomplete.js); until it\n arrives, and when it cannot be found, the cell shows the id.\n-->\n<template>\n\t<span class=\"cn-group-name-cell\" :title=\"gid\">{{ label }}</span>\n</template>\n\n<script>\nimport { groupDisplayName, loadGroupDisplayName } from '../../utils/groupAutocomplete.js'\n\n/**\n * CnGroupNameCell renders a Nextcloud group id as its display name.\n *\n * ```vue\n * <CnGroupNameCell gid=\"toezicht\" />\n * ```\n *\n * @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-a-group-cell-shows-the-groups-display-name\n */\nexport default {\n\tname: 'CnGroupNameCell',\n\n\tprops: {\n\t\t/** The group id to show. */\n\t\tgid: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The cached display name, or the id while it loads or when unknown.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tlabel() {\n\t\t\treturn groupDisplayName(this.gid) || this.gid\n\t\t},\n\t},\n\n\twatch: {\n\t\tgid: {\n\t\t\timmediate: true,\n\t\t\t/**\n\t\t\t * Ask for the name of a new id (cached, so once per id per page).\n\t\t\t *\n\t\t\t * @param {string} gid The group id.\n\t\t\t */\n\t\t\thandler(gid) {\n\t\t\t\tloadGroupDisplayName(gid)\n\t\t\t},\n\t\t},\n\t},\n}\n</script>\n"],"names":[],"mappings":";;AAeA;;AAEC;AACA;AACA;AACA;AACA;AACA;;AAED,aAAe;CACd,IAAI,EAAE,iBAAiB;;AAEvB,CAAA,KAAK,EAAE;AACN;EACA,GAAG,EAAE;GACJ,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;EACD;;AAED,CAAA,QAAQ,EAAE;AACT;;AAEC;AACA;;AAED,EAAA,KAAK,GAAG;AACP,GAAA,OAAO,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAE,IAAG,IAAI,CAAC,GAAE;GAC5C;EACD;;AAED,CAAA,KAAK,EAAE;EACN,GAAG,EAAE;AACJ,GAAA,SAAS,EAAE,IAAI;AACf;;AAEC;;;GAGD,OAAO,CAAC,GAAG,EAAE;IACZ,oBAAoB,CAAC,GAAG,EAAA;IACxB;GACD;EACD;AACF;;;;"}
@@ -0,0 +1,13 @@
1
+ import { openBlock, createElementBlock, toDisplayString } from 'vue';
2
+
3
+ const _hoisted_1 = ["title"];
4
+
5
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
6
+ return (openBlock(), createElementBlock("span", {
7
+ class: "cn-group-name-cell",
8
+ title: $props.gid
9
+ }, toDisplayString($options.label), 9 /* TEXT, PROPS */, _hoisted_1))
10
+ }
11
+
12
+ export { render };
13
+ //# sourceMappingURL=CnGroupNameCell.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnGroupNameCell.vue3.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;"}
@@ -2,12 +2,14 @@ import axios__default from '@nextcloud/axios';
2
2
  import { translate, translatePlural } from '@nextcloud/l10n';
3
3
  import { generateUrl } from '@nextcloud/router';
4
4
  import { NcLoadingIcon, NcCheckboxRadioSwitch } from '@nextcloud/vue';
5
- import FilterIcon from 'vue-material-design-icons/Filter.vue';
5
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue';
6
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue';
6
7
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue';
7
8
  import __sfc_main$1 from './CnColumnFilterPopover.vue.js';
8
9
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js';
9
10
  import { normalizeLook } from '../../composables/useLook.js';
10
11
  import { columnFilterDef, columnFilterState, isColumnFilterActive, columnFilterParams, clearedColumnFilterParams, isColumnSortable } from '../../utils/columnFilters.js';
12
+ import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js';
11
13
  import { resolveHref, openRowTarget, followLinkClick } from '../../utils/linkNavigation.js';
12
14
  import { nextSortState } from '../../utils/multiColumnSort.js';
13
15
  import { preventMiddleClickAutoscroll, isRowMiddleClick, isNewTabHandled, markNewTabHandled, isNewTabClick } from '../../utils/rowAuxClick.js';
@@ -112,7 +114,8 @@ var script = {
112
114
  CnIcon: __sfc_main$2,
113
115
  CnLockIndicator: __sfc_main$3,
114
116
  CnUnreadMarker: __sfc_main$4,
115
- FilterIcon,
117
+ ChevronDown,
118
+ ChevronUp,
116
119
  FilterOutline,
117
120
  },
118
121
 
@@ -293,6 +296,20 @@ var script = {
293
296
  default: () => translate('nextcloud-vue', 'No items found'),
294
297
  },
295
298
 
299
+ /**
300
+ * App wording for an unavailable personal lens in self-fetch mode,
301
+ * keyed `<lens>.<reason>` (`recent.audit-trail-disabled`) or
302
+ * `<reason>`. Without it the library's object-neutral text is shown
303
+ * when the response reports `@self.lenses.<lens>.available: false`.
304
+ *
305
+ * @type {{[key: string]: string}|null}
306
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
307
+ */
308
+ lensReasonTexts: {
309
+ type: Object,
310
+ default: null,
311
+ },
312
+
296
313
  /** Function returning CSS class(es) for a row: (row) => string|object */
297
314
  rowClass: {
298
315
  type: Function,
@@ -569,6 +586,8 @@ var script = {
569
586
  aggregateRequestId: 0,
570
587
  /** Rows fetched in self-fetch mode (register + schemaId). */
571
588
  fetchedRows: [],
589
+ /** Personal-lens reports (`@self.lenses`) of the latest self-fetch response. */
590
+ fetchedLenses: {},
572
591
  /** True while a self-fetch request is in flight. */
573
592
  selfFetchLoading: false,
574
593
  }
@@ -593,6 +612,22 @@ var script = {
593
612
  return translate('nextcloud-vue', 'Actions')
594
613
  },
595
614
 
615
+ /**
616
+ * Why the self-fetched list is empty when a personal lens could not
617
+ * answer (openregister#4514), or `''` to keep `emptyText`. Only the
618
+ * table's own fetch reports: external `rows` carry no report.
619
+ *
620
+ * @return {string}
621
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
622
+ */
623
+ lensReasonText() {
624
+ if (this.rows && this.rows.length > 0) {
625
+ return ''
626
+ }
627
+ const text = lensUnavailableText(this.fetchedLenses, this.lensReasonTexts);
628
+ return text ? this.translateLabel(text) : ''
629
+ },
630
+
596
631
  /** @return {number} Leading cells before the data columns (selection, icon). */
597
632
  leadingCount() {
598
633
  return (this.selectable ? 1 : 0) + (this.rowIcon ? 1 : 0)
@@ -1122,8 +1157,10 @@ var script = {
1122
1157
  ...(this.fetchParams ? { params: this.fetchParams } : {}),
1123
1158
  });
1124
1159
  this.fetchedRows = (data && data.results) || (Array.isArray(data) ? data : []);
1160
+ this.fetchedLenses = readLensReports(data);
1125
1161
  } catch {
1126
1162
  this.fetchedRows = [];
1163
+ this.fetchedLenses = {};
1127
1164
  } finally {
1128
1165
  this.selfFetchLoading = false;
1129
1166
  }
@@ -1602,8 +1639,8 @@ var script = {
1602
1639
  filterButtonLabel(col) {
1603
1640
  const column = this.translateLabel(col.label);
1604
1641
  return this.isColumnFiltered(col)
1605
- ? translate('nextcloud-vue', 'Filter {column}, active', { column })
1606
- : translate('nextcloud-vue', 'Filter {column}', { column })
1642
+ ? translate('nextcloud-vue', 'Filter by {column}, active', { column })
1643
+ : translate('nextcloud-vue', 'Filter by {column}', { column })
1607
1644
  },
1608
1645
 
1609
1646
  /**