@conduction/nextcloud-vue 2.148.0 → 2.150.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 (111) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +24 -7
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  6. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +12 -1
  7. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  9. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  15. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +23 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +1 -1
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +1 -1
  26. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +1 -1
  29. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +64 -8
  30. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +14 -4
  32. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +26 -3
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +10 -2
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +84 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +61 -33
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  44. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +49 -2
  45. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +90 -59
  47. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js +3 -1
  49. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js.map +1 -1
  50. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue3.js.map +1 -1
  51. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  52. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +10 -0
  53. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  54. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  56. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +11 -0
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +28 -21
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  62. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -15
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +123 -115
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  70. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +4 -0
  71. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  72. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  73. package/dist/esm/l10n/en.json.js +2 -1
  74. package/dist/esm/l10n/en.json.js.map +1 -1
  75. package/dist/esm/l10n/nl.json.js +2 -1
  76. package/dist/esm/l10n/nl.json.js.map +1 -1
  77. package/dist/esm/library-style.js +1 -1
  78. package/dist/esm/nextcloud-vue.css +761 -520
  79. package/dist/esm/utils/fillTemplate.js +19 -2
  80. package/dist/esm/utils/fillTemplate.js.map +1 -1
  81. package/dist/nextcloud-vue.cjs.js +1368 -945
  82. package/dist/nextcloud-vue.cjs.js.map +2 -2
  83. package/dist/nextcloud-vue.css +761 -520
  84. package/l10n/en.json +2 -1
  85. package/l10n/nl.json +2 -1
  86. package/package.json +1 -1
  87. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +24 -7
  88. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +13 -1
  89. package/src/components/CnChartWidget/CnChartWidget.md +1 -1
  90. package/src/components/CnChartWidget/CnChartWidget.vue +12 -0
  91. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
  92. package/src/components/CnDataTable/CnDataTable.vue +34 -1
  93. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +6 -1
  94. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +1 -0
  95. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +74 -10
  96. package/src/components/CnFactsWidget/CnFactsWidget.vue +35 -3
  97. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +131 -3
  98. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +103 -5
  99. package/src/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue +3 -1
  100. package/src/components/CnNotesCard/CnNotesCard.vue +10 -0
  101. package/src/components/CnObjectCard/CnObjectCard.vue +16 -0
  102. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  103. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
  104. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
  105. package/src/css/board.css +2 -0
  106. package/src/css/dialog.css +17 -0
  107. package/src/css/look-board-detail.css +52 -3
  108. package/src/css/look-board-index.css +42 -2
  109. package/src/css/look-board.css +45 -5
  110. package/src/css/table.css +5 -2
  111. package/src/utils/fillTemplate.js +19 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnObjectListWidget.vue3.js","sources":["../../../../src/components/CnObjectListWidget/CnObjectListWidget.vue?vue&type=template&id=7a3cf020&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-object-list-widget\"\n\t\t:class=\"{ 'cn-object-list-widget--dropping': dropping, 'cn-object-list-widget--bleed': content.tableBleed === true }\"\n\t\t@dragenter=\"onDragEnter\"\n\t\t@dragover=\"onDragOver\"\n\t\t@dragleave=\"onDragLeave\"\n\t\t@drop=\"onDrop\">\n\t\t<!-- Drop overlay. Rendered only while a drag carrying files is over a\n\t\t widget that declares `content.dropZone`; without the key the drag\n\t\t handlers below all return before touching any state, so a widget\n\t\t that did not ask for a drop zone behaves exactly as before. -->\n\t\t<div v-if=\"dropping\" class=\"cn-object-list-widget__drop-overlay\">\n\t\t\t{{ dropLabel }}\n\t\t</div>\n\t\t<p v-if=\"waitingForContext\" class=\"cn-object-list-widget__prompt\">\n\t\t\t{{ promptText }}\n\t\t</p>\n\t\t<!-- A fetch failed: show ONE quiet line WITHOUT the raw axios status\n\t\t text (\"Request failed with status code 404\") — the real error is\n\t\t logged to the console (ADR-062: an error surface is never a leaked\n\t\t stack). Takes precedence over the empty state so the two never\n\t\t stack. -->\n\t\t<p v-else-if=\"error\" class=\"cn-object-list-widget__error\">\n\t\t\t{{ loadErrorLabel }}\n\t\t</p>\n\t\t<!-- Designed empty state. This used to be a bare CnDataTable with no\n\t\t rows, which paints its <thead> — a full-width grey strip floating\n\t\t in the middle of an otherwise blank card, the \"strange grey bar\"\n\t\t from the review. CnWidgetEmptyState replaces it with something\n\t\t deliberate, and stays `compact` inside a fit-measured cell so it\n\t\t cannot outgrow a short tile (ADR-062). -->\n\t\t<CnWidgetEmptyState\n\t\t\tv-else-if=\"!loading && rows.length === 0 && readOnlyRows.length === 0\"\n\t\t\t:name=\"resolvedEmptyText\"\n\t\t\t:compact=\"fitRows !== null && fitRows < 3\"\n\t\t\tclass=\"cn-object-list-widget__empty\">\n\t\t\t<template v-if=\"allowCreate && !waitingForContext\" #action>\n\t\t\t\t<NcButton class=\"cn-object-list-widget__empty-add\" @click=\"openCreate\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ addLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnWidgetEmptyState>\n\t\t<!-- The fetch came back with rows, but the active facet selection\n\t\t matches none of them (all loaded rows have that filter row set\n\t\t empty of a facet-clearable state). Distinct from the \"no items at\n\t\t all\" state above: an Add button is not the fix here, clearing the\n\t\t filter is. -->\n\t\t<CnWidgetEmptyState\n\t\t\tv-else-if=\"showNoFacetMatchState\"\n\t\t\t:name=\"t('nextcloud-vue', 'No items match this filter')\"\n\t\t\t:compact=\"fitRows !== null && fitRows < 3\"\n\t\t\tclass=\"cn-object-list-widget__empty\">\n\t\t\t<template #action>\n\t\t\t\t<button type=\"button\" class=\"cn-object-list-widget__facet-clear\" @click=\"clearFacet\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear filter') }}\n\t\t\t\t</button>\n\t\t\t</template>\n\t\t</CnWidgetEmptyState>\n\t\t<template v-else>\n\t\t\t<!-- Facet filter (`content.facet`). Chips built from the values\n\t\t\t actually present on the loaded rows, matching the \"keyword\n\t\t\t filter\" pattern this seam was measured against — narrows the\n\t\t\t rendered rows client-side, no refetch. Absent facet config\n\t\t\t renders nothing, so an ungrouped/unfiltered widget is unchanged. -->\n\t\t\t<div v-if=\"facetOptions.length > 0\" class=\"cn-object-list-widget__facet\" data-testid=\"object-list-facet\">\n\t\t\t\t<span class=\"cn-object-list-widget__facet-label\">{{ facetLabel }}</span>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"value in facetOptions\"\n\t\t\t\t\t:key=\"value\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__facet-chip\"\n\t\t\t\t\t:class=\"{ 'cn-object-list-widget__facet-chip--active': facetSelected.includes(value) }\"\n\t\t\t\t\t:aria-pressed=\"facetSelected.includes(value)\"\n\t\t\t\t\tdata-testid=\"object-list-facet-chip\"\n\t\t\t\t\t@click=\"toggleFacetValue(value)\">\n\t\t\t\t\t{{ value }}\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"facetSelected.length > 0\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__facet-clear\"\n\t\t\t\t\t@click=\"clearFacet\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear filter') }}\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Bulk-actions bar (`content.selectable` + `content.bulkActions`).\n\t\t\t Renders only once something is selected, so a selectable widget\n\t\t\t with nothing picked looks exactly like a non-selectable one. -->\n\t\t\t<div\n\t\t\t\tv-if=\"content.selectable && mappedBulkActions.length > 0 && selectedIds.length > 0\"\n\t\t\t\tclass=\"cn-object-list-widget__bulk-bar\"\n\t\t\t\tdata-testid=\"object-list-bulk-bar\">\n\t\t\t\t<span class=\"cn-object-list-widget__bulk-count\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{count} selected', { count: selectedIds.length }) }}\n\t\t\t\t</span>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"(action, index) in mappedBulkActions\"\n\t\t\t\t\t:key=\"index\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__bulk-action\"\n\t\t\t\t\t:class=\"{ 'cn-object-list-widget__bulk-action--destructive': action.destructive }\"\n\t\t\t\t\tdata-testid=\"object-list-bulk-action\"\n\t\t\t\t\t@click=\"action.handler\">\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</button>\n\t\t\t\t<button type=\"button\" class=\"cn-object-list-widget__bulk-clear\" @click=\"clearSelection\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear selection') }}\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"isGrouped\" class=\"cn-object-list-widget__groups\">\n\t\t\t\t<div v-for=\"group in groupedRows\"\n\t\t\t\t\t:key=\"group.key\"\n\t\t\t\t\tclass=\"cn-object-list-widget__group\"\n\t\t\t\t\tdata-testid=\"object-list-group\">\n\t\t\t\t\t<component :is=\"innerHeadingTag\" class=\"cn-object-list-widget__group-heading cn-inner-heading\">\n\t\t\t\t\t\t<!-- `groupLabelResolve` reuses the fkResolve cell for the\n\t\t\t\t\t\t heading itself: `groupBy` may bucket by a uuid that is\n\t\t\t\t\t\t ONE MORE hop past what `extend` inlined (e.g. a\n\t\t\t\t\t\t reference's own reference), which `objectFieldValue`\n\t\t\t\t\t\t cannot read off the row at all. The live object-store\n\t\t\t\t\t\t lookup CnDataTable columns already use for a `$ref`\n\t\t\t\t\t\t column is the same answer for a `$ref` GROUP. -->\n\t\t\t\t\t\t<CnFkResolveCell\n\t\t\t\t\t\t\tv-if=\"groupLabelResolveConfig\"\n\t\t\t\t\t\t\t:value=\"group.key\"\n\t\t\t\t\t\t\t:register=\"groupLabelResolveConfig.register\"\n\t\t\t\t\t\t\t:schema=\"groupLabelResolveConfig.schema\"\n\t\t\t\t\t\t\t:labelField=\"groupLabelResolveConfig.labelField || 'name'\" />\n\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t({{ group.rows.length }})\n\t\t\t\t\t</component>\n\t\t\t\t\t<CnDataTable\n\t\t\t\t\t\t:columns=\"resolvedColumns\"\n\t\t\t\t\t\t:rows=\"group.rows\"\n\t\t\t\t\t\t:loading=\"loading\"\n\t\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t\t:selectable=\"content.selectable === true\"\n\t\t\t\t\t\t:selectedIds=\"selectedIds\"\n\t\t\t\t\t\t:sortKey=\"localSort.field || null\"\n\t\t\t\t\t\t:sortOrder=\"localSort.dir || 'asc'\"\n\t\t\t\t\t\tborderless\n\t\t\t\t\t\t:rowTitle=\"rowTitle\"\n\t\t\t\t\t\t:fitWidth=\"fitWidthActive\"\n\t\t\t\t\t\t:fixedLayout=\"fixedLayoutActive\"\n\t\t\t\t\t\t:hideHeader=\"content.hideHeader === true\"\n\t\t\t\t\t\t:filterable=\"content.filterable === true\"\n\t\t\t\t\t\t:activeFilters=\"headerFilters\"\n\t\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t\t@sort=\"onSort\"\n\t\t\t\t\t\t@columnFilter=\"onHeaderFilter\">\n\t\t\t\t\t\t<template v-if=\"mappedRowActions.length > 0\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t\t<CnRowActions v-if=\"!row._readOnly\" :actions=\"mappedRowActions\" :row=\"row\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnDataTable>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-object-list-widget__table\">\n\t\t\t\t<CnDataTable\n\t\t\t\t\t:reorderable=\"reorderField !== ''\"\n\t\t\t\t\t:columns=\"resolvedColumns\"\n\t\t\t\t\t:rows=\"facetedRows\"\n\t\t\t\t\t:rowClass=\"selectionRowClass\"\n\t\t\t\t\t:loading=\"loading\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t:selectable=\"content.selectable === true\"\n\t\t\t\t\t:selectedIds=\"selectedIds\"\n\t\t\t\t\t:sortKey=\"localSort.field || null\"\n\t\t\t\t\t:sortOrder=\"localSort.dir || 'asc'\"\n\t\t\t\t\tborderless\n\t\t\t\t\t:rowTitle=\"rowTitle\"\n\t\t\t\t\t:fitWidth=\"fitWidthActive\"\n\t\t\t\t\t:fixedLayout=\"fixedLayoutActive\"\n\t\t\t\t\t:hideHeader=\"content.hideHeader === true\"\n\t\t\t\t\t:filterable=\"content.filterable === true\"\n\t\t\t\t\t:activeFilters=\"headerFilters\"\n\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t@sort=\"onSort\"\n\t\t\t\t\t@columnFilter=\"onHeaderFilter\"\n\t\t\t\t\t@rowMove=\"onRowMove\">\n\t\t\t\t\t<!-- Declarative per-row actions (`content.rowActions`).\n\t\t\t\t\t CnDataTable only paints the trailing actions column\n\t\t\t\t\t when this slot is supplied, so a widget without\n\t\t\t\t\t `rowActions` keeps the column count it had. -->\n\t\t\t\t\t<template v-if=\"mappedRowActions.length > 0\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t<CnRowActions v-if=\"!row._readOnly\" :actions=\"mappedRowActions\" :row=\"row\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<!-- `content.children`: each row's child records under it, as\n\t\t\t\t\t a nested list fed from one query for all rows. -->\n\t\t\t\t\t<template v-if=\"childrenConfig && childLink\" #row-detail=\"{ row }\">\n\t\t\t\t\t\t<CnObjectListWidget\n\t\t\t\t\t\t\tclass=\"cn-object-list-widget__children\"\n\t\t\t\t\t\t\tdata-testid=\"cn-object-list-children\"\n\t\t\t\t\t\t\t:content=\"childContentFor(row)\"\n\t\t\t\t\t\t\t:presetRows=\"childRowsFor(row)\"\n\t\t\t\t\t\t\t@rowsStale=\"fetchChildren\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnDataTable>\n\t\t\t</div>\n\t\t\t<!--\n\t\t\t Footer. Two affordances, and they answer different questions.\n\n\t\t\t The PAGER walks the rest of the matching objects in place —\n\t\t\t server-side (`_page`), so \"1–5 of 137\" is the truth and not a\n\t\t\t count of what happened to be fetched. It renders only when a\n\t\t\t whole page fits the cell; on a cell too short for one page the\n\t\t\t fit-to-cell path below is all there is, because a pager under\n\t\t\t rows that are themselves clipped would page a lie.\n\n\t\t\t VIEW ALL leaves for the full index. It shows whenever there is\n\t\t\t more than the widget is showing, pager or no pager — a widget is\n\t\t\t a summary, and the index is where the list actually lives.\n\t\t\t-->\n\t\t\t<!-- A reorder's outcome, read out: the row's new place, or why the\n\t\t\t server refused it. -->\n\t\t\t<p class=\"hidden-visually cn-object-list-widget__reorder-status\" aria-live=\"polite\" data-testid=\"cn-object-list-reorder-status\">\n\t\t\t\t{{ reorderStatus }}\n\t\t\t</p>\n\t\t\t<CnPagination\n\t\t\t\tv-if=\"showPager\"\n\t\t\t\tcompact\n\t\t\t\tclass=\"cn-object-list-widget__pager\"\n\t\t\t\t:currentPage=\"page\"\n\t\t\t\t:totalPages=\"totalPages\"\n\t\t\t\t:totalItems=\"total\"\n\t\t\t\t:currentPageSize=\"pageSize\"\n\t\t\t\t:minItemsToShow=\"0\"\n\t\t\t\t@pageChanged=\"onPageChange\" />\n\t\t\t<a\n\t\t\t\tv-if=\"hiddenCount > 0 && content.viewAllRoute && viewAllHref\"\n\t\t\t\tclass=\"cn-object-list-widget__view-all\"\n\t\t\t\t:href=\"viewAllHref\"\n\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t{{ viewAllLabel }}\n\t\t\t</a>\n\t\t\t<button\n\t\t\t\tv-else-if=\"hiddenCount > 0 && content.viewAllRoute\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-object-list-widget__view-all\"\n\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t{{ viewAllLabel }}\n\t\t\t</button>\n\t\t\t<p v-else-if=\"hiddenCount > 0 && !showPager\" class=\"cn-object-list-widget__more\">\n\t\t\t\t{{ moreLabel }}\n\t\t\t</p>\n\t\t</template>\n\t\t<!-- `content.footnote`: a line under the table (\"Awb 4:17 en 4:18\"),\n\t\t through the host translate. Not while the list waits for its\n\t\t context or failed to load. -->\n\t\t<!-- `content.summary`: counts over the loaded rows (\"(1/2 completed)\"). -->\n\t\t<p\n\t\t\tv-if=\"resolvedSummary && !waitingForContext && !error && rows.length > 0\"\n\t\t\tclass=\"cn-object-list-widget__summary\"\n\t\t\tdata-testid=\"cn-object-list-widget-summary\">\n\t\t\t{{ resolvedSummary }}\n\t\t</p>\n\t\t<p\n\t\t\tv-if=\"resolvedFootnote && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__footnote\"\n\t\t\tdata-testid=\"cn-object-list-widget-footnote\">\n\t\t\t{{ resolvedFootnote }}\n\t\t</p>\n\t\t<!-- `content.footerLinks`: a row of links under the list, each with\n\t\t an optional muted note (\"Objecttypen beheren (alleen voor\n\t\t beheerders)\"). -->\n\t\t<div\n\t\t\tv-if=\"footerLinks.length > 0 && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__links\"\n\t\t\tdata-testid=\"cn-object-list-widget-links\">\n\t\t\t<span v-for=\"link in footerLinks\" :key=\"link.key\" class=\"cn-object-list-widget__link-item\">\n\t\t\t\t<a\n\t\t\t\t\tclass=\"cn-object-list-widget__link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\tdata-testid=\"cn-object-list-widget-link\"\n\t\t\t\t\t@click=\"onFooterLink($event, link)\">{{ link.label }}</a>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"link.note\"\n\t\t\t\t\tclass=\"cn-object-list-widget__link-note\"\n\t\t\t\t\tdata-testid=\"cn-object-list-widget-link-note\">{{ link.note }}</span>\n\t\t\t</span>\n\t\t</div>\n\t\t<!-- Why the declared create action is withheld on this record\n\t\t (`content.createAction.hiddenNote`). -->\n\t\t<p\n\t\t\tv-if=\"createHiddenNote && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__hidden-note\"\n\t\t\tdata-testid=\"cn-object-list-widget-hidden-note\">\n\t\t\t{{ createHiddenNote }}\n\t\t</p>\n\t\t<!-- Create affordance (ADR-062): every collection carries its Add at\n\t\t the bottom of the widget; the host card's Actions menu calls the\n\t\t same openCreate() through the public method. Suppressed while the\n\t\t empty state is showing — that renders its own copy in its #action\n\t\t slot, and two Add buttons on one empty card read as a bug. -->\n\t\t<div\n\t\t\tv-if=\"allowCreate && !waitingForContext && !showingEmptyState\"\n\t\t\tclass=\"cn-object-list-widget__add\">\n\t\t\t<NcButton variant=\"tertiary\" wide @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<!-- Upload affordance. A `dropZone` action already accepts a dropped\n\t\t `File[]`; this is the click-to-pick equivalent of the same drop,\n\t\t for the reader who never drags a file. `content.upload: false`\n\t\t opts a drop-only widget out of the button. -->\n\t\t<button\n\t\t\tv-if=\"showUploadButton\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-object-list-widget__upload\"\n\t\t\tdata-testid=\"object-list-upload\"\n\t\t\t@click=\"triggerUpload\">\n\t\t\t{{ uploadLabel }}\n\t\t</button>\n\t\t<input\n\t\t\tv-if=\"showUploadButton\"\n\t\t\tref=\"uploadInput\"\n\t\t\ttype=\"file\"\n\t\t\tmultiple\n\t\t\t:aria-label=\"uploadLabel\"\n\t\t\tclass=\"cn-object-list-widget__upload-input\"\n\t\t\tdata-testid=\"object-list-upload-input\"\n\t\t\t@change=\"onUploadFilesSelected\">\n\t\t<CnFormDialog\n\t\t\tv-if=\"(showCreate || editingRow) && createSchema\"\n\t\t\tref=\"createDialog\"\n\t\t\t:schema=\"createSchema\"\n\t\t\t:item=\"editingRow\"\n\t\t\t:register=\"content.register || ''\"\n\t\t\t:initialData=\"editingRow ? null : createInitialData\"\n\t\t\t:lockedFields=\"createLockedFields\"\n\t\t\t:size=\"formSize\"\n\t\t\t:columns=\"formColumns\"\n\t\t\t:includeFields=\"formIncludeFields\"\n\t\t\t:excludeFields=\"formExcludeFields\"\n\t\t\t:fieldOverrides=\"formFieldOverrides\"\n\t\t\t@confirm=\"editingRow ? onEditConfirm($event) : onCreateConfirm($event)\"\n\t\t\t@close=\"closeForm\" />\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnDataTable from '../CnDataTable/CnDataTable.vue'\nimport CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'\nimport CnFormDialog from '../CnFormDialog/CnFormDialog.vue'\nimport CnPagination from '../CnPagination/CnPagination.vue'\nimport CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'\nimport { endpointErrorMessage, fetchEndpointSource, getByPath, resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useWriteFeedback } from '../../composables/useWriteFeedback.js'\nimport innerHeading from '../../mixins/innerHeading.js'\nimport { actionLink, dispatchAction, hasActionTargetTokens } from '../../utils/actionsDispatcher.js'\nimport { fillCountLabel } from '../../utils/fillCountLabel.js'\nimport { headerFilterParams, nextHeaderFilters } from '../../utils/headerFilterState.js'\nimport { buildExportUrl } from '../../utils/indexExportHelpers.js'\nimport { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'\nimport { createActionState } from '../../utils/listCreateAction.js'\nimport { objectDisplayName, objectFieldValue } from '../../utils/objectName.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { moveIndex, renumberRows, rowIdOf } from '../../utils/reorderRows.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\nimport { markNewTabHandled } from '../../utils/rowAuxClick.js'\nimport { actionForRow, hasRowTokens, resolveRowFilter, rowTokenField } from '../../utils/rowTokens.js'\nimport { changedKeys } from '../../utils/saveEdited.js'\nimport { evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'\nimport { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'\nimport { HOST_WIDGET_ID, WIDGET_SELECTION } from '../../utils/widgetSelection.js'\nimport { getCurrentUserGroups } from '../../utils/widgetVisibility.js'\nimport { CnRowActions } from '../CnRowActions/index.js'\n\n/**\n * Event-bus channel a page-level refresh is announced on. The page's Actions\n * menu emits it, the manifest `refresh` / `api-call` actions bump it, and an\n * app's own dialog emits it after a successful write.\n */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * CnObjectListWidget — an abstract, manifest-configured object list / table.\n *\n * Queries OpenRegister objects for a `register` + `schema` with a `filter`,\n * `sort`, and `limit`, then renders the chosen `columns` in a `CnDataTable`.\n * Replaces per-app coded list widgets (closing-soon, recently-won-lost,\n * renewals-due, …) — the data, columns, filter and ordering are all editable\n * through the cog modal (ADR-041). Resolved by its registry type key\n * `object-list`; configured via `CnObjectListWidgetForm`.\n *\n * Example content blob:\n * ```js\n * content: {\n * register: 'pipelinq', schema: 'lead',\n * filter: { status: 'open' },\n * sort: { field: 'expectedCloseDate', dir: 'asc' },\n * limit: 5,\n * columns: [{ key: 'title', label: 'Deal' }, { key: 'value', label: 'Value' }],\n * rowRoute: 'leads-detail',\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectListWidget',\n\n\tcomponents: { CnDataTable, CnFormDialog, CnPagination, CnWidgetEmptyState, CnRowActions, CnFkResolveCell, NcButton, Plus },\n\n\tmixins: [innerHeading],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object, register, schema }`)\n\t\t * provided by CnDetailPage. Enables `@objectId` / `@object.<field>`\n\t\t * filter tokens so a detail-page list can be scoped to the current\n\t\t * object. Null on dashboards (tokens then pass through unresolved).\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/** The page's row selection (CnDetailPage), for `content.rowSelection`. */\n\t\tcnWidgetSelection: { from: WIDGET_SELECTION, default: null },\n\t\t/** This widget's id on the page (CnDetailWidgetHost), the key it publishes under. */\n\t\tcnHostWidgetId: { from: HOST_WIDGET_ID, default: null },\n\t\t/**\n\t\t * Page-level workspace context (reactive `ref({})`) provided by\n\t\t * CnDashboardPage. Lets `@workspace.<key>` filter tokens resolve so a\n\t\t * list can react to state another widget on the page wrote (e.g. a\n\t\t * client-overview list scoped to the selected client). Null on pages\n\t\t * that don't provide it (tokens then stay unresolved and the list shows\n\t\t * its `promptText`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.emptyText` is run through it for this\n\t\t * component's OWN empty state, and so are `content.prompt` and\n\t\t * `content.addLabel`. Defaults to an identity function so\n\t\t * an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Pre-bound `dispatchAction` provided by CnPageRenderer (router,\n\t\t * registry, handlers and openModal already wired). `rowActions` and\n\t\t * `dropZone` dispatch through it; outside a CnPageRenderer tree the\n\t\t * component falls back to a bare `dispatchAction` call, which reaches\n\t\t * the router-backed types and warns on the registry-backed ones.\n\t\t */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. `limit` is a fetch cap\n\t\t * (default 25) — the rendered row count fits the host cell (ADR-062).\n\t\t * `viewAllRoute` / `viewAllQuery` configure the \"View all (N)\" footer\n\t\t * navigation; `viewAllQuery` values are token-resolved (`@objectId`).\n\t\t *\n\t\t * `extend` is the OpenRegister `_extend[]` list forwarded on the fetch.\n\t\t * It is what makes a DOTTED column key work: `CnDataTable` reads\n\t\t * `informatieobject.title` as a path into the row, and without\n\t\t * `extend: ['informatieobject']` the row holds a uuid string at that\n\t\t * key, so six columns off one referenced object render as six copies\n\t\t * of the same uuid. `fkResolve` is the one-label answer; `extend` is\n\t\t * the several-fields-off-the-same-reference answer.\n\t\t *\n\t\t * `rowActions` is an array in the unified manifest action shape\n\t\t * (`handler` | `open-modal` | `open-page` | `navigate` | …), rendered\n\t\t * per row through CnRowActions. `dropZone` is one action of that same\n\t\t * shape, dispatched when files are dropped on the widget, with the\n\t\t * dropped `File[]` handed to it. `content.upload: false` hides the\n\t\t * click-to-pick button that otherwise renders alongside a `dropZone`.\n\t\t * Neither carries authorization: OpenRegister RBAC is the only\n\t\t * authority over what a write may do.\n\t\t *\n\t\t * `groupBy` (a row field path, dotted paths read the `extend`-inlined\n\t\t * reference the same way a column does) buckets the fetched rows into\n\t\t * one heading + table per distinct value, in first-seen order.\n\t\t * `groupLabel` is the field path used for the heading text; it\n\t\t * defaults to the raw `groupBy` value when absent. `groupLabelResolve`\n\t\t * (`{register, schema, labelField?}`) is for a `groupBy` whose human\n\t\t * label sits a hop further than `extend` reaches (a reference's own\n\t\t * reference): the heading resolves the raw group key through the\n\t\t * shared object store, the same `fkResolve` cell widget a column uses.\n\t\t *\n\t\t * `selectable` turns on CnDataTable's checkbox column; `bulkActions`\n\t\t * (same action shape as `rowActions`) render as buttons in a bar that\n\t\t * appears once something is selected. A `handler` bulk action receives\n\t\t * the array of selected row objects appended to its `args`; an\n\t\t * `open-modal` bulk action receives them as `props.selectedIds`\n\t\t * (mirroring how a drop hands `props.files` to a modal).\n\t\t *\n\t\t * `sortable` (`true`, or an array of column keys) makes the matching\n\t\t * `CnDataTable` headers clickable; clicking re-fetches with that\n\t\t * field's `_order`, same as the fixed `sort` key but user-driven.\n\t\t *\n\t\t * `facet` (`{ field, label? }`) renders one filter chip per distinct\n\t\t * value of that field across the currently loaded rows (an array\n\t\t * field's entries are flattened) and narrows the rendered rows to\n\t\t * those carrying a selected value. Client-side over the fetched page,\n\t\t * like the rest of this widget's row set.\n\t\t *\n\t\t * `createAction` (one action of the `rowActions` shape) replaces the\n\t\t * list's own create form: every Add, here and in the host card's menu,\n\t\t * runs it instead, labelled with its `label`. Its `visibleWhen` is\n\t\t * evaluated against the page record, and while it fails the Add is\n\t\t * hidden and `hiddenNote` (a string, or `[{ when, text }]` where the\n\t\t * first holding entry wins) is shown in its place.\n\t\t *\n\t\t * `summary` (`{ text, counts }`) is a line under the rows: `{total}` is\n\t\t * the number of loaded rows and each `counts` key the number of loaded\n\t\t * rows its `visibleWhen`-shaped condition holds for.\n\t\t *\n\t\t * `hideHeader: true` drops the table's column header row (CnDataTable's\n\t\t * `hideHeader`), for a short list that reads as rows, not as a table.\n\t\t *\n\t\t * `rowIdField` (a dot-path) names the row field holding the id\n\t\t * `rowRoute` opens, for a row that links to another record.\n\t\t * `footnote` is a line under the table, through the host translate.\n\t\t *\n\t\t * `export` (`{ label?, format?, group? }`) offers the filtered list as a\n\t\t * download in the card's header; `group` shows it to one group only.\n\t\t * With `url` (and `params`, tokens such as `@objectId`) the button\n\t\t * downloads that app endpoint instead of OpenRegister's export.\n\t\t *\n\t\t * `endpointSource` (`{ url, params?, responsePath? }`) reads the rows\n\t\t * from an app endpoint instead of OpenRegister, for a list without\n\t\t * `register`/`schema`: rows at `responsePath` (default `results`),\n\t\t * the count at `total`. Such a list is read-only.\n\t\t *\n\t\t * `tableBleed: true` runs the table edge to edge in its card: in the\n\t\t * board look it goes under the card's side padding and its outer cells\n\t\t * take that padding, so the column text lines up with the card title.\n\t\t * Without it the table stays inside the padding.\n\t\t * `headerSubtitle` is a line under the card title with the list's\n\t\t * counts: `{total}` and `{count:<field>=<value>}` (\"{total} vastgelegd\").\n\t\t * `headerNote` takes the same counts beside the card title instead.\n\t\t *\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 * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowIdField?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}, tableBleed?: boolean}}\n\t\t * `children` (`{ register?, schema, filter, columns, reorder, rowActions,\n\t\t * … }`) shows each row's child records under it, as a list of the same\n\t\t * kind: `filter` names the link with an `@row.<field>` token\n\t\t * (`{ courseId: \"@row.id\" }`). The children of every row come from one\n\t\t * query on the list of parent values.\n\t\t *\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 * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowIdField?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}, children?: object}}\n\t\t * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupOrder?: Array<string>, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowRouteField?: string, rowRouteMap?: object, rowIdField?: string, rowRouteIdField?: string, createEndpoint?: {url: string, bodyKey?: string, extra?: object}, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Rows handed in instead of fetched. The list then reads nothing\n\t\t * itself: where it would read again (after a write, a refused\n\t\t * reorder) it emits `rows-stale` for the owner to hand in new rows.\n\t\t * A `children` list gives each nested list its rows this way, from\n\t\t * one query for all parents.\n\t\t */\n\t\tpresetRows: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped', 'header', 'rows-stale'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The current user's groups once a `footerLinks` entry with `group` asked for them; null before. */\n\t\t\tfooterGroups: null,\n\t\t\t/** What the last reorder did, for the live region: the new place, or the server's refusal. */\n\t\t\treorderStatus: '',\n\t\t\trows: [],\n\t\t\t/** The rows `content.readOnlyRows` adds after the list's own, tagged `_readOnly`. */\n\t\t\treadOnlyRows: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\t/** Server-side total for the resolved filter (drives \"View all (N)\"). */\n\t\t\ttotal: 0,\n\t\t\t/** The card header's Download action once `content.export` offered it; null before. */\n\t\t\theaderAction: null,\n\t\t\t/** Whether the header was last told a counts line (`content.headerSubtitle`), so an empty one clears it. */\n\t\t\theaderSubtitleShown: false,\n\t\t\t/** Current 1-based page. Paging is SERVER-side (`_page`). */\n\t\t\tpage: 1,\n\t\t\t/** Rows that fit the host cell; null = unconstrained (dashboards). */\n\t\t\tfitRows: null,\n\t\t\t/**\n\t\t\t * How far the table has been squeezed to the widget's width, so the\n\t\t\t * card never scrolls sideways: 0 = its own auto layout, 1 = one column\n\t\t\t * takes what is left and is cut with an ellipsis while the rest keep\n\t\t\t * their content on one line, 2 = a fixed layout, every column an equal\n\t\t\t * share with its text wrapped. Each step is taken only while the table\n\t\t\t * still scrolls sideways. `content.fitWidth` forces stage 1 on or off.\n\t\t\t */\n\t\t\tfitStage: 0,\n\t\t\t/** Whether the create dialog is open. */\n\t\t\tshowCreate: false,\n\t\t\t/** The row the form edits (`content.editable`), or null while it creates. */\n\t\t\teditingRow: null,\n\t\t\t/**\n\t\t\t * The list's own schema definition, once fetched.\n\t\t\t *\n\t\t\t * Shared by two readers: the create dialog renders its form from it,\n\t\t\t * and `resolvedColumns` takes a column's HEADING from the matching\n\t\t\t * property's `title` when the manifest gave only a key.\n\t\t\t */\n\t\t\tcreateSchema: null,\n\t\t\t/**\n\t\t\t * Depth of nested dragenter/dragleave pairs over the widget.\n\t\t\t * A single counter, because `dragleave` fires when the pointer\n\t\t\t * crosses into a CHILD element: tracking a boolean instead makes\n\t\t\t * the overlay flicker off the moment the drag reaches the table.\n\t\t\t */\n\t\t\tdragDepth: 0,\n\t\t\t/** Selected row ids (`content.selectable`). Shared across groups. */\n\t\t\tselectedIds: [],\n\t\t\t/**\n\t\t\t * The active sort, seeded from `content.sort` and overwritten by an\n\t\t\t * interactive header click (`content.sortable`). Kept as component\n\t\t\t * state rather than read from `content` directly so a click can\n\t\t\t * override the manifest-declared default without mutating a prop.\n\t\t\t */\n\t\t\tlocalSort: { ...(this.content.sort || {}) },\n\t\t\t/** The column-head filters (`content.filterable`), `{ paramKey: values[] }`. */\n\t\t\theaderFilters: {},\n\t\t\t/** Facet values currently toggled on (`content.facet`). */\n\t\t\tfacetSelected: [],\n\t\t\t/** Child rows per parent row id (`content.children`). */\n\t\t\tchildRows: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The class that marks the selected row (`content.rowSelection`), or\n\t\t * null when this list offers no row selection.\n\t\t *\n\t\t * @return {((row: object) => string)|null} The class per row.\n\t\t * @spec openspec/changes/side-card-follows-row/specs/side-card-follows-row/spec.md#requirement-a-list-publishes-its-rows-and-its-selected-row\n\t\t */\n\t\tselectionRowClass() {\n\t\t\tif (this.content.rowSelection !== true || !this.cnWidgetSelection || typeof this.cnHostWidgetId !== 'function') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst selected = this.cnWidgetSelection.entry(this.cnHostWidgetId()).selected\n\t\t\tconst idOf = (row) => String((row && (row.id ?? row['@self']?.id)) ?? '')\n\t\t\treturn (row) => (selected && idOf(row) !== '' && idOf(row) === idOf(selected) ? 'cn-object-list-widget__row--selected' : '')\n\t\t},\n\n\t\t/**\n\t\t * How the table draws its title column (`content.rowTitle`: link, plain,\n\t\t * text or strong), link when unset or unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text\n\t\t */\n\t\trowTitle() {\n\t\t\treturn ['plain', 'text', 'strong'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'\n\t\t},\n\n\t\t/**\n\t\t * Whether the table fits the widget's width (stage 1): `content.fitWidth`\n\t\t * when it says, else once the columns were measured not to fit.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfitWidthActive() {\n\t\t\tconst forced = this.content?.fitWidth\n\t\t\treturn typeof forced === 'boolean' ? forced : this.fitStage === 1\n\t\t},\n\n\t\t/**\n\t\t * Whether the table takes a fixed layout, the last resort when even the\n\t\t * fitted table is wider than the widget (not with `content.fitWidth` set).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfixedLayoutActive() {\n\t\t\treturn typeof this.content?.fitWidth !== 'boolean' && this.fitStage >= 2\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped detail-page object context for token resolution, or null\n\t\t * on surfaces (dashboards) that don't provide one.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tobjectCtx() {\n\t\t\tconst c = this.cnObjectContext\n\t\t\tif (!c) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn (typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace context bag (or null). Vue 2.7 `setup`/inject\n\t\t * may hand back a raw ref; unwrap `.value` so token resolution reads the\n\t\t * plain object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tworkspaceCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tif (!c) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn (typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t},\n\n\t\t/**\n\t\t * Token-resolution context merged from the detail-page object context\n\t\t * and the page-level workspace bag (`@workspace.<key>`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst base = this.objectCtx ? { ...this.objectCtx } : {}\n\t\t\tbase.workspace = this.workspaceCtx || {}\n\t\t\treturn base\n\t\t},\n\n\t\t/**\n\t\t * The filter with every `@`-token resolved against `tokenCtx`, then with\n\t\t * any UNRESOLVED OPTIONAL token (`@workspace.<key>?`) dropped — so an\n\t\t * optional queue filter simply shows all rows until a queue is picked,\n\t\t * while a REQUIRED token (`@workspace.selectedClient`) stays to trigger the\n\t\t * prompt.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedFilter() {\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(this.content.filter || {}, this.tokenCtx))\n\t\t},\n\n\t\t/**\n\t\t * Whether a context-dependent filter token (e.g. `@workspace.selectedClient`)\n\t\t * is still unresolved — the page state this list depends on isn't set yet,\n\t\t * so the list renders a prompt instead of fetching the whole register.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\twaitingForContext() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/**\n\t\t * Prompt shown while a context-bound list has an unresolved REQUIRED\n\t\t * token. A `content.prompt` override always wins. Otherwise the default\n\t\t * is context-aware: the \"Select an item to see related records\"\n\t\t * master-detail copy only fits a DASHBOARD list waiting on a selection —\n\t\t * on a detail page (an object context is present) that copy is wrong, so\n\t\t * fall back to the neutral \"Nothing here yet\" (ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tpromptText() {\n\t\t\tif (this.content.prompt) {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(this.content.prompt)\n\t\t\t}\n\t\t\treturn this.objectCtx\n\t\t\t\t? t('nextcloud-vue', 'Nothing here yet')\n\t\t\t\t: t('nextcloud-vue', 'Select an item to see related records')\n\t\t},\n\n\t\t/**\n\t\t * Quiet, status-code-free label shown when a fetch fails. The real\n\t\t * axios error is logged to the console, never rendered (ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tloadErrorLabel() {\n\t\t\treturn this.content.errorText || t('nextcloud-vue', 'Could not load these records')\n\t\t},\n\n\t\t/**\n\t\t * Column definitions normalised for CnDataTable. A string column becomes\n\t\t * `{ key, label }`; an object column keeps its key/label AND carries the\n\t\t * presentation hints CnDataTable forwards to CnCellRenderer — `format`\n\t\t * (currency / duration / number / percent / date / date-time), `widget`\n\t\t * (badge / link / swatch), `widgetProps`, `formatter`, `align`, and\n\t\t * `width`. Without this pass-through a `relatedCollections` column's\n\t\t * `format: 'currency'` / `'date'` would silently render as plain text.\n\t\t *\n\t\t * `enumLabels` travels alongside `enum` for the same reason `enum`\n\t\t * itself is here: a list column often addresses a DOTTED path into an\n\t\t * `extend`-inlined reference (e.g. `informatieobject.direction`), and\n\t\t * this widget never fetches that reference's own schema to feed\n\t\t * CnDataTable's live `:schema` prop — only `openCreate()`'s create-form\n\t\t * schema is ever fetched. A column that needs its raw enum code\n\t\t * (`\"incoming\"`) to render as a human label (`\"Incoming\"`) has to carry\n\t\t * that mapping itself, the same way it already carries `enum`. This is\n\t\t * the same generic mechanism CnCellRenderer already applies to a real\n\t\t * schema property's `x-enum-labels`, completed here for the\n\t\t * manifest-column path rather than special-cased per column.\n\t\t */\n\t\tresolvedColumns() {\n\t\t\tconst cols = Array.isArray(this.content.columns) ? this.content.columns : []\n\t\t\tconst sortableConfig = this.content.sortable\n\t\t\t// A column may arrive as a bare key. Falling back to that key as the\n\t\t\t// HEADING is what printed `customerName` and `dateReceived` at users\n\t\t\t// on a dashboard, while the index page beside it said \"Customer name\"\n\t\t\t// and \"Date received\" — the same data, labelled twice, differently.\n\t\t\t// The schema already holds the answer, so ask it, and keep the key\n\t\t\t// only when it has nothing to say.\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst headingFor = (key) => {\n\t\t\t\tconst prop = props[key]\n\t\t\t\tconst title = prop && typeof prop.title === 'string' ? prop.title.trim() : ''\n\t\t\t\treturn title !== '' ? title : key\n\t\t\t}\n\t\t\t// CnDataTable gets no `:schema` here, so a column takes the\n\t\t\t// schema property's `format` and enum (with its labels) itself,\n\t\t\t// unless it says its own or draws a widget or formatter.\n\t\t\tconst withSchemaPresentation = (out) => {\n\t\t\t\tconst prop = props[out.key]\n\t\t\t\t// The `label` widget writes the enum's label, so it still takes the enum.\n\t\t\t\tif (!prop || (out.widget !== undefined && out.widget !== 'label') || out.formatter !== undefined) {\n\t\t\t\t\treturn out\n\t\t\t\t}\n\t\t\t\tif (out.format === undefined && typeof prop.format === 'string' && prop.format !== '') {\n\t\t\t\t\tout.format = prop.format\n\t\t\t\t}\n\t\t\t\tif (out.enum === undefined && Array.isArray(prop.enum) && prop.enum.length > 0) {\n\t\t\t\t\tout.enum = prop.enum\n\t\t\t\t\tconst labels = prop.enumLabels || prop['x-enum-labels']\n\t\t\t\t\tif (out.enumLabels === undefined && labels && typeof labels === 'object') {\n\t\t\t\t\t\tout.enumLabels = labels\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t}\n\t\t\tconst mapped = cols.map((c) => {\n\t\t\t\tif (typeof c === 'string') {\n\t\t\t\t\treturn withSchemaPresentation({ key: c, label: headingFor(c) })\n\t\t\t\t}\n\t\t\t\tconst out = { key: c.key, label: c.label || headingFor(c.key) }\n\t\t\t\t// `secondary` (a second, muted line under the cell) rides\n\t\t\t\t// along too: the table draws it, and dropping it here took the\n\t\t\t\t// list rows' second line away (zuiddrecht-pixel-gaps-3).\n\t\t\t\tfor (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {\n\t\t\t\t\tif (c[k] !== undefined) {\n\t\t\t\t\t\tout[k] = c[k]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twithSchemaPresentation(out)\n\t\t\t\t// `content.sortable` (bool = every column, array = the listed\n\t\t\t\t// keys) fills in `sortable` for a column that did not already\n\t\t\t\t// say so on its own definition — an explicit `sortable: false`\n\t\t\t\t// on the column still wins, so one header can opt out of an\n\t\t\t\t// otherwise sortable list.\n\t\t\t\tif (out.sortable === undefined && sortableConfig !== undefined) {\n\t\t\t\t\tout.sortable = sortableConfig === true\n\t\t\t\t\t\t|| (Array.isArray(sortableConfig) && sortableConfig.includes(out.key))\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t})\n\n\t\t\t// The registry used to default to `[{ key: 'title' }]`, but plenty of schemas\n\t\t\t// have no `title` — a Barn has `name`. Such a widget rendered a table of\n\t\t\t// em-dashes forever, and nothing told the user why.\n\t\t\t//\n\t\t\t// Don't guess the right key from the data: OpenRegister already guessed, and\n\t\t\t// published the answer as `@self.name`. When NOT ONE configured column\n\t\t\t// resolves on any loaded row, fall back to that single display-name column.\n\t\t\t//\n\t\t\t// Only when none resolve: a partly-matching column set is a deliberate choice\n\t\t\t// (a column can be legitimately empty across the current page of rows), and\n\t\t\t// silently replacing it would override the user.\n\t\t\tif (this.rows.length > 0 && mapped.length > 0) {\n\t\t\t\tconst resolves = mapped.some((c) => this.rows.some((r) => objectFieldValue(r, c.key) !== undefined))\n\t\t\t\tif (resolves === false) {\n\t\t\t\t\treturn [{ key: 'name', label: t('nextcloud-vue', 'Name') }]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn mapped\n\t\t},\n\n\t\t/**\n\t\t * Declared per-row actions mapped onto the CnRowActions shape. Every\n\t\t * entry keeps its label, icon and `destructive` flag and routes its\n\t\t * click back through `runRowAction`, so the dispatcher decides what a\n\t\t * type means and this component does not grow a second action\n\t\t * vocabulary.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmappedRowActions() {\n\t\t\tconst declared = Array.isArray(this.content.rowActions) ? this.content.rowActions : []\n\t\t\tconst mapped = declared\n\t\t\t\t.filter((a) => a && typeof a === 'object')\n\t\t\t\t.map((action) => ({\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\ticon: action.icon,\n\t\t\t\t\tdestructive: action.destructive === true,\n\t\t\t\t\t// CnRowActions renders a link when these are set and skips the handler.\n\t\t\t\t\t...this.rowActionLink(action),\n\t\t\t\t\thandler: (row) => this.runRowAction(action, row),\n\t\t\t\t}))\n\t\t\tif (this.content.editable === true) {\n\t\t\t\tmapped.unshift({ label: t('nextcloud-vue', 'Edit'), icon: 'Pencil', handler: (row) => this.openEdit(row) })\n\t\t\t}\n\t\t\tif (this.content.deletable === true) {\n\t\t\t\tmapped.push({ label: t('nextcloud-vue', 'Delete'), icon: 'TrashCanOutline', destructive: true, handler: (row) => this.deleteRow(row) })\n\t\t\t}\n\t\t\tif (this.reorderField === '') {\n\t\t\t\treturn mapped\n\t\t\t}\n\t\t\t// The keyboard half of the drag: one place up or down.\n\t\t\treturn [\n\t\t\t\t...mapped,\n\t\t\t\t{ label: t('nextcloud-vue', 'Move up'), icon: 'ArrowUp', handler: (row) => this.moveRowBy(row, -1) },\n\t\t\t\t{ label: t('nextcloud-vue', 'Move down'), icon: 'ArrowDown', handler: (row) => this.moveRowBy(row, 1) },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The field a reorderable list is ordered by (`content.reorder.field`),\n\t\t * or '' when the list does not reorder. A grouped list does not.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\treorderField() {\n\t\t\tconst reorder = this.content && this.content.reorder\n\t\t\tconst field = reorder && typeof reorder === 'object' ? reorder.field : ''\n\t\t\treturn typeof field === 'string' && field !== '' && !this.isGrouped ? field : ''\n\t\t},\n\n\t\t/**\n\t\t * The declared child list (`content.children`), or null. A grouped\n\t\t * list shows no children.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildrenConfig() {\n\t\t\tconst children = this.content && this.content.children\n\t\t\tif (!children || typeof children !== 'object' || Array.isArray(children) || !children.schema || this.isGrouped) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn children\n\t\t},\n\n\t\t/**\n\t\t * How a child points at its row: the child `filter` key whose value\n\t\t * is an `@row.<field>` token, and that row field. Null without one.\n\t\t *\n\t\t * @return {{key: string, field: string}|null}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildLink() {\n\t\t\tconst filter = (this.childrenConfig && this.childrenConfig.filter) || {}\n\t\t\tfor (const [key, value] of Object.entries(filter)) {\n\t\t\t\tconst field = rowTokenField(value)\n\t\t\t\tif (field !== '') {\n\t\t\t\t\treturn { key, field }\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The declared drop-zone action, or null. A widget without the key\n\t\t * takes no part in a drag at all.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tdropZoneAction() {\n\t\t\tconst dz = this.content.dropZone\n\t\t\treturn (dz && typeof dz === 'object') ? dz : null\n\t\t},\n\n\t\t/** Whether a file drag is currently over a drop-enabled widget. */\n\t\tdropping() {\n\t\t\treturn this.dropZoneAction !== null && this.dragDepth > 0\n\t\t},\n\n\t\t/** Copy shown on the drop overlay (overridable via `dropZone.label`). */\n\t\tdropLabel() {\n\t\t\treturn (this.dropZoneAction && this.dropZoneAction.label)\n\t\t\t\t|| t('nextcloud-vue', 'Drop files here')\n\t\t},\n\n\t\t/**\n\t\t * Whether the click-to-pick upload button renders: a `dropZone` is\n\t\t * declared and the host has not opted out with `content.upload: false`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowUploadButton() {\n\t\t\treturn this.dropZoneAction !== null && this.content.upload !== false\n\t\t},\n\n\t\t/** Pre-translated Upload label (overridable via `content.uploadLabel`). */\n\t\tuploadLabel() {\n\t\t\treturn this.content.uploadLabel || t('nextcloud-vue', 'Upload')\n\t\t},\n\n\t\t/**\n\t\t * Declared bulk actions mapped onto the same `{label, icon, destructive,\n\t\t * handler}` shape `mappedRowActions` uses, so CnRowActions and the bulk\n\t\t * bar render identically — the only difference is what `handler` does:\n\t\t * a row action dispatches with one row, a bulk action with the whole\n\t\t * selection.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmappedBulkActions() {\n\t\t\tconst declared = Array.isArray(this.content.bulkActions) ? this.content.bulkActions : []\n\t\t\treturn declared\n\t\t\t\t.filter((a) => a && typeof a === 'object')\n\t\t\t\t.map((action) => ({\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\ticon: action.icon,\n\t\t\t\t\tdestructive: action.destructive === true,\n\t\t\t\t\thandler: () => this.runBulkAction(action),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The currently selected row objects, resolved from `rows` (not just\n\t\t * the visible/faceted slice) so a selection made before a facet filter\n\t\t * narrows the view is not silently dropped from a bulk action.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tselectedRows() {\n\t\t\treturn this.rows.filter((row) => this.selectedIds.includes(row.id))\n\t\t},\n\n\t\t/**\n\t\t * Distinct values of `content.facet.field` across the currently loaded\n\t\t * rows, flattening an array-valued field (the \"keywords across the\n\t\t * dossier\" shape this seam was measured against). Sorted for a stable\n\t\t * chip order run to run.\n\t\t *\n\t\t * @return {Array<string>}\n\t\t */\n\t\tfacetOptions() {\n\t\t\tconst facet = this.content.facet\n\t\t\tif (!facet || !facet.field) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst values = new Set()\n\t\t\tfor (const row of this.visibleRows) {\n\t\t\t\tconst raw = objectFieldValue(row, facet.field)\n\t\t\t\tconst list = Array.isArray(raw) ? raw : (raw !== undefined && raw !== null && raw !== '' ? [raw] : [])\n\t\t\t\tfor (const v of list) {\n\t\t\t\t\tif (v !== undefined && v !== null && v !== '') {\n\t\t\t\t\t\tvalues.add(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.from(values).sort()\n\t\t},\n\n\t\t/** Pre-translated facet label (`content.facet.label`, falls back to a generic \"Filter\"). */\n\t\tfacetLabel() {\n\t\t\tconst facet = this.content.facet\n\t\t\treturn (facet && facet.label) || t('nextcloud-vue', 'Filter')\n\t\t},\n\n\t\t/**\n\t\t * Rows narrowed by the selected facet values. A row matches when at\n\t\t * least one of its (possibly array) values at `facet.field` is among\n\t\t * `facetSelected` — no selection means no narrowing at all.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tfacetedRows() {\n\t\t\tconst facet = this.content.facet\n\t\t\tif (!facet || !facet.field || this.facetSelected.length === 0) {\n\t\t\t\treturn this.visibleRows\n\t\t\t}\n\t\t\treturn this.visibleRows.filter((row) => {\n\t\t\t\tconst raw = objectFieldValue(row, facet.field)\n\t\t\t\tconst list = Array.isArray(raw) ? raw : (raw !== undefined && raw !== null ? [raw] : [])\n\t\t\t\treturn list.some((v) => this.facetSelected.includes(v))\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the fetch returned rows but the active facet selection\n\t\t * matches none of them — the \"clear filter\" empty state, distinct\n\t\t * from \"there are no items at all\".\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowNoFacetMatchState() {\n\t\t\treturn !this.loading && this.rows.length > 0 && this.facetSelected.length > 0 && this.facetedRows.length === 0\n\t\t},\n\n\t\t/** Whether `content.groupBy` is declared, so the template picks the grouped render path. */\n\t\tisGrouped() {\n\t\t\treturn typeof this.content.groupBy === 'string' && this.content.groupBy !== ''\n\t\t},\n\n\t\t/**\n\t\t * `content.groupLabelResolve` (`{register, schema, labelField?}`), or\n\t\t * null. When set, the group heading resolves `group.key` through the\n\t\t * shared object store instead of reading a plain field off the row —\n\t\t * for a `groupBy` whose human label lives a hop further than `extend`\n\t\t * inlined (a reference's own reference, e.g. grouping documents by\n\t\t * type when only the document itself, not its type, was extended).\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tgroupLabelResolveConfig() {\n\t\t\tconst cfg = this.content.groupLabelResolve\n\t\t\treturn (cfg && typeof cfg === 'object' && cfg.register && cfg.schema) ? cfg : null\n\t\t},\n\n\t\t/**\n\t\t * The faceted rows bucketed by `content.groupBy`, one entry per\n\t\t * distinct value: the keys `content.groupOrder` lists first, in that\n\t\t * order, then the others in first-seen order (matching how the rows\n\t\t * already arrived — server-sorted). A `$ref` group key that resolves to an\n\t\t * object (an `extend`-inlined reference) groups by its `id`, so\n\t\t * `groupBy: 'informatieobjecttype'` groups the actual referenced\n\t\t * objects rather than one group per string-ified object.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, rows: Array<object>}>}\n\t\t */\n\t\tgroupedRows() {\n\t\t\tif (!this.isGrouped) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst field = this.content.groupBy\n\t\t\tconst labelField = this.content.groupLabel || field\n\t\t\tconst order = []\n\t\t\tconst byKey = new Map()\n\t\t\tfor (const row of this.facetedRows) {\n\t\t\t\tconst rawKey = objectFieldValue(row, field)\n\t\t\t\tconst key = (rawKey && typeof rawKey === 'object')\n\t\t\t\t\t? (rawKey.id || JSON.stringify(rawKey))\n\t\t\t\t\t: String(rawKey ?? '')\n\t\t\t\tif (!byKey.has(key)) {\n\t\t\t\t\tconst rawLabel = objectFieldValue(row, labelField)\n\t\t\t\t\tconst label = (rawLabel && typeof rawLabel === 'object')\n\t\t\t\t\t\t? (rawLabel.title || rawLabel.name || key)\n\t\t\t\t\t\t: (rawLabel || key || t('nextcloud-vue', 'Ungrouped'))\n\t\t\t\t\tbyKey.set(key, { key, label, rows: [] })\n\t\t\t\t\torder.push(key)\n\t\t\t\t}\n\t\t\t\tbyKey.get(key).rows.push(row)\n\t\t\t}\n\t\t\tconst first = Array.isArray(this.content.groupOrder) ? this.content.groupOrder.map(String) : []\n\t\t\tconst rank = (key) => (first.includes(key) ? first.indexOf(key) : first.length)\n\t\t\treturn order\n\t\t\t\t.map((key, index) => ({ key, index }))\n\t\t\t\t.sort((a, b) => rank(a.key) - rank(b.key) || a.index - b.index)\n\t\t\t\t.map(({ key }) => byKey.get(key))\n\t\t},\n\n\t\t/** Empty-state text (overridable via `content.emptyText`). */\n\t\temptyText() {\n\t\t\treturn this.content.emptyText || t('nextcloud-vue', 'No items')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text run through the host translate function. Used\n\t\t * only by this component's OWN empty state — the raw `emptyText`\n\t\t * still goes to CnDataTable, which translates at its own render\n\t\t * boundary, so the string is never translated twice.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\n\t\t/**\n\t\t * `content.footerLinks` (`[{ label, route?, query?, href?, note?, group? }]`)\n\t\t * ready to draw: labels and notes through the host translate, a `route`\n\t\t * with its token-resolved `query` as a router location and its href, an\n\t\t * `href` as given. A link with `group` shows only to that group's\n\t\t * members, and not before the groups are known.\n\t\t *\n\t\t * @spec openspec/changes/object-list-footer-links/specs/object-list/spec.md#requirement-an-object-list-can-end-with-a-row-of-links\n\t\t * @return {Array<{key: string, label: string, note: string, href: string, location: object|null}>}\n\t\t */\n\t\tfooterLinks() {\n\t\t\tconst links = Array.isArray(this.content.footerLinks) ? this.content.footerLinks : []\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\tconst out = []\n\t\t\tlinks.forEach((link, index) => {\n\t\t\t\tif (!link || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (typeof link.group === 'string' && link.group !== '' && !(this.footerGroups || []).includes(link.group)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst location = typeof link.route === 'string' && link.route !== ''\n\t\t\t\t\t? { name: link.route, query: resolveFilterTokens(link.query || {}, this.tokenCtx) }\n\t\t\t\t\t: null\n\t\t\t\tconst href = location ? resolveHref(location, this.$router) : (typeof link.href === 'string' ? link.href : '')\n\t\t\t\tif (!location && href === '') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: `link-${index}`,\n\t\t\t\t\tlabel: fn(link.label),\n\t\t\t\t\tnote: typeof link.note === 'string' && link.note !== '' ? fn(link.note) : '',\n\t\t\t\t\thref,\n\t\t\t\t\tlocation,\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * `content.footnote` run through the host translate function, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedFootnote() {\n\t\t\tconst note = typeof this.content.footnote === 'string' ? this.content.footnote : ''\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn note ? fn(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * The rows actually rendered: capped to what fits the host grid cell\n\t\t * (ADR-062 — content adapts to the cell, never a nested scrollbar).\n\t\t * Unconstrained on surfaces without a fixed-height cell (dashboards\n\t\t * measure null and render every fetched row, as before).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tvisibleRows() {\n\t\t\tconst rows = this.readOnlyRows.length > 0 ? [...this.rows, ...this.readOnlyRows] : this.rows\n\t\t\treturn this.fitRows ? rows.slice(0, this.fitRows) : rows\n\t\t},\n\n\t\t/**\n\t\t * Rows per page. This is `content.limit` (the fetch cap) — a FIXED\n\t\t * number, deliberately not the measured `fitRows`: a page size derived\n\t\t * from a post-render measurement would change the pagination under the\n\t\t * reader every time the tile resized, and would need a refetch to\n\t\t * settle.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\tpageSize() {\n\t\t\tif (this.presetRows !== null) {\n\t\t\t\treturn Math.max(this.presetRows.length, 1)\n\t\t\t}\n\t\t\treturn Number((this.content || {}).limit) || 25\n\t\t},\n\n\t\t/**\n\t\t * Total pages for the resolved filter, from the SERVER's total.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\ttotalPages() {\n\t\t\treturn Math.max(Math.ceil((this.total || 0) / this.pageSize), 1)\n\t\t},\n\n\t\t/**\n\t\t * Whether the compact pager renders.\n\t\t *\n\t\t * Two conditions, and the second is the load-bearing one. There must\n\t\t * be more than one page (otherwise there is nothing to page), AND the\n\t\t * cell must be able to show a whole page — because `visibleRows` clips\n\t\t * the page to what fits, and a pager over clipped rows would claim to\n\t\t * be showing \"1–25 of 137\" while five rows are on screen. On a cell\n\t\t * that short the existing fit-to-cell \"View all\" is the honest answer.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowPager() {\n\t\t\tif (this.totalPages <= 1) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.fitRows === null || this.fitRows >= this.rows.length\n\t\t},\n\n\t\t/** Whether the designed empty state is what the widget is showing. */\n\t\tshowingEmptyState() {\n\t\t\treturn !this.waitingForContext && !this.error && !this.loading && this.rows.length === 0 && this.readOnlyRows.length === 0\n\t\t},\n\n\t\t/**\n\t\t * How many matching objects are NOT rendered (server total minus the\n\t\t * visible slice). Drives the \"View all (N)\" footer.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\thiddenCount() {\n\t\t\treturn Math.max((this.total || this.rows.length) - this.visibleRows.length, 0)\n\t\t},\n\n\t\t/** Pre-translated \"View all (N)\" footer label. */\n\t\tviewAllLabel() {\n\t\t\treturn t('nextcloud-vue', 'View all ({total})', { total: this.total || this.rows.length })\n\t\t},\n\n\t\t/**\n\t\t * The \"View all (N)\" router location: `viewAllRoute` with its\n\t\t * token-resolved `viewAllQuery`, or null without a route.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tviewAllLocation() {\n\t\t\tconst route = this.content.viewAllRoute\n\t\t\tif (!route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, query: resolveFilterTokens(this.content.viewAllQuery || {}, this.tokenCtx) }\n\t\t},\n\n\t\t/**\n\t\t * The href \"View all (N)\" links to, or '' when there is no router to\n\t\t * resolve it (the footer then renders as a button).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewAllHref() {\n\t\t\treturn resolveHref(this.viewAllLocation, this.$router)\n\t\t},\n\n\t\t/** Pre-translated \"+N more\" footer label (no viewAllRoute configured). */\n\t\tmoreLabel() {\n\t\t\treturn t('nextcloud-vue', '+{count} more', { count: this.hiddenCount })\n\t\t},\n\n\t\t/** Whether the create affordance renders (on by default; `content.allowCreate: false` opts out). */\n\t\tallowCreate() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (this.createState.action) {\n\t\t\t\treturn c.allowCreate !== false && this.createState.visible\n\t\t\t}\n\t\t\treturn c.allowCreate !== false && Boolean(c.register) && Boolean(c.schema)\n\t\t},\n\n\t\t/**\n\t\t * The declared `content.createAction` against the page record: the\n\t\t * action, whether it shows, and the untranslated note when it does not.\n\t\t *\n\t\t * @return {{action: (object|null), visible: boolean, note: string}}\n\t\t */\n\t\tcreateState() {\n\t\t\treturn createActionState(this.content, this.objectCtx ? this.objectCtx.object : null)\n\t\t},\n\n\t\t/**\n\t\t * Why the create action is withheld, through the host translate, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tcreateHiddenNote() {\n\t\t\tconst note = this.createState.note\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn note ? fn(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * `content.summary` filled in: `{total}` is the number of loaded rows\n\t\t * and every `counts` key the number of loaded rows its condition holds\n\t\t * for. The text goes through the host translate first, so a catalogue\n\t\t * entry keeps its placeholders.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * `content.headerSubtitle` filled in (`{total}`, `{count:field=value}`),\n\t\t * or '' without one.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderSubtitleText() {\n\t\t\treturn this.countLine(this.content.headerSubtitle)\n\t\t},\n\n\t\t/**\n\t\t * `content.headerNote` filled in like `headerSubtitle`, for beside the\n\t\t * card title (\"4 vastgelegd\"), or '' without one.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/object-list-header-note/specs/object-list-header-note/spec.md#requirement-an-object-list-can-say-its-counts-beside-its-card-title\n\t\t */\n\t\theaderNoteText() {\n\t\t\treturn this.countLine(this.content.headerNote)\n\t\t},\n\n\t\tresolvedSummary() {\n\t\t\tconst summary = this.content.summary\n\t\t\tif (!summary || typeof summary.text !== 'string' || summary.text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\tconst values = { total: this.rows.length }\n\t\t\tconst counts = summary.counts && typeof summary.counts === 'object' ? summary.counts : {}\n\t\t\tfor (const [key, cond] of Object.entries(counts)) {\n\t\t\t\tvalues[key] = this.rows.filter((row) => evaluateVisibleWhenLocal(cond, row)).length\n\t\t\t}\n\t\t\treturn fn(summary.text).replace(/\\{(\\w+)\\}/g, (match, key) => (key in values ? String(values[key]) : match))\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for the create dialog (`content.formSize`).\n\t\t *\n\t\t * The widget fetches the WHOLE schema to build this form, so a list\n\t\t * scoped to a handful of columns still opened a create dialog asking\n\t\t * every property the schema declares — at `normal` width, in one\n\t\t * column. A case schema with thirty visible properties is unusable\n\t\t * that way, and every consumer that hit it worked around it by\n\t\t * turning the create button off.\n\t\t */\n\t\tformSize() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn c.formSize || 'normal'\n\t\t},\n\n\t\t/** How many columns the create dialog flows its fields into (`content.formColumns`). */\n\t\tformColumns() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn c.formColumns === 2 ? 2 : 1\n\t\t},\n\n\t\t/** Whitelist of fields the create dialog asks for (`content.formIncludeFields`). Null means all. */\n\t\tformIncludeFields() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn Array.isArray(c.formIncludeFields) ? c.formIncludeFields : null\n\t\t},\n\n\t\t/** Fields the create dialog leaves out (`content.formExcludeFields`). */\n\t\tformExcludeFields() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn Array.isArray(c.formExcludeFields) ? c.formExcludeFields : []\n\t\t},\n\n\t\t/**\n\t\t * Create-form seed values: the list's scalar filter values (the parent\n\t\t * this list is scoped to, e.g. `{ lead: '<uuid>' }`), limited to keys\n\t\t * the schema declares, so the new row shows its parent already chosen.\n\t\t *\n\t\t * `content.createDefaults` merges over it for values a filter cannot express.\n\t\t *\n\t\t * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md\n\t\t * @spec openspec/changes/object-list-create-with-initial-data/tasks.md#task-1\n\t\t * @return {object}\n\t\t */\n\t\tcreateInitialData() {\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst out = { ...this.filterSeed }\n\t\t\t// A new row of a reorderable list goes last.\n\t\t\tif (this.reorderField !== '' && this.reorderField in props) {\n\t\t\t\tout[this.reorderField] = Math.max(this.total, this.rows.length) + 1\n\t\t\t}\n\t\t\tconst declared = this.content && this.content.createDefaults\n\t\t\t// Tokens resolve as the filter's do: `@objectId` is the page's object.\n\t\t\tconst defaults = declared && typeof declared === 'object' && !Array.isArray(declared) ? resolveFilterTokens(declared, this.tokenCtx) : null\n\t\t\tif (defaults) {\n\t\t\t\tfor (const [key, value] of Object.entries(defaults)) {\n\t\t\t\t\tif (key in props && value !== undefined) {\n\t\t\t\t\t\tout[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The scalar, schema-declared values of the resolved filter: the parent\n\t\t * this list is scoped to. Operator keys (`deadline[lt]`), keys the schema\n\t\t * does not declare and unresolved `@`-tokens are dropped.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tfilterSeed() {\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter || {})) {\n\t\t\t\tif (!(key in props) || value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = value\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The parent keys seeded from the filter are locked (read-only with\n\t\t * their label): a row added to this list belongs to the record the list\n\t\t * is scoped to. `content.lockFilterFields: false` leaves them editable.\n\t\t * `createDefaults` values are never locked.\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcreateLockedFields() {\n\t\t\tif (this.content && this.content.lockFilterFields === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn Object.keys(this.filterSeed)\n\t\t},\n\n\t\t/** Per-field overrides for the create dialog (`content.formFieldOverrides`). */\n\t\tformFieldOverrides() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn (c.formFieldOverrides && typeof c.formFieldOverrides === 'object') ? c.formFieldOverrides : {}\n\t\t},\n\n\t\t/**\n\t\t * The Add label: `content.addLabel` through the app's translate\n\t\t * function, else the library's \"Add\".\n\t\t *\n\t\t * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-list-add-label/spec.md\n\t\t * @return {string}\n\t\t */\n\t\taddLabel() {\n\t\t\t// `content.addLabel` is manifest copy in the APP's catalogue, so it\n\t\t\t// goes through the host translate function like `emptyText` and\n\t\t\t// `prompt`. It used to print raw, so a Dutch user read the English\n\t\t\t// manifest string. A label an app already translated has no key\n\t\t\t// of its own in the catalogue and comes back unchanged.\n\t\t\tconst action = this.createState.action\n\t\t\tif (action && typeof action.label === 'string' && action.label !== '') {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(action.label)\n\t\t\t}\n\t\t\tif (this.content.addLabel) {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(this.content.addLabel)\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Add')\n\t\t},\n\n\t\t/** Stable signature of the query so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify({\n\t\t\t\tregister: c.register || '',\n\t\t\t\tschema: c.schema || '',\n\t\t\t\t// The RESOLVED filter (workspace + object tokens applied) so the\n\t\t\t\t// watcher refetches when page-level state a token reads changes.\n\t\t\t\tfilter: this.resolvedFilter,\n\t\t\t\t// `localSort`, not `c.sort` — an interactive header click\n\t\t\t\t// (`content.sortable`) overrides the manifest default without\n\t\t\t\t// touching the prop, and the fetch must follow that override.\n\t\t\t\tsort: this.localSort || {},\n\t\t\t\theaderFilters: this.headerFilters,\n\t\t\t\tlimit: c.limit || 25,\n\t\t\t\tobjectId: this.objectCtx ? this.objectCtx.objectId : null,\n\t\t\t})\n\t\t},\n\t},\n\n\twatch: {\n\t\tpresetRows(next) {\n\t\t\tif (next !== null) {\n\t\t\t\tthis.rows = [...next]\n\t\t\t\tthis.total = this.rows.length\n\t\t\t}\n\t\t},\n\n\t\tfacetedRows: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.publishSelectionRows()\n\t\t\t},\n\t\t},\n\n\t\t/** The counts under the card title follow the rows (`content.headerSubtitle`). */\n\t\theaderSubtitleText() {\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\t/** The counts beside the card title follow the rows (`content.headerNote`). */\n\t\theaderNoteText() {\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\tsourceKey() {\n\t\t\t// The query changed, so page 3 of the OLD result set means nothing\n\t\t\t// against the new one — and an out-of-range `_page` returns an empty\n\t\t\t// list, which would read as \"no matches\" rather than \"wrong page\".\n\t\t\tthis.page = 1\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t// The create-dialog schema is cached per instance, and an instance can\n\t\t// be retargeted (CnRelatedCollections keys its children by index, so a\n\t\t// reordered entry reuses the component). A schema resolved for the OLD\n\t\t// register is exactly the wrong-app schema the register scoping above\n\t\t// exists to prevent — drop it so the next open resolves it again.\n\t\t//\n\t\t// The headings read the same cache, and unlike the dialog they have no\n\t\t// later moment that reloads it: without the second call a retargeted\n\t\t// widget goes back to printing `customerName` at the user until\n\t\t// somebody happens to open Add.\n\t\t'content.register': function() {\n\t\t\tthis.forgetCreateSchema()\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t},\n\n\t\t'content.schema': function() {\n\t\t\tthis.forgetCreateSchema()\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.presetRows !== null) {\n\t\t\t// A nested list: its owner reads the rows and keeps them current.\n\t\t\tthis.rows = [...this.presetRows]\n\t\t\tthis.total = this.rows.length\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t\treturn\n\t\t}\n\t\tthis.fetchRows()\n\t\tthis.loadHeadingsIfNeeded()\n\t\tthis.offerExport()\n\t\tthis.loadFooterGroups()\n\t\t// A table wider than the widget fits its columns to the width instead of\n\t\t// scrolling sideways (no inner scrollbars).\n\t\tif (typeof ResizeObserver !== 'undefined') {\n\t\t\tthis._widthObserver = new ResizeObserver(() => this.measureWidthFit())\n\t\t\tthis._widthObserver.observe(this.$el)\n\t\t}\n\t\t// Re-read on a page-level refresh. This list fetches its own rows from\n\t\t// OpenRegister and subscribed to nothing, so after a write elsewhere on\n\t\t// the page it went on rendering the result set it fetched on mount —\n\t\t// values the backend had already changed. The endpoint-bound widgets\n\t\t// have answered this channel since Wave 2 (useEndpointSource); the\n\t\t// store-backed ones did not.\n\t\t//\n\t\t// Guarded on `loading`: the channel is a broadcast, and a page whose\n\t\t// action fires it more than once (or a dialog that emits alongside the\n\t\t// page's own Refresh) must not turn one write into a queue of\n\t\t// overlapping reads for one list.\n\t\tthis._onPageRefresh = (payload) => {\n\t\t\tif (this.loading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst done = this.fetchRows()\n\t\t\tpayload?.waitUntil?.(done)\n\t\t}\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t// Observe the host grid cell so the visible row count re-fits on\n\t\t// resize/layout changes. Only detail-grid cells constrain height;\n\t\t// on dashboards the closest() lookup misses and fitRows stays null.\n\t\tconst cell = this.$el.closest && this.$el.closest('.grid-stack-item-content')\n\t\tif (cell && typeof ResizeObserver !== 'undefined') {\n\t\t\tthis._fitObserver = new ResizeObserver(() => this.measureFit())\n\t\t\tthis._fitObserver.observe(cell)\n\t\t}\n\t\tthis.$nextTick(() => this.measureFit())\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this._onPageRefresh) {\n\t\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t\tthis._onPageRefresh = null\n\t\t}\n\t\tif (this._fitObserver) {\n\t\t\tthis._fitObserver.disconnect()\n\t\t}\n\t\tif (this._widthObserver) {\n\t\t\tthis._widthObserver.disconnect()\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Offer `content.export` as a button in the card's header: the list as\n\t\t * it is filtered here, through OpenRegister's export (which applies its\n\t\t * own read rights to every row). `group` keeps the button to the members\n\t\t * of one Nextcloud group, a role such as AI oversight; it hides the\n\t\t * button, and the export endpoint still decides what anyone may read.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/widget-subtitle-and-list-export/specs/object-list-widget/spec.md#requirement-a-list-can-offer-its-rows-as-an-export\n\t\t */\n\t\t/**\n\t\t * The content of one row's child list: the declared `children` with\n\t\t * its `@row` tokens resolved against the row and the parent's\n\t\t * register when it names none.\n\t\t *\n\t\t * @param {object} row The parent row.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildContentFor(row) {\n\t\t\tconst children = this.childrenConfig || {}\n\t\t\treturn {\n\t\t\t\t...children,\n\t\t\t\tregister: children.register || (this.content || {}).register,\n\t\t\t\tfilter: resolveRowFilter(children.filter, row),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The child rows of one row, as last read.\n\t\t *\n\t\t * @param {object} row The parent row.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tchildRowsFor(row) {\n\t\t\tconst link = this.childLink\n\t\t\tif (!link) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst value = resolveRowFilter({ v: `@row.${link.field}` }, row).v\n\t\t\treturn this.childRows[String(value)] || []\n\t\t},\n\n\t\t/**\n\t\t * Read the children of every loaded row in one query (`<link>[]=` the\n\t\t * row values, page by page) and file them under their row.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\t/**\n\t\t * Read the rows `content.readOnlyRows` adds after the list's own:\n\t\t * `{ url, responsePath?, where? }`, an app endpoint (tokens such as\n\t\t * `@objectId` in the url), narrowed by `where` (`{ field: value }` or\n\t\t * `{ field: [values] }`). Each row is tagged `_readOnly`. A read that\n\t\t * fails adds nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-read-only-rows/specs/object-list-widget/spec.md#requirement-a-list-can-add-read-only-rows-from-an-endpoint\n\t\t */\n\t\tasync fetchReadOnlyRows() {\n\t\t\tconst config = this.content && this.content.readOnlyRows\n\t\t\tif (!config || typeof config !== 'object' || typeof config.url !== 'string' || config.url === '') {\n\t\t\t\tthis.readOnlyRows = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet data = null\n\t\t\ttry {\n\t\t\t\tdata = await fetchEndpointSource({ url: config.url, responsePath: config.responsePath || '' }, this.tokenCtx)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load the read-only rows:', e)\n\t\t\t}\n\t\t\tconst where = config.where && typeof config.where === 'object' ? Object.entries(config.where) : []\n\t\t\tconst matches = (row) => where.every(([field, want]) => {\n\t\t\t\tconst value = readPath(row, field)\n\t\t\t\treturn Array.isArray(want) ? want.map(String).includes(String(value)) : String(value) === String(want)\n\t\t\t})\n\t\t\tthis.readOnlyRows = (Array.isArray(data) ? data : [])\n\t\t\t\t.filter((row) => row && typeof row === 'object' && matches(row))\n\t\t\t\t.map((row) => ({ ...row, _readOnly: true }))\n\t\t},\n\n\t\tasync fetchChildren() {\n\t\t\tconst children = this.childrenConfig\n\t\t\tconst link = this.childLink\n\t\t\tif (!children || !link) {\n\t\t\t\tthis.childRows = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst values = [...new Set(this.rows\n\t\t\t\t.map((row) => resolveRowFilter({ v: `@row.${link.field}` }, row).v)\n\t\t\t\t.filter((v) => v !== undefined && v !== null && v !== '')\n\t\t\t\t.map(String))]\n\t\t\tif (values.length === 0) {\n\t\t\t\tthis.childRows = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst register = children.register || (this.content || {}).register\n\t\t\tconst params = { [link.key]: values }\n\t\t\tfor (const [k, v] of Object.entries(resolveFilterTokens(children.filter || {}, this.tokenCtx))) {\n\t\t\t\tif (k !== link.key && rowTokenField(v) === '' && v !== '' && v !== null && v !== undefined && typeof v !== 'object') {\n\t\t\t\t\tparams[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst order = children.reorder && children.reorder.field\n\t\t\tif (typeof order === 'string' && order !== '') {\n\t\t\t\tparams[`_order[${order}]`] = 'asc'\n\t\t\t} else if (children.sort && children.sort.field) {\n\t\t\t\tparams[`_order[${children.sort.field}]`] = children.sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tif (Array.isArray(children.extend) && children.extend.length > 0) {\n\t\t\t\tparams._extend = children.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t}\n\t\t\tconst limit = Number(children.limit) || 500\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register, schema: children.schema })\n\t\t\t\tconst grouped = {}\n\t\t\t\tfor (let page = 1; ; page++) {\n\t\t\t\t\tconst res = await axios.get(url, { params: { ...params, _limit: limit, _page: page } })\n\t\t\t\t\tconst results = (res && res.data && res.data.results) || []\n\t\t\t\t\tfor (const child of results) {\n\t\t\t\t\t\tconst key = String(objectFieldValue(child, link.key))\n\t\t\t\t\t\tif (values.includes(key)) {\n\t\t\t\t\t\t\t(grouped[key] = grouped[key] || []).push(child)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst total = res && res.data && typeof res.data.total === 'number' ? res.data.total : results.length\n\t\t\t\t\tif (results.length < limit || page * limit >= total) {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.childRows = grouped\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load child rows:', e)\n\t\t\t\tthis.childRows = {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The export URL for this list as it is filtered here.\n\t\t *\n\t\t * @param {'csv'|'excel'} format The export format.\n\t\t * @return {string} The URL.\n\t\t */\n\t\texportUrl(format) {\n\t\t\treturn buildExportUrl(this.content.register, this.content.schema, this.resolvedFilter, format)\n\t\t},\n\n\t\t/**\n\t\t * The download URL of an `export` that names an app endpoint: its\n\t\t * `url` and `params` resolved against the record, '' while a required\n\t\t * token is unresolved.\n\t\t *\n\t\t * @param {{url: string, params?: object}} config The export config.\n\t\t * @return {string} The URL under the Nextcloud base, or ''.\n\t\t * @spec openspec/changes/object-list-endpoint-rows/specs/object-list-widget/spec.md#requirement-a-list-can-read-its-rows-from-an-app-endpoint\n\t\t */\n\t\tendpointExportUrl(config) {\n\t\t\tconst request = resolveEndpointRequest({ url: config.url, params: config.params || {} }, this.tokenCtx)\n\t\t\tif (request.blocked) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst base = /^https?:\\/\\//i.test(request.url) ? request.url : generateUrl(request.url)\n\t\t\tconst query = new URLSearchParams(Object.entries(request.params).map(([key, value]) => [key, String(value)])).toString()\n\t\t\treturn query === '' ? base : `${base}${base.includes('?') ? '&' : '?'}${query}`\n\t\t},\n\n\t\t/**\n\t\t * Read the rows from `content.endpointSource`: rows at its\n\t\t * `responsePath` (default `results`), the count at `total`.\n\t\t *\n\t\t * @param {{url: string, params?: object, responsePath?: string}} source The endpoint.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-endpoint-rows/specs/object-list-widget/spec.md#requirement-a-list-can-read-its-rows-from-an-app-endpoint\n\t\t */\n\t\tasync fetchEndpointRows(source) {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst body = await fetchEndpointSource({ url: source.url, method: source.method, params: source.params || {} }, this.tokenCtx)\n\t\t\t\tconst rows = getByPath(body, typeof source.responsePath === 'string' && source.responsePath !== '' ? source.responsePath : 'results')\n\t\t\t\tthis.rows = Array.isArray(rows) ? rows : []\n\t\t\t\tthis.total = body && typeof body.total === 'number' ? body.total : this.rows.length\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tthis.measureFit()\n\t\t\t\t\tthis.measureWidthFit()\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load the endpoint rows:', e)\n\t\t\t\tthis.error = endpointErrorMessage(e)\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync offerExport() {\n\t\t\tconst config = this.content && this.content.export\n\t\t\tconst endpointUrl = config && typeof config.url === 'string' && config.url !== '' ? this.endpointExportUrl(config) : ''\n\t\t\tif (!config || typeof config !== 'object' || (!endpointUrl && (!this.content.register || !this.content.schema))) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof config.group === 'string' && config.group !== '') {\n\t\t\t\tconst groups = await getCurrentUserGroups().catch(() => [])\n\t\t\t\tif (!groups.includes(config.group)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst format = config.format === 'excel' ? 'excel' : 'csv'\n\t\t\tconst label = typeof config.label === 'string' && config.label !== '' ? (typeof this.cnTranslate === 'function' ? this.cnTranslate(config.label) : config.label) : t('nextcloud-vue', 'Download')\n\t\t\t/**\n\t\t\t * @event header The card header's Download action for the list as\n\t\t\t * filtered here (`content.export`), handed to the host card.\n\t\t\t * @type {{action: {label: string, icon: string, run: Function}}}\n\t\t\t */\n\t\t\tthis.headerAction = {\n\t\t\t\tlabel,\n\t\t\t\ticon: 'Download',\n\t\t\t\trun: () => window.location.assign(endpointUrl || this.exportUrl(format)),\n\t\t\t}\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\t/**\n\t\t * A counts template (`{total}`, `{count:<field>=<value>}`) translated and\n\t\t * filled from the rows, or '' without one.\n\t\t *\n\t\t * @param {unknown} text The template.\n\t\t * @return {string}\n\t\t */\n\t\tcountLine(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn fillCountLabel(fn(text), this.total || this.rows.length, this.rows)\n\t\t},\n\n\t\t/**\n\t\t * Tell the card around this list what its header shows: the Download\n\t\t * action (`content.export`), the counts line under the title\n\t\t * (`content.headerSubtitle`) and the note beside it\n\t\t * (`content.headerNote`). Nothing when the list has none of them.\n\t\t *\n\t\t * @spec openspec/changes/widget-header-subtitle-counts/specs/widget-header-subtitle-counts/spec.md#requirement-a-list-can-say-its-counts-under-its-card-title\n\t\t */\n\t\temitHeader() {\n\t\t\tconst subtitle = this.headerSubtitleText\n\t\t\tconst note = this.headerNoteText\n\t\t\t// A line shown before is cleared with an empty one, so the card does\n\t\t\t// not keep a stale count; a list that never had one stays silent.\n\t\t\tif (!this.headerAction && subtitle === '' && note === '' && !this.headerSubtitleShown) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.headerSubtitleShown = subtitle !== '' || note !== ''\n\t\t\t/**\n\t\t\t * @event header The card header's parts: the Download action for the\n\t\t\t * list as filtered here (`content.export`) and the counts line under\n\t\t\t * the title (`content.headerSubtitle`).\n\t\t\t * @type {{action?: {label: string, icon: string, run: Function}, subtitle: string}}\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\t...(this.headerAction ? { action: this.headerAction } : {}),\n\t\t\t\tsubtitle,\n\t\t\t\tnote,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Move a row one place up or down (the row actions).\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {number} delta -1 for up, 1 for down.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\tmoveRowBy(row, delta) {\n\t\t\tconst from = this.rows.findIndex((r) => rowIdOf(r) === rowIdOf(row))\n\t\t\treturn this.reorderTo(from, from + delta)\n\t\t},\n\n\t\t/**\n\t\t * A row dragged to another place in the table.\n\t\t *\n\t\t * @param {{from: number, to: number}} move The old and new index.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tonRowMove({ from, to } = {}) {\n\t\t\treturn this.reorderTo(from, to)\n\t\t},\n\n\t\t/**\n\t\t * Put the row at `from` at `to`, renumber the rows from this page's\n\t\t * first place, and write the order field on the rows whose number\n\t\t * changed, as a PATCH of that field only. A refusal is read out and the\n\t\t * list is read again.\n\t\t *\n\t\t * @param {number} from The index moved from.\n\t\t * @param {number} to The index moved to.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\tasync reorderTo(from, to) {\n\t\t\tconst field = this.reorderField\n\t\t\tif (field === '' || from < 0 || to < 0 || from >= this.rows.length || to >= this.rows.length || from === to) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst moved = this.rows[from]\n\t\t\tconst { rows, changes } = renumberRows(moveIndex(this.rows, from, to), field, (this.page - 1) * this.pageSize + 1)\n\t\t\tthis.rows = rows\n\t\t\tconst c = this.content || {}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\tfor (const change of changes) {\n\t\t\t\t\tawait axios.patch(\n\t\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id: change.id }),\n\t\t\t\t\t\t{ [field]: change.value },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthis.reorderStatus = t('nextcloud-vue', '{name} moved to place {place} of {total}', {\n\t\t\t\t\tname: objectDisplayName(moved || {}) || t('nextcloud-vue', 'Row'),\n\t\t\t\t\tplace: (this.page - 1) * this.pageSize + to + 1,\n\t\t\t\t\ttotal: Math.max(this.total, this.rows.length),\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tthis.reorderStatus = (data && (data.message || data.error)) || t('nextcloud-vue', 'The new order could not be saved.')\n\t\t\t\tawait this.fetchRows()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the object rows from OpenRegister. Lazily imports the\n\t\t * axios/router helpers (the library never hard-depends on them at module\n\t\t * load — same pattern as CnStatWidget / CnFilesWidget).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tif (this.presetRows !== null) {\n\t\t\t\t/**\n\t\t\t\t * @event rows-stale The rows handed in through `presetRows` are\n\t\t\t\t * out of date (after a write or a refused reorder); the owner\n\t\t\t\t * reads them again.\n\t\t\t\t */\n\t\t\t\tthis.$emit('rows-stale')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst c = this.content || {}\n\t\t\tif ((!c.register || !c.schema) && c.endpointSource && typeof c.endpointSource.url === 'string' && c.endpointSource.url !== '') {\n\t\t\t\tawait this.fetchEndpointRows(c.endpointSource)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!c.register || !c.schema) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A context token this list depends on (e.g. @workspace.selectedClient)\n\t\t\t// isn't set yet — don't fetch the whole register; the prompt shows.\n\t\t\tif (this.waitingForContext) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}',\n\t\t\t\t\t{ register: c.register, schema: c.schema },\n\t\t\t\t)\n\t\t\t\t// `limit` is a FETCH CAP (ADR-062), not a render promise — the\n\t\t\t\t// visible count fits the cell; fetch enough to fill big cells.\n\t\t\t\t// It doubles as the PAGE SIZE: one fetch is exactly one page, so\n\t\t\t\t// the pager's \"1–25 of 137\" is the server's arithmetic and not a\n\t\t\t\t// client-side count of an already-capped window.\n\t\t\t\tconst params = { _limit: this.pageSize, _page: this.page }\n\t\t\t\tif (this.reorderField !== '') {\n\t\t\t\t\t// A reorderable list is always shown in its stored order.\n\t\t\t\t\tparams[`_order[${this.reorderField}]`] = 'asc'\n\t\t\t\t} else if (this.localSort && this.localSort.field) {\n\t\t\t\t\tparams[`_order[${this.localSort.field}]`] = (this.localSort.dir === 'desc' ? 'desc' : 'asc')\n\t\t\t\t}\n\t\t\t\t// `content.extend` → OpenRegister's repeated `_extend[]`. axios\n\t\t\t\t// serializes an array value as `_extend[]=a&_extend[]=b`, which\n\t\t\t\t// is the wire form OR reads. This is what turns a dotted column\n\t\t\t\t// key into a real value: the referenced object is inlined on the\n\t\t\t\t// row, so `informatieobject.title` resolves instead of reading a\n\t\t\t\t// uuid string as an object path.\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// The OpenRegister OBJECT-SEARCH endpoint filters on DIRECT field\n\t\t\t\t// params (`status=open`, `value[gt]=30000`) — unlike the\n\t\t\t\t// aggregation endpoints which use the nested `filter[...]` shape.\n\t\t\t\tconst filter = this.resolvedFilter\n\t\t\t\tif (filter && typeof filter === 'object') {\n\t\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\t\tif (Array.isArray(v)) {\n\t\t\t\t\t\t\t// A list matches any of its values: `field[]=a&field[]=b`.\n\t\t\t\t\t\t\tif (v.length > 0) {\n\t\t\t\t\t\t\t\tparams[k] = v\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (v && typeof v === 'object') {\n\t\t\t\t\t\t\tfor (const [op, ov] of Object.entries(v)) {\n\t\t\t\t\t\t\t\tparams[`${k}[${op}]`] = ov\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\t\tparams[k] = v\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tObject.assign(params, headerFilterParams(this.headerFilters))\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tthis.rows = (res && res.data && res.data.results) || []\n\t\t\t\tthis.total = (res && res.data && typeof res.data.total === 'number') ? res.data.total : this.rows.length\n\t\t\t\tawait this.fetchChildren()\n\t\t\t\tawait this.fetchReadOnlyRows()\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tthis.measureFit()\n\t\t\t\t\tthis.measureWidthFit()\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\t// Keep the raw message OUT of the template — log it for\n\t\t\t\t// debugging, surface only the quiet `loadErrorLabel` line.\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load objects:', e)\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Squeeze the table to the widget's width while it scrolls sideways: its\n\t\t * `.cn-data-table__scroll` wrapper is wider than it is visible. First the\n\t\t * fit (stage 1), then, if that is still too wide, a fixed layout\n\t\t * (stage 2). It never goes back (a squeezed table does not overflow, so\n\t\t * there is nothing to measure to relax it). No-op when `content.fitWidth`\n\t\t * is set.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureWidthFit() {\n\t\t\tif (this.fitStage >= 2 || typeof this.content?.fitWidth === 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst scroller = this.$el && this.$el.querySelector ? this.$el.querySelector('.cn-data-table__scroll') : null\n\t\t\tif (scroller && scroller.scrollWidth > scroller.clientWidth + 1) {\n\t\t\t\tthis.fitStage += 1\n\t\t\t\tthis.$nextTick(() => this.measureWidthFit())\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fit the visible row count to the host grid cell (ADR-062 — the cell\n\t\t * is the budget). Measures the cell's remaining height below the table\n\t\t * top, reserves room for the \"View all\" footer, and derives the row\n\t\t * budget from the first rendered row's height. No-ops (fitRows null =\n\t\t * render all fetched rows) outside a fixed-height cell and inside a\n\t\t * tab panel, unless the content sets `fit: true`.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureFit() {\n\t\t\t// `content.fit: false` opts out of the cell budget: render every row\n\t\t\t// fetched and let the container scroll. The budget below assumes the\n\t\t\t// list OWNS its cell, measuring from where the table starts to the\n\t\t\t// cell's bottom. That is wrong for a list stacked below other content\n\t\t\t// in the same cell (a section of a tabbed panel, say): it starts near\n\t\t\t// the bottom, the budget floors to one row, and the rest are clipped.\n\t\t\t// Only an explicit `false` opts out, so a manifest that never sets the\n\t\t\t// key keeps the ADR-062 behaviour unchanged.\n\t\t\tif ((this.content && this.content.fit === false) || this.presetRows !== null) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cell = this.$el && this.$el.closest && this.$el.closest('.grid-stack-item-content')\n\t\t\tif (!cell) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A list inside a tab panel does not own the cell, as on a\n\t\t\t// dashboard: it renders every fetched row unless the content asks\n\t\t\t// for the fit with `fit: true`.\n\t\t\tconst panel = this.$el.closest('[role=\"tabpanel\"]')\n\t\t\tif (panel && cell.contains(panel) && !(this.content && this.content.fit === true)) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst table = this.$el.querySelector('.cn-object-list-widget__table table')\n\t\t\tif (!table) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cellRect = cell.getBoundingClientRect()\n\t\t\tconst tableRect = table.getBoundingClientRect()\n\t\t\tconst firstRow = table.querySelector('tbody tr')\n\t\t\tconst rowH = (firstRow && firstRow.getBoundingClientRect().height) || 44\n\t\t\tconst head = table.querySelector('thead')\n\t\t\tconst headH = (head && head.getBoundingClientRect().height) || 40\n\t\t\t// Room for the \"View all\" footer AND the Add button (ADR-062).\n\t\t\tconst footerReserve = 68\n\t\t\tconst available = cellRect.bottom - tableRect.top - footerReserve\n\t\t\tconst fit = Math.floor((available - headH) / rowH)\n\t\t\tthis.fitRows = Math.max(fit, 1)\n\t\t},\n\n\t\t/**\n\t\t * Drop the cached create-dialog schema (and close the dialog if it is\n\t\t * open) when the list points at another register or schema.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tforgetCreateSchema() {\n\t\t\tthis.createSchema = null\n\t\t\tthis.schemaRequest = null\n\t\t\tthis.showCreate = false\n\t\t\tthis.editingRow = null\n\t\t},\n\n\t\t/**\n\t\t * Open the create dialog for the list's target schema. PUBLIC — the\n\t\t * host card's Actions-menu \"Add\" entry calls this through a ref, the\n\t\t * widget's own footer button calls it directly (ADR-062: both\n\t\t * affordances, one dialog).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\t/**\n\t\t * Edit a row in the list's own form (`content.editable`): the form the\n\t\t * Add button opens, filled with the row.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync openEdit(row) {\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t\tthis.showCreate = false\n\t\t\t\tthis.editingRow = row ? { ...row } : null\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Delete a row of the list's own schema (`content.deletable`). The\n\t\t * toast offers Undo; a refusal toasts the server's message.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync deleteRow(row) {\n\t\t\tconst c = this.content || {}\n\t\t\tconst id = rowIdOf(row)\n\t\t\tif (id === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst pending = (async () => {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\ttry {\n\t\t\t\t\tawait axios.delete(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\t\tthrow new Error((typeof refusal === 'string' && refusal) || (e && e.message) || t('nextcloud-vue', 'The delete failed'), { cause: e })\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})()\n\t\t\tawait this.reportDelete(row, pending)\n\t\t\tawait pending.catch(() => {})\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t/** Close the form, whether it created or edited. */\n\t\tcloseForm() {\n\t\t\tthis.showCreate = false\n\t\t\tthis.editingRow = null\n\t\t},\n\n\t\t/**\n\t\t * Save an edited row: a PATCH of the keys the form changed, so every\n\t\t * key it does not show keeps its stored value; nothing when nothing\n\t\t * changed. A refusal shows the server's message in the dialog.\n\t\t *\n\t\t * @param {object} formData The edited values.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync onEditConfirm(formData) {\n\t\t\tconst c = this.content || {}\n\t\t\tconst row = this.editingRow\n\t\t\tconst id = rowIdOf(row)\n\t\t\tconst changes = changedKeys(row, formData)\n\t\t\ttry {\n\t\t\t\tif (id !== '' && Object.keys(changes).length > 0) {\n\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\t\tawait axios.patch(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }), changes)\n\t\t\t\t}\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event updated Emitted after a row was edited, with the keys that changed.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('updated', { id, changes })\n\t\t\t\tthis.fetchRows()\n\t\t\t} catch (e) {\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ error: (typeof refusal === 'string' && refusal) || (e && e.message) || 'error' })\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tasync openCreate() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst { action, visible } = this.createState\n\t\t\tif (action) {\n\t\t\t\tif (visible) {\n\t\t\t\t\tthis.dispatch(action)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!c.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t\tthis.showCreate = true\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch this list's schema once, for whoever needs it.\n\t\t *\n\t\t * Two readers share it: the create dialog builds its form from the\n\t\t * properties, and `resolvedColumns` reads each property's `title` to\n\t\t * label a column the manifest gave only a key for. A request in flight\n\t\t * for the same register and schema is shared, and an answer for a list\n\t\t * that has since been retargeted is dropped.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSchema() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (this.createSchema || !c.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst key = `${c.register || ''}/${c.schema}`\n\t\t\tif (this.schemaRequest && this.schemaRequest.key === key) {\n\t\t\t\treturn this.schemaRequest.promise\n\t\t\t}\n\t\t\tconst promise = this.fetchSchema(c)\n\t\t\tthis.schemaRequest = { key, promise }\n\t\t\ttry {\n\t\t\t\tconst schema = await promise\n\t\t\t\tif (this.schemaRequest && this.schemaRequest.promise === promise) {\n\t\t\t\t\tthis.createSchema = schema\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (this.schemaRequest && this.schemaRequest.promise === promise) {\n\t\t\t\t\tthis.schemaRequest = null\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read a list's schema definition from OpenRegister.\n\t\t *\n\t\t * @param {object} c The list config (`register`, `schema`).\n\t\t * @return {Promise<object|null>} The schema, or null.\n\t\t */\n\t\tasync fetchSchema(c) {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\t// A slug is NOT a namespace (same rule as useObjectStore.fetchSchema,\n\t\t\t// #725): on an instance where two apps both own a `page` schema the\n\t\t\t// bare endpoint resolves instance-wide and serves the OTHER app's\n\t\t\t// schema into this form — the dialog then asks for fields the\n\t\t\t// register-scoped POST does not know and refuses the ones it\n\t\t\t// requires (portaliq + opencatalogi, WOO-564: a 400 on every\n\t\t\t// \"Add page\"/\"Add menu\"). Name the list's own register whenever it\n\t\t\t// has one; a backend that does not know the parameter ignores it.\n\t\t\tconst url = generateUrl('/apps/openregister/api/schemas/{sch}', { sch: c.schema })\n\t\t\tconst params = c.register ? { register: c.register } : undefined\n\t\t\tconst res = await axios.get(url, params ? { params } : undefined)\n\t\t\treturn (res && res.data) || null\n\t\t},\n\n\t\t/**\n\t\t * Load the schema when, and only when, a column needs it: for its\n\t\t * heading (no `label`), or for its presentation (a top-level key\n\t\t * without its own `format`, `enum`, `widget` or `formatter`, which\n\t\t * takes the property's format and enum labels).\n\t\t *\n\t\t * A widget whose every column says its label and presentation, or\n\t\t * reads a dotted path the list's schema does not hold, never makes\n\t\t * this request.\n\t\t *\n\t\t * Failure is deliberately silent: an unreachable schema costs the nicer\n\t\t * heading, and the column still renders under its key. It must never\n\t\t * cost the list itself.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadHeadingsIfNeeded() {\n\t\t\tconst cols = Array.isArray(this.content.columns) ? this.content.columns : []\n\t\t\tconst needsSchema = cols.some((c) => (typeof c === 'string')\n\t\t\t\t|| !c.label\n\t\t\t\t|| (typeof c.key === 'string' && !c.key.includes('.') && !c.key.startsWith('@')\n\t\t\t\t\t&& c.format === undefined && c.enum === undefined && c.widget === undefined && c.formatter === undefined))\n\t\t\tif (needsSchema === false) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t} catch {\n\t\t\t\t// Headings only. Swallowed on purpose; see the docblock.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist the create-dialog form: the resolved scalar filter values\n\t\t * are merged in as defaults (an FK-scoped list creates PRE-LINKED\n\t\t * children — a task added on a case detail already carries the case).\n\t\t *\n\t\t * @param {object} formData Confirmed form values.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreateConfirm(formData) {\n\t\t\tconst c = this.content || {}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst payload = { ...formData }\n\t\t\t\tconst filter = this.resolvedFilter || {}\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v && typeof v !== 'object' && (payload[k] === undefined || payload[k] === null || payload[k] === '')) {\n\t\t\t\t\t\tpayload[k] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst endpoint = c.createEndpoint && typeof c.createEndpoint === 'object' && c.createEndpoint.url ? c.createEndpoint : null\n\t\t\t\tconst url = endpoint ? generateUrl(endpoint.url) : generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst body = endpoint && endpoint.bodyKey ? { ...(endpoint.extra || {}), [endpoint.bodyKey]: payload } : { ...((endpoint && endpoint.extra) || {}), ...payload }\n\t\t\t\tconst response = await axios.post(url, body)\n\t\t\t\tdispatchObjectCreated({ register: c.register, schema: c.schema, object: (response && response.data) || payload })\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event created Emitted after a successful create with the sent payload.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('created', payload)\n\t\t\t\tthis.fetchRows()\n\t\t\t} catch (e) {\n\t\t\t\t// The server's own refusal (\"a case type keeps one final status\")\n\t\t\t\t// says more than the transport's \"Request failed with status 422\".\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ error: (typeof refusal === 'string' && refusal) || (e && e.message) || 'error' })\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pager click — refetch that page from the server. Paging is never\n\t\t * client-side over an already-capped window: that would silently drop\n\t\t * every row past the cap while still displaying a total that counted\n\t\t * them.\n\t\t *\n\t\t * @param {number} next The 1-based page to load.\n\t\t * @return {void}\n\t\t */\n\t\tonPageChange(next) {\n\t\t\tconst target = Math.min(Math.max(Number(next) || 1, 1), this.totalPages)\n\t\t\tif (target === this.page) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.page = target\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t/**\n\t\t * Load the current user's groups when a `footerLinks` entry names one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFooterGroups() {\n\t\t\tconst links = Array.isArray(this.content.footerLinks) ? this.content.footerLinks : []\n\t\t\tif (this.footerGroups !== null || !links.some((link) => link && typeof link.group === 'string' && link.group !== '')) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.footerGroups = await getCurrentUserGroups().catch(() => [])\n\t\t},\n\n\t\t/**\n\t\t * A footer link click: a route link goes through the router (a\n\t\t * modified click is left to the browser), an `href` is a plain link.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @param {{location: object|null}} link The link.\n\t\t * @return {void}\n\t\t */\n\t\tonFooterLink(event, link) {\n\t\t\tif (link.location) {\n\t\t\t\tfollowLinkClick(event, link.location, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * \"View all (N)\" footer click: emits `view-all` and, when the content\n\t\t * blob names a `viewAllRoute`, navigates there with `viewAllQuery`\n\t\t * (its values token-resolved, so `{\"case\": \"@objectId\"}` carries the\n\t\t * current object scope into the target index page). A modified click\n\t\t * on the link is left to the browser.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonViewAll(event) {\n\t\t\t/**\n\t\t\t * @event view-all Emitted when the \"View all (N)\" footer is clicked.\n\t\t\t * @type {{ total: number }}\n\t\t\t */\n\t\t\tthis.$emit('view-all', { total: this.total })\n\t\t\tif (this.viewAllLocation) {\n\t\t\t\tfollowLinkClick(event, this.viewAllLocation, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate to a configured detail route on row click (when `rowRoute`\n\t\t * is set and a router is available); a ctrl/cmd/shift click opens it\n\t\t * in a new tab.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t * @return {void}\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (row && row._readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.rowSelection === true && this.cnWidgetSelection && typeof this.cnHostWidgetId === 'function') {\n\t\t\t\tthis.cnWidgetSelection.select(this.cnHostWidgetId(), row)\n\t\t\t}\n\t\t\tthis.openRowRoute(row, event)\n\t\t\t/**\n\t\t\t * @event row-click Emitted with the clicked object (for hosts that\n\t\t\t * want to handle navigation themselves). Payload: `(row, event)`, the\n\t\t\t * second being the native click event. A middle click emits\n\t\t\t * `row-aux-click` instead.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Publish the rows this list shows under its widget id, for a card that\n\t\t * follows them (`content.rowSelection`).\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/side-card-follows-row/specs/side-card-follows-row/spec.md#requirement-a-list-publishes-its-rows-and-its-selected-row\n\t\t */\n\t\tpublishSelectionRows() {\n\t\t\tif (this.content.rowSelection !== true || !this.cnWidgetSelection || typeof this.cnHostWidgetId !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnWidgetSelection.publishRows(this.cnHostWidgetId(), this.facetedRows)\n\t\t},\n\n\t\t/**\n\t\t * A row middle click: opens the configured detail route in a new tab\n\t\t * and emits `row-aux-click`, not `row-click`.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} event The originating auxclick event.\n\t\t * @return {void}\n\t\t */\n\t\tonRowAuxClick(row, event) {\n\t\t\tthis.openRowRoute(row, event)\n\t\t\t/**\n\t\t\t * @event row-aux-click Emitted with the middle-clicked object, for\n\t\t\t * opening it in a new tab. Payload: `(row, event)`, the second being\n\t\t\t * the native auxclick event.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('row-aux-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Open `content.rowRoute` for a row: in place, or in a new tab on a\n\t\t * ctrl/cmd/shift or middle click.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} [event] The originating event.\n\t\t * @return {void}\n\t\t */\n\t\topenRowRoute(row, event) {\n\t\t\tconst byField = this.content.rowRouteField && row ? (this.content.rowRouteMap || {})[String(row[this.content.rowRouteField])] : undefined\n\t\t\tconst route = byField || this.content.rowRoute\n\t\t\tconst field = this.content.rowIdField || this.content.rowRouteIdField\n\t\t\tconst raw = field ? readPath(row, field) : row && (row.id || (row['@self'] && row['@self'].id))\n\t\t\tconst id = typeof raw === 'string' || typeof raw === 'number' ? String(raw) : ''\n\t\t\tif (route && id && this.$router) {\n\t\t\t\tmarkNewTabHandled(event, openRowTarget(event, { name: route, params: { id } }, this.$router))\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch one declared row action. A `handler` action gets the row\n\t\t * appended as its last argument, the same convention CnWidgetObjectTable\n\t\t * uses, so a registry function reads its row from where it already\n\t\t * expects one. An `open-modal` action gets the row merged onto its\n\t\t * `props` as `row` — mirroring how a drop hands a modal `props.files`\n\t\t * and a bulk action hands it `props.selectedIds` — because a \"Versions\"\n\t\t * or \"Delete\" row action is meaningless without knowing WHICH row it\n\t\t * was clicked on, and `open-modal`'s `action.props` is forwarded\n\t\t * verbatim otherwise (it carries no per-click information at all).\n\t\t *\n\t\t * @param {object} action The declared action.\n\t\t * @param {object|null} row The row the action was triggered on.\n\t\t * @return {void}\n\t\t */\n\t\trunRowAction(action, row) {\n\t\t\tconst pending = this.dispatch(actionForRow(action, row), [row], { row })\n\t\t\tif (action && action.type === 'object-op' && action.op === 'delete') {\n\t\t\t\tthis.reportDelete(row, pending)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Toast a row delete, with an Undo for ten seconds that restores the row\n\t\t * from the OpenRegister trash and refreshes the list. `feedback: false` in\n\t\t * the widget `content` suppresses it. A create needs nothing here: the\n\t\t * create dialog already toasts, so the widget stays quiet to give one.\n\t\t *\n\t\t * @param {object} row The deleted row.\n\t\t * @param {Promise<unknown>|unknown} pending What the dispatcher returned.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync reportDelete(row, pending) {\n\t\t\tif ((this.content || {}).feedback === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst feedback = useWriteFeedback()\n\t\t\tlet outcome\n\t\t\ttry {\n\t\t\t\toutcome = await pending\n\t\t\t} catch (e) {\n\t\t\t\tfeedback.error((e && e.message) || t('nextcloud-vue', 'The delete failed'))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (outcome === false) {\n\t\t\t\tfeedback.error(t('nextcloud-vue', 'The delete failed'))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = row && (row.id || (row['@self'] && row['@self'].id))\n\t\t\tconst title = objectDisplayName(row || {})\n\t\t\tconst message = title !== '' ? t('nextcloud-vue', 'Deleted {title}', { title }) : t('nextcloud-vue', 'Deleted')\n\t\t\tfeedback.success(message, id\n\t\t\t\t? {\n\t\t\t\t\t\tundo: async () => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\t\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/deleted/{id}/restore', { id }))\n\t\t\t\t\t\t\t\tthis.fetchRows()\n\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\tfeedback.error((e && e.message) || t('nextcloud-vue', 'The row could not be restored'))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: {})\n\t\t},\n\n\t\t/**\n\t\t * Link fields for a declared row action that only navigates, so\n\t\t * CnRowActions renders it as a link. A target with tokens is left to\n\t\t * dispatch, which resolves them against the page.\n\t\t *\n\t\t * @param {object} action The declared row action.\n\t\t * @return {object} `{ href, linkTarget }`, `{ to }`, or `{}`.\n\t\t */\n\t\trowActionLink(action) {\n\t\t\t// A route with a query: a router location per row, the query's row\n\t\t\t// tokens filled raw (the router encodes) and its page tokens resolved.\n\t\t\tif (action && action.type === 'navigate' && !action.confirm && typeof action.route === 'string' && action.route !== '') {\n\t\t\t\tconst locationFor = (row) => ({\n\t\t\t\t\tname: action.route,\n\t\t\t\t\tquery: resolveFilterTokens(actionForRow(action, row).query || {}, this.tokenCtx),\n\t\t\t\t})\n\t\t\t\treturn {\n\t\t\t\t\thref: () => '',\n\t\t\t\t\tto: (row) => locationFor(row),\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (action && action.type === 'navigate' && !action.confirm && hasRowTokens(action.target)) {\n\t\t\t\tconst linkFor = (row) => actionLink(actionForRow(action, row), { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\t// An in-app link answers `to` (followed by the router), an external one `href`.\n\t\t\t\treturn {\n\t\t\t\t\thref: (row) => {\n\t\t\t\t\t\tconst link = linkFor(row)\n\t\t\t\t\t\treturn link && link.external ? link.href : ''\n\t\t\t\t\t},\n\n\t\t\t\t\tto: (row) => {\n\t\t\t\t\t\tconst link = linkFor(row)\n\t\t\t\t\t\treturn link && !link.external ? link.to : null\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (hasActionTargetTokens(action.target)) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst link = actionLink(action, { router: this.$router || null })\n\t\t\tif (!link) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn link.external ? { href: link.href, linkTarget: '_blank' } : { to: link.to }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a declared action through the page's pre-bound dispatcher,\n\t\t * falling back to a bare `dispatchAction` when this widget is mounted\n\t\t * outside a CnPageRenderer tree.\n\t\t *\n\t\t * `extraArgs` is appended to a `handler` action's `args`; it is NOT\n\t\t * merged into any other type, because only `handler` has an argument\n\t\t * list. A drop's `File[]` additionally rides an `open-modal` action's\n\t\t * `props.files`, since that is how a modal receives anything at all.\n\t\t *\n\t\t * @param {object} action The declared action.\n\t\t * @param {Array} extraArgs Arguments appended for a `handler` action.\n\t\t * @param {object} extraProps Props merged for an `open-modal` action.\n\t\t * @return {unknown} Whatever the dispatcher returns (a promise for a write).\n\t\t */\n\t\tdispatch(action, extraArgs = [], extraProps = {}) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst type = action.type || 'handler'\n\t\t\tlet wrapped = action\n\t\t\tif (type === 'handler') {\n\t\t\t\twrapped = { ...action, args: [...(action.args || []), ...extraArgs] }\n\t\t\t} else if (type === 'open-modal' && Object.keys(extraProps).length > 0) {\n\t\t\t\twrapped = { ...action, props: { ...(action.props || {}), ...extraProps } }\n\t\t\t}\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(wrapped)\n\t\t\t}\n\t\t\treturn dispatchAction(wrapped, { router: this.$router || null })\n\t\t},\n\n\t\t/**\n\t\t * Whether a drag event is carrying files. A drag of text or of a row\n\t\t * from another table is not a file drop and must not paint the\n\t\t * overlay; `dataTransfer.types` is the only thing readable during a\n\t\t * dragover (the items themselves are not).\n\t\t *\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {boolean}\n\t\t */\n\t\tdragHasFiles(event) {\n\t\t\tconst types = event && event.dataTransfer && event.dataTransfer.types\n\t\t\tif (!types) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Array.prototype.indexOf.call(types, 'Files') !== -1\n\t\t},\n\n\t\t/**\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {void}\n\t\t */\n\t\tonDragEnter(event) {\n\t\t\tif (!this.dropZoneAction || !this.dragHasFiles(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.dragDepth += 1\n\t\t},\n\n\t\t/**\n\t\t * `dragover` must preventDefault or the browser refuses the drop and\n\t\t * navigates to the file instead.\n\t\t *\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver(event) {\n\t\t\tif (!this.dropZoneAction || !this.dragHasFiles(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t},\n\n\t\t/**\n\t\t * Leaving one element of the widget. The event is not read: a\n\t\t * `dragleave` fires for the widget AND for every child crossed, so the\n\t\t * counter — not this event — decides whether the drag is still over us.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tif (!this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.dragDepth = Math.max(0, this.dragDepth - 1)\n\t\t},\n\n\t\t/**\n\t\t * Hand the dropped files to the declared `dropZone` action. Nothing is\n\t\t * uploaded here: this component reads no file content and calls no\n\t\t * write endpoint, so whoever receives the files decides where they go\n\t\t * — the same split the form file field keeps.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tif (!this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.dragHasFiles(event)) {\n\t\t\t\tthis.dragDepth = 0\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.dragDepth = 0\n\t\t\tconst files = Array.from((event.dataTransfer && event.dataTransfer.files) || [])\n\t\t\tif (files.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event files-dropped Emitted with the dropped files, for a host\n\t\t\t * that wants to handle the drop itself rather than declare an action.\n\t\t\t * @type {Array<File>}\n\t\t\t */\n\t\t\tthis.$emit('files-dropped', files)\n\t\t\tthis.dispatch(this.dropZoneAction, [files], { files })\n\t\t},\n\n\t\t/**\n\t\t * Open the native file picker for the click-to-upload button — the\n\t\t * same `dropZoneAction` a drop dispatches, so a reader who never drags\n\t\t * a file reaches the identical outcome.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttriggerUpload() {\n\t\t\tif (this.$refs.uploadInput) {\n\t\t\t\tthis.$refs.uploadInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle files chosen via the upload button's file picker.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonUploadFilesSelected(event) {\n\t\t\tconst files = Array.from((event.target && event.target.files) || [])\n\t\t\tif (files.length === 0 || !this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('files-dropped', files)\n\t\t\tthis.dispatch(this.dropZoneAction, [files], { files })\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `select` handler — replaces the whole selected-ids\n\t\t * array (it already computed the add/remove/select-all arithmetic).\n\t\t *\n\t\t * @param {Array<string>} ids The new selection.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(ids) {\n\t\t\tthis.selectedIds = Array.isArray(ids) ? ids : []\n\t\t},\n\n\t\t/**\n\t\t * Clear the current selection. Called after a bulk action fires, and\n\t\t * from the bulk bar's own \"Clear selection\" button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearSelection() {\n\t\t\tthis.selectedIds = []\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a declared bulk action against the current selection, then\n\t\t * clear it — the action owns its own success/error feedback and page\n\t\t * refresh (same convention as a row action), this widget only owns the\n\t\t * checkbox state.\n\t\t *\n\t\t * @param {object} action The declared bulk action.\n\t\t * @return {void}\n\t\t */\n\t\trunBulkAction(action) {\n\t\t\tthis.dispatch(action, [this.selectedRows], { selectedIds: [...this.selectedIds] })\n\t\t\tthis.clearSelection()\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `sort` handler for an interactively-sortable column\n\t\t * (`content.sortable`). Overwrites `localSort`, which both the fetch\n\t\t * params and the table's own sort-indicator props read — a cleared\n\t\t * sort (`key: null`, a third click on a two-state header) goes back to\n\t\t * the unordered default rather than sticking on the last field.\n\t\t *\n\t\t * @param {{key: string|null, order: string|null}} payload The new sort.\n\t\t * @return {void}\n\t\t */\n\t\tonSort({ key, order }) {\n\t\t\tthis.localSort = key ? { field: key, dir: order || 'asc' } : {}\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `column-filter` handler (`content.filterable`): the\n\t\t * filter joins the others and the list fetches again from page one.\n\t\t *\n\t\t * @param {{key: string, params: object}} payload The column's filter.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/table-widget-header-controls/specs/table-widget-header-controls/spec.md\n\t\t */\n\t\tonHeaderFilter(payload) {\n\t\t\tthis.headerFilters = nextHeaderFilters(this.headerFilters, payload)\n\t\t},\n\n\t\t/**\n\t\t * Toggle one facet value on/off (`content.facet`).\n\t\t *\n\t\t * @param {string} value The facet value clicked.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacetValue(value) {\n\t\t\tconst index = this.facetSelected.indexOf(value)\n\t\t\tif (index === -1) {\n\t\t\t\tthis.facetSelected = [...this.facetSelected, value]\n\t\t\t} else {\n\t\t\t\tthis.facetSelected = this.facetSelected.filter((v) => v !== value)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear the facet filter, so every loaded row shows again.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacet() {\n\t\t\tthis.facetSelected = []\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The row a following card describes (content.rowSelection). */\n.cn-object-list-widget :deep(.cn-object-list-widget__row--selected) {\n\tbackground: var(--color-primary-element-light, #eaf0f7);\n}\n\n/* The widget already lives inside CnWidgetWrapper's padded card chrome. The\n table is rendered `borderless` so CnDataTable drops its own border/shadow/\n background AND its bottom margin (the `.cn-table-container--borderless`\n modifier) — otherwise it reads as a card-in-a-card with dead space below.\n Horizontal overflow is owned by `.cn-table-container` (overflow-x: auto); a\n second scroll container here would produce a nested scrollbar, so this host\n stays plain. */\n.cn-object-list-widget {\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n/* The overlay covers the widget while a file drag is over it. `position:\n relative` is set only in the dropping state so a widget without a drop zone\n keeps the containing block it had — an unconditional rule here would\n re-parent any absolutely-positioned descendant a host has placed. */\n.cn-object-list-widget--dropping {\n\tposition: relative;\n}\n\n.cn-object-list-widget__drop-overlay {\n\tposition: absolute;\n\tinset: 0;\n\tz-index: 2;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\ttext-align: center;\n\tpadding: var(--cn-spacing-m, 12px);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\t/* The overlay is a painted state, not a target: letting it swallow the\n\t pointer would fire dragleave the instant it appeared under the cursor,\n\t and the drop would land on nothing. */\n\tpointer-events: none;\n}\n\n.cn-object-list-widget__table {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: hidden;\n}\n\n/* The empty state is CnWidgetEmptyState now — it brings its own layout, so\n all this rule still owns is letting it claim the cell's leftover height\n (ADR-062: it fits the cell, it never forces one). */\n.cn-object-list-widget__empty {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The pager sits flush at the bottom of the card, above \"View all\". */\n.cn-object-list-widget__pager {\n\tflex-shrink: 0;\n}\n\n.cn-object-list-widget__view-all {\n\talign-self: flex-start;\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tfont: inherit;\n\tmargin-top: 4px;\n\tpadding: 4px;\n\ttext-decoration: none;\n}\n\n.cn-object-list-widget__view-all:hover,\n.cn-object-list-widget__view-all:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-object-list-widget__more {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tmargin: 4px 0 0;\n\tpadding: 4px;\n}\n\n.cn-object-list-widget__links {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 16px;\n\tmargin: 12px 0 0;\n\tfont-size: 14px;\n}\n\n.cn-object-list-widget__link {\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__link-note {\n\tmargin-inline-start: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-object-list-widget__footnote {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tmargin: 8px 0 0;\n}\n\n/* Footer Add — same pattern as the integration leaves' footer action\n (\"Open in Calendar\"): full-width, centered, divider-topped, pinned to\n the card bottom. */\n.cn-object-list-widget__add {\n\talign-self: stretch;\n\tborder-top: 1px solid var(--color-border);\n\t/* Bleed through the host card's 16px content padding so the divider\n\t spans edge-to-edge, exactly like the integration leaves' footer. */\n\tmargin: auto -16px -16px;\n\tpadding: 4px;\n}\n\n.cn-object-list-widget__error {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tmargin: 4px 0 0;\n}\n\n.cn-object-list-widget__prompt {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tpadding: 16px 4px;\n\tmargin: 0;\n\ttext-align: center;\n}\n\n.cn-object-list-widget__facet {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-wrap: wrap;\n\tgap: 6px;\n\tpadding: 0 4px 8px;\n}\n\n.cn-object-list-widget__facet-label {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__facet-chip {\n\tbackground-color: var(--color-background-hover);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-pill, 16px);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont: inherit;\n\tfont-size: 0.85em;\n\tpadding: 2px 10px;\n}\n\n.cn-object-list-widget__facet-chip--active {\n\tbackground-color: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, var(--color-main-text));\n}\n\n.cn-object-list-widget__facet-clear,\n.cn-object-list-widget__bulk-clear {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tfont: inherit;\n\tfont-size: 0.85em;\n\tpadding: 2px 4px;\n}\n\n.cn-object-list-widget__facet-clear:hover,\n.cn-object-list-widget__bulk-clear:hover {\n\ttext-decoration: underline;\n}\n\n.cn-object-list-widget__bulk-bar {\n\talign-items: center;\n\tbackground-color: var(--color-primary-element-light);\n\tborder-radius: var(--border-radius, 8px);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tpadding: 6px 10px;\n}\n\n.cn-object-list-widget__bulk-count {\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__bulk-action {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tcursor: pointer;\n\tfont: inherit;\n\tpadding: 4px 10px;\n}\n\n.cn-object-list-widget__bulk-action--destructive {\n\tborder-color: var(--color-error);\n\tcolor: var(--color-error);\n}\n\n.cn-object-list-widget__groups {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tmin-height: 0;\n\toverflow: auto;\n}\n\n.cn-object-list-widget__group-heading {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tmargin: 0 0 4px;\n\ttext-transform: uppercase;\n}\n\n.cn-object-list-widget__upload {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tcursor: pointer;\n\tfont: inherit;\n\tmargin: 8px 0 0;\n\tpadding: 6px 12px;\n}\n\n.cn-object-list-widget__upload-input {\n\tdisplay: none;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_createBlock","_createVNode","_createTextVNode","_createElementVNode","_Fragment","_renderList","_openBlock","_normalizeClass","_resolveDynamicComponent","_withCtx"],"mappings":"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"}
1
+ {"version":3,"file":"CnObjectListWidget.vue3.js","sources":["../../../../src/components/CnObjectListWidget/CnObjectListWidget.vue?vue&type=template&id=29fab8f0&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-object-list-widget\"\n\t\t:class=\"{ 'cn-object-list-widget--dropping': dropping, 'cn-object-list-widget--bleed': content.tableBleed === true }\"\n\t\t@dragenter=\"onDragEnter\"\n\t\t@dragover=\"onDragOver\"\n\t\t@dragleave=\"onDragLeave\"\n\t\t@drop=\"onDrop\">\n\t\t<!-- Drop overlay. Rendered only while a drag carrying files is over a\n\t\t widget that declares `content.dropZone`; without the key the drag\n\t\t handlers below all return before touching any state, so a widget\n\t\t that did not ask for a drop zone behaves exactly as before. -->\n\t\t<div v-if=\"dropping\" class=\"cn-object-list-widget__drop-overlay\">\n\t\t\t{{ dropLabel }}\n\t\t</div>\n\t\t<p v-if=\"waitingForContext\" class=\"cn-object-list-widget__prompt\">\n\t\t\t{{ promptText }}\n\t\t</p>\n\t\t<!-- A fetch failed: show ONE quiet line WITHOUT the raw axios status\n\t\t text (\"Request failed with status code 404\") — the real error is\n\t\t logged to the console (ADR-062: an error surface is never a leaked\n\t\t stack). Takes precedence over the empty state so the two never\n\t\t stack. -->\n\t\t<p v-else-if=\"error\" class=\"cn-object-list-widget__error\">\n\t\t\t{{ loadErrorLabel }}\n\t\t</p>\n\t\t<!-- Designed empty state. This used to be a bare CnDataTable with no\n\t\t rows, which paints its <thead> — a full-width grey strip floating\n\t\t in the middle of an otherwise blank card, the \"strange grey bar\"\n\t\t from the review. CnWidgetEmptyState replaces it with something\n\t\t deliberate, and stays `compact` inside a fit-measured cell so it\n\t\t cannot outgrow a short tile (ADR-062). -->\n\t\t<CnWidgetEmptyState\n\t\t\tv-else-if=\"!loading && rows.length === 0 && readOnlyRows.length === 0\"\n\t\t\t:name=\"resolvedEmptyText\"\n\t\t\t:compact=\"fitRows !== null && fitRows < 3\"\n\t\t\tclass=\"cn-object-list-widget__empty\">\n\t\t\t<template v-if=\"allowCreate && !waitingForContext\" #action>\n\t\t\t\t<NcButton class=\"cn-object-list-widget__empty-add\" @click=\"openCreate\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ addLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnWidgetEmptyState>\n\t\t<!-- The fetch came back with rows, but the active facet selection\n\t\t matches none of them (all loaded rows have that filter row set\n\t\t empty of a facet-clearable state). Distinct from the \"no items at\n\t\t all\" state above: an Add button is not the fix here, clearing the\n\t\t filter is. -->\n\t\t<CnWidgetEmptyState\n\t\t\tv-else-if=\"showNoFacetMatchState\"\n\t\t\t:name=\"t('nextcloud-vue', 'No items match this filter')\"\n\t\t\t:compact=\"fitRows !== null && fitRows < 3\"\n\t\t\tclass=\"cn-object-list-widget__empty\">\n\t\t\t<template #action>\n\t\t\t\t<button type=\"button\" class=\"cn-object-list-widget__facet-clear\" @click=\"clearFacet\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear filter') }}\n\t\t\t\t</button>\n\t\t\t</template>\n\t\t</CnWidgetEmptyState>\n\t\t<template v-else>\n\t\t\t<!-- Facet filter (`content.facet`). Chips built from the values\n\t\t\t actually present on the loaded rows, matching the \"keyword\n\t\t\t filter\" pattern this seam was measured against — narrows the\n\t\t\t rendered rows client-side, no refetch. Absent facet config\n\t\t\t renders nothing, so an ungrouped/unfiltered widget is unchanged. -->\n\t\t\t<div v-if=\"facetOptions.length > 0\" class=\"cn-object-list-widget__facet\" data-testid=\"object-list-facet\">\n\t\t\t\t<span class=\"cn-object-list-widget__facet-label\">{{ facetLabel }}</span>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"value in facetOptions\"\n\t\t\t\t\t:key=\"value\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__facet-chip\"\n\t\t\t\t\t:class=\"{ 'cn-object-list-widget__facet-chip--active': facetSelected.includes(value) }\"\n\t\t\t\t\t:aria-pressed=\"facetSelected.includes(value)\"\n\t\t\t\t\tdata-testid=\"object-list-facet-chip\"\n\t\t\t\t\t@click=\"toggleFacetValue(value)\">\n\t\t\t\t\t{{ value }}\n\t\t\t\t</button>\n\t\t\t\t<button\n\t\t\t\t\tv-if=\"facetSelected.length > 0\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__facet-clear\"\n\t\t\t\t\t@click=\"clearFacet\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear filter') }}\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<!-- Bulk-actions bar (`content.selectable` + `content.bulkActions`).\n\t\t\t Renders only once something is selected, so a selectable widget\n\t\t\t with nothing picked looks exactly like a non-selectable one. -->\n\t\t\t<div\n\t\t\t\tv-if=\"content.selectable && mappedBulkActions.length > 0 && selectedIds.length > 0\"\n\t\t\t\tclass=\"cn-object-list-widget__bulk-bar\"\n\t\t\t\tdata-testid=\"object-list-bulk-bar\">\n\t\t\t\t<span class=\"cn-object-list-widget__bulk-count\">\n\t\t\t\t\t{{ t('nextcloud-vue', '{count} selected', { count: selectedIds.length }) }}\n\t\t\t\t</span>\n\t\t\t\t<button\n\t\t\t\t\tv-for=\"(action, index) in mappedBulkActions\"\n\t\t\t\t\t:key=\"index\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tclass=\"cn-object-list-widget__bulk-action\"\n\t\t\t\t\t:class=\"{ 'cn-object-list-widget__bulk-action--destructive': action.destructive }\"\n\t\t\t\t\tdata-testid=\"object-list-bulk-action\"\n\t\t\t\t\t@click=\"action.handler\">\n\t\t\t\t\t{{ action.label }}\n\t\t\t\t</button>\n\t\t\t\t<button type=\"button\" class=\"cn-object-list-widget__bulk-clear\" @click=\"clearSelection\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Clear selection') }}\n\t\t\t\t</button>\n\t\t\t</div>\n\n\t\t\t<div v-if=\"isGrouped\" class=\"cn-object-list-widget__groups\">\n\t\t\t\t<div v-for=\"group in groupedRows\"\n\t\t\t\t\t:key=\"group.key\"\n\t\t\t\t\tclass=\"cn-object-list-widget__group\"\n\t\t\t\t\tdata-testid=\"object-list-group\">\n\t\t\t\t\t<component :is=\"innerHeadingTag\" class=\"cn-object-list-widget__group-heading cn-inner-heading\">\n\t\t\t\t\t\t<!-- `groupLabelResolve` reuses the fkResolve cell for the\n\t\t\t\t\t\t heading itself: `groupBy` may bucket by a uuid that is\n\t\t\t\t\t\t ONE MORE hop past what `extend` inlined (e.g. a\n\t\t\t\t\t\t reference's own reference), which `objectFieldValue`\n\t\t\t\t\t\t cannot read off the row at all. The live object-store\n\t\t\t\t\t\t lookup CnDataTable columns already use for a `$ref`\n\t\t\t\t\t\t column is the same answer for a `$ref` GROUP. -->\n\t\t\t\t\t\t<CnFkResolveCell\n\t\t\t\t\t\t\tv-if=\"groupLabelResolveConfig\"\n\t\t\t\t\t\t\t:value=\"group.key\"\n\t\t\t\t\t\t\t:register=\"groupLabelResolveConfig.register\"\n\t\t\t\t\t\t\t:schema=\"groupLabelResolveConfig.schema\"\n\t\t\t\t\t\t\t:labelField=\"groupLabelResolveConfig.labelField || 'name'\" />\n\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t({{ group.rows.length }})\n\t\t\t\t\t</component>\n\t\t\t\t\t<CnDataTable\n\t\t\t\t\t\t:columns=\"resolvedColumns\"\n\t\t\t\t\t\t:rows=\"group.rows\"\n\t\t\t\t\t\t:loading=\"loading\"\n\t\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t\t:selectable=\"content.selectable === true\"\n\t\t\t\t\t\t:selectedIds=\"selectedIds\"\n\t\t\t\t\t\t:sortKey=\"localSort.field || null\"\n\t\t\t\t\t\t:sortOrder=\"localSort.dir || 'asc'\"\n\t\t\t\t\t\tborderless\n\t\t\t\t\t\t:rowTitle=\"rowTitle\"\n\t\t\t\t\t\t:fitWidth=\"fitWidthActive\"\n\t\t\t\t\t\t:fixedLayout=\"fixedLayoutActive\"\n\t\t\t\t\t\t:hideHeader=\"content.hideHeader === true\"\n\t\t\t\t\t\t:filterable=\"content.filterable === true\"\n\t\t\t\t\t\t:activeFilters=\"headerFilters\"\n\t\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t\t@sort=\"onSort\"\n\t\t\t\t\t\t@columnFilter=\"onHeaderFilter\">\n\t\t\t\t\t\t<template v-if=\"mappedRowActions.length > 0\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t\t<CnRowActions v-if=\"!row._readOnly\" :actions=\"mappedRowActions\" :row=\"row\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnDataTable>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div v-else class=\"cn-object-list-widget__table\">\n\t\t\t\t<CnDataTable\n\t\t\t\t\t:reorderable=\"reorderField !== ''\"\n\t\t\t\t\t:columns=\"resolvedColumns\"\n\t\t\t\t\t:rows=\"facetedRows\"\n\t\t\t\t\t:rowClass=\"selectionRowClass\"\n\t\t\t\t\t:loading=\"loading\"\n\t\t\t\t\t:emptyText=\"emptyText\"\n\t\t\t\t\t:selectable=\"content.selectable === true\"\n\t\t\t\t\t:selectedIds=\"selectedIds\"\n\t\t\t\t\t:sortKey=\"localSort.field || null\"\n\t\t\t\t\t:sortOrder=\"localSort.dir || 'asc'\"\n\t\t\t\t\tborderless\n\t\t\t\t\t:rowTitle=\"rowTitle\"\n\t\t\t\t\t:fitWidth=\"fitWidthActive\"\n\t\t\t\t\t:fixedLayout=\"fixedLayoutActive\"\n\t\t\t\t\t:hideHeader=\"content.hideHeader === true\"\n\t\t\t\t\t:filterable=\"content.filterable === true\"\n\t\t\t\t\t:activeFilters=\"headerFilters\"\n\t\t\t\t\t@rowClick=\"onRowClick\"\n\t\t\t\t\t@rowAuxClick=\"onRowAuxClick\"\n\t\t\t\t\t@select=\"onSelect\"\n\t\t\t\t\t@sort=\"onSort\"\n\t\t\t\t\t@columnFilter=\"onHeaderFilter\"\n\t\t\t\t\t@rowMove=\"onRowMove\">\n\t\t\t\t\t<!-- Declarative per-row actions (`content.rowActions`).\n\t\t\t\t\t CnDataTable only paints the trailing actions column\n\t\t\t\t\t when this slot is supplied, so a widget without\n\t\t\t\t\t `rowActions` keeps the column count it had. -->\n\t\t\t\t\t<template v-if=\"mappedRowActions.length > 0\" #row-actions=\"{ row }\">\n\t\t\t\t\t\t<CnRowActions v-if=\"!row._readOnly\" :actions=\"mappedRowActions\" :row=\"row\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<!-- `content.children`: each row's child records under it, as\n\t\t\t\t\t a nested list fed from one query for all rows. -->\n\t\t\t\t\t<template v-if=\"childrenConfig && childLink\" #row-detail=\"{ row }\">\n\t\t\t\t\t\t<CnObjectListWidget\n\t\t\t\t\t\t\tclass=\"cn-object-list-widget__children\"\n\t\t\t\t\t\t\tdata-testid=\"cn-object-list-children\"\n\t\t\t\t\t\t\t:content=\"childContentFor(row)\"\n\t\t\t\t\t\t\t:presetRows=\"childRowsFor(row)\"\n\t\t\t\t\t\t\t@rowsStale=\"fetchChildren\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnDataTable>\n\t\t\t</div>\n\t\t\t<!--\n\t\t\t Footer. Two affordances, and they answer different questions.\n\n\t\t\t The PAGER walks the rest of the matching objects in place —\n\t\t\t server-side (`_page`), so \"1–5 of 137\" is the truth and not a\n\t\t\t count of what happened to be fetched. It renders only when a\n\t\t\t whole page fits the cell; on a cell too short for one page the\n\t\t\t fit-to-cell path below is all there is, because a pager under\n\t\t\t rows that are themselves clipped would page a lie.\n\n\t\t\t VIEW ALL leaves for the full index. It shows whenever there is\n\t\t\t more than the widget is showing, pager or no pager — a widget is\n\t\t\t a summary, and the index is where the list actually lives.\n\t\t\t-->\n\t\t\t<!-- A reorder's outcome, read out: the row's new place, or why the\n\t\t\t server refused it. -->\n\t\t\t<p class=\"hidden-visually cn-object-list-widget__reorder-status\" aria-live=\"polite\" data-testid=\"cn-object-list-reorder-status\">\n\t\t\t\t{{ reorderStatus }}\n\t\t\t</p>\n\t\t\t<CnPagination\n\t\t\t\tv-if=\"showPager\"\n\t\t\t\tcompact\n\t\t\t\tclass=\"cn-object-list-widget__pager\"\n\t\t\t\t:currentPage=\"page\"\n\t\t\t\t:totalPages=\"totalPages\"\n\t\t\t\t:totalItems=\"total\"\n\t\t\t\t:currentPageSize=\"pageSize\"\n\t\t\t\t:minItemsToShow=\"0\"\n\t\t\t\t@pageChanged=\"onPageChange\" />\n\t\t\t<a\n\t\t\t\tv-if=\"hiddenCount > 0 && content.viewAllRoute && viewAllHref\"\n\t\t\t\tclass=\"cn-object-list-widget__view-all\"\n\t\t\t\t:href=\"viewAllHref\"\n\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t{{ viewAllLabel }}\n\t\t\t</a>\n\t\t\t<button\n\t\t\t\tv-else-if=\"hiddenCount > 0 && content.viewAllRoute\"\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-object-list-widget__view-all\"\n\t\t\t\t@click=\"onViewAll\">\n\t\t\t\t{{ viewAllLabel }}\n\t\t\t</button>\n\t\t\t<p v-else-if=\"hiddenCount > 0 && !showPager\" class=\"cn-object-list-widget__more\">\n\t\t\t\t{{ moreLabel }}\n\t\t\t</p>\n\t\t</template>\n\t\t<!-- `content.footnote`: a line under the table (\"Awb 4:17 en 4:18\"),\n\t\t through the host translate. Not while the list waits for its\n\t\t context or failed to load. -->\n\t\t<!-- `content.summary`: counts over the loaded rows (\"(1/2 completed)\"). -->\n\t\t<p\n\t\t\tv-if=\"resolvedSummary && !waitingForContext && !error && rows.length > 0\"\n\t\t\tclass=\"cn-object-list-widget__summary\"\n\t\t\tdata-testid=\"cn-object-list-widget-summary\">\n\t\t\t{{ resolvedSummary }}\n\t\t</p>\n\t\t<p\n\t\t\tv-if=\"resolvedFootnote && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__footnote\"\n\t\t\tdata-testid=\"cn-object-list-widget-footnote\">\n\t\t\t{{ resolvedFootnote }}\n\t\t</p>\n\t\t<!-- `content.footerLinks`: a row of links under the list, each with\n\t\t an optional muted note (\"Objecttypen beheren (alleen voor\n\t\t beheerders)\"). -->\n\t\t<div\n\t\t\tv-if=\"footerLinks.length > 0 && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__links\"\n\t\t\tdata-testid=\"cn-object-list-widget-links\">\n\t\t\t<span v-for=\"link in footerLinks\" :key=\"link.key\" class=\"cn-object-list-widget__link-item\">\n\t\t\t\t<a\n\t\t\t\t\tclass=\"cn-object-list-widget__link\"\n\t\t\t\t\t:href=\"link.href\"\n\t\t\t\t\tdata-testid=\"cn-object-list-widget-link\"\n\t\t\t\t\t@click=\"onFooterLink($event, link)\">{{ link.label }}</a>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"link.note\"\n\t\t\t\t\tclass=\"cn-object-list-widget__link-note\"\n\t\t\t\t\tdata-testid=\"cn-object-list-widget-link-note\">{{ link.note }}</span>\n\t\t\t</span>\n\t\t</div>\n\t\t<!-- Why the declared create action is withheld on this record\n\t\t (`content.createAction.hiddenNote`). -->\n\t\t<p\n\t\t\tv-if=\"createHiddenNote && !waitingForContext && !error\"\n\t\t\tclass=\"cn-object-list-widget__hidden-note\"\n\t\t\tdata-testid=\"cn-object-list-widget-hidden-note\">\n\t\t\t{{ createHiddenNote }}\n\t\t</p>\n\t\t<!-- Create affordance (ADR-062): every collection carries its Add at\n\t\t the bottom of the widget; the host card's Actions menu calls the\n\t\t same openCreate() through the public method. Suppressed while the\n\t\t empty state is showing — that renders its own copy in its #action\n\t\t slot, and two Add buttons on one empty card read as a bug. -->\n\t\t<div\n\t\t\tv-if=\"allowCreate && !waitingForContext && !showingEmptyState\"\n\t\t\tclass=\"cn-object-list-widget__add\">\n\t\t\t<NcButton variant=\"tertiary\" wide @click=\"openCreate\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Plus :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ addLabel }}\n\t\t\t</NcButton>\n\t\t</div>\n\t\t<!-- Upload affordance. A `dropZone` action already accepts a dropped\n\t\t `File[]`; this is the click-to-pick equivalent of the same drop,\n\t\t for the reader who never drags a file. `content.upload: false`\n\t\t opts a drop-only widget out of the button. -->\n\t\t<button\n\t\t\tv-if=\"showUploadButton\"\n\t\t\ttype=\"button\"\n\t\t\tclass=\"cn-object-list-widget__upload\"\n\t\t\tdata-testid=\"object-list-upload\"\n\t\t\t@click=\"triggerUpload\">\n\t\t\t{{ uploadLabel }}\n\t\t</button>\n\t\t<input\n\t\t\tv-if=\"showUploadButton\"\n\t\t\tref=\"uploadInput\"\n\t\t\ttype=\"file\"\n\t\t\tmultiple\n\t\t\t:aria-label=\"uploadLabel\"\n\t\t\tclass=\"cn-object-list-widget__upload-input\"\n\t\t\tdata-testid=\"object-list-upload-input\"\n\t\t\t@change=\"onUploadFilesSelected\">\n\t\t<CnFormDialog\n\t\t\tv-if=\"(showCreate || editingRow) && createSchema\"\n\t\t\tref=\"createDialog\"\n\t\t\t:schema=\"createSchema\"\n\t\t\t:item=\"editingRow\"\n\t\t\t:register=\"content.register || ''\"\n\t\t\t:initialData=\"editingRow ? null : createInitialData\"\n\t\t\t:lockedFields=\"createLockedFields\"\n\t\t\t:size=\"formSize\"\n\t\t\t:columns=\"formColumns\"\n\t\t\t:includeFields=\"formIncludeFields\"\n\t\t\t:excludeFields=\"formExcludeFields\"\n\t\t\t:fieldOverrides=\"formFieldOverrides\"\n\t\t\t@confirm=\"editingRow ? onEditConfirm($event) : onCreateConfirm($event)\"\n\t\t\t@close=\"closeForm\" />\n\t</div>\n</template>\n\n<script>\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport CnDataTable from '../CnDataTable/CnDataTable.vue'\nimport CnFkResolveCell from '../CnFkResolveCell/CnFkResolveCell.vue'\nimport CnFormDialog from '../CnFormDialog/CnFormDialog.vue'\nimport CnPagination from '../CnPagination/CnPagination.vue'\nimport CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'\nimport { endpointErrorMessage, fetchEndpointSource, getByPath, resolveEndpointRequest } from '../../composables/useEndpointSource.js'\nimport { useWriteFeedback } from '../../composables/useWriteFeedback.js'\nimport innerHeading from '../../mixins/innerHeading.js'\nimport { actionLink, dispatchAction, hasActionTargetTokens } from '../../utils/actionsDispatcher.js'\nimport { fillCountLabel } from '../../utils/fillCountLabel.js'\nimport { headerFilterParams, nextHeaderFilters } from '../../utils/headerFilterState.js'\nimport { buildExportUrl } from '../../utils/indexExportHelpers.js'\nimport { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'\nimport { createActionState } from '../../utils/listCreateAction.js'\nimport { objectDisplayName, objectFieldValue } from '../../utils/objectName.js'\nimport { readPath } from '../../utils/readPath.js'\nimport { moveIndex, renumberRows, rowIdOf } from '../../utils/reorderRows.js'\nimport { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\nimport { markNewTabHandled } from '../../utils/rowAuxClick.js'\nimport { actionForRow, hasRowTokens, resolveRowFilter, rowTokenField } from '../../utils/rowTokens.js'\nimport { changedKeys } from '../../utils/saveEdited.js'\nimport { evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'\nimport { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'\nimport { HOST_WIDGET_ID, WIDGET_SELECTION } from '../../utils/widgetSelection.js'\nimport { getCurrentUserGroups } from '../../utils/widgetVisibility.js'\nimport { CnRowActions } from '../CnRowActions/index.js'\n\n/**\n * Event-bus channel a page-level refresh is announced on. The page's Actions\n * menu emits it, the manifest `refresh` / `api-call` actions bump it, and an\n * app's own dialog emits it after a successful write.\n */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/**\n * CnObjectListWidget — an abstract, manifest-configured object list / table.\n *\n * Queries OpenRegister objects for a `register` + `schema` with a `filter`,\n * `sort`, and `limit`, then renders the chosen `columns` in a `CnDataTable`.\n * Replaces per-app coded list widgets (closing-soon, recently-won-lost,\n * renewals-due, …) — the data, columns, filter and ordering are all editable\n * through the cog modal (ADR-041). Resolved by its registry type key\n * `object-list`; configured via `CnObjectListWidgetForm`.\n *\n * Example content blob:\n * ```js\n * content: {\n * register: 'pipelinq', schema: 'lead',\n * filter: { status: 'open' },\n * sort: { field: 'expectedCloseDate', dir: 'asc' },\n * limit: 5,\n * columns: [{ key: 'title', label: 'Deal' }, { key: 'value', label: 'Value' }],\n * rowRoute: 'leads-detail',\n * }\n * ```\n */\nexport default {\n\tname: 'CnObjectListWidget',\n\n\tcomponents: { CnDataTable, CnFormDialog, CnPagination, CnWidgetEmptyState, CnRowActions, CnFkResolveCell, NcButton, Plus },\n\n\tmixins: [innerHeading],\n\n\tinject: {\n\t\t/**\n\t\t * Detail-page object context (`{ objectId, object, register, schema }`)\n\t\t * provided by CnDetailPage. Enables `@objectId` / `@object.<field>`\n\t\t * filter tokens so a detail-page list can be scoped to the current\n\t\t * object. Null on dashboards (tokens then pass through unresolved).\n\t\t */\n\t\tcnObjectContext: { default: null },\n\t\t/** The page's row selection (CnDetailPage), for `content.rowSelection`. */\n\t\tcnWidgetSelection: { from: WIDGET_SELECTION, default: null },\n\t\t/** This widget's id on the page (CnDetailWidgetHost), the key it publishes under. */\n\t\tcnHostWidgetId: { from: HOST_WIDGET_ID, default: null },\n\t\t/**\n\t\t * Page-level workspace context (reactive `ref({})`) provided by\n\t\t * CnDashboardPage. Lets `@workspace.<key>` filter tokens resolve so a\n\t\t * list can react to state another widget on the page wrote (e.g. a\n\t\t * client-overview list scoped to the selected client). Null on pages\n\t\t * that don't provide it (tokens then stay unresolved and the list shows\n\t\t * its `promptText`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored `content.emptyText` is run through it for this\n\t\t * component's OWN empty state, and so are `content.prompt` and\n\t\t * `content.addLabel`. Defaults to an identity function so\n\t\t * an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/**\n\t\t * Pre-bound `dispatchAction` provided by CnPageRenderer (router,\n\t\t * registry, handlers and openModal already wired). `rowActions` and\n\t\t * `dropZone` dispatch through it; outside a CnPageRenderer tree the\n\t\t * component falls back to a bare `dispatchAction` call, which reaches\n\t\t * the router-backed types and warns on the registry-backed ones.\n\t\t */\n\t\tcnDispatchAction: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's persisted configuration blob. `limit` is a fetch cap\n\t\t * (default 25) — the rendered row count fits the host cell (ADR-062).\n\t\t * `viewAllRoute` / `viewAllQuery` configure the \"View all (N)\" footer\n\t\t * navigation; `viewAllQuery` values are token-resolved (`@objectId`).\n\t\t *\n\t\t * `extend` is the OpenRegister `_extend[]` list forwarded on the fetch.\n\t\t * It is what makes a DOTTED column key work: `CnDataTable` reads\n\t\t * `informatieobject.title` as a path into the row, and without\n\t\t * `extend: ['informatieobject']` the row holds a uuid string at that\n\t\t * key, so six columns off one referenced object render as six copies\n\t\t * of the same uuid. `fkResolve` is the one-label answer; `extend` is\n\t\t * the several-fields-off-the-same-reference answer.\n\t\t *\n\t\t * `rowActions` is an array in the unified manifest action shape\n\t\t * (`handler` | `open-modal` | `open-page` | `navigate` | …), rendered\n\t\t * per row through CnRowActions. `dropZone` is one action of that same\n\t\t * shape, dispatched when files are dropped on the widget, with the\n\t\t * dropped `File[]` handed to it. `content.upload: false` hides the\n\t\t * click-to-pick button that otherwise renders alongside a `dropZone`.\n\t\t * Neither carries authorization: OpenRegister RBAC is the only\n\t\t * authority over what a write may do.\n\t\t *\n\t\t * `groupBy` (a row field path, dotted paths read the `extend`-inlined\n\t\t * reference the same way a column does) buckets the fetched rows into\n\t\t * one heading + table per distinct value, in first-seen order.\n\t\t * `groupLabel` is the field path used for the heading text; it\n\t\t * defaults to the raw `groupBy` value when absent. `groupLabelResolve`\n\t\t * (`{register, schema, labelField?}`) is for a `groupBy` whose human\n\t\t * label sits a hop further than `extend` reaches (a reference's own\n\t\t * reference): the heading resolves the raw group key through the\n\t\t * shared object store, the same `fkResolve` cell widget a column uses.\n\t\t *\n\t\t * `selectable` turns on CnDataTable's checkbox column; `bulkActions`\n\t\t * (same action shape as `rowActions`) render as buttons in a bar that\n\t\t * appears once something is selected. A `handler` bulk action receives\n\t\t * the array of selected row objects appended to its `args`; an\n\t\t * `open-modal` bulk action receives them as `props.selectedIds`\n\t\t * (mirroring how a drop hands `props.files` to a modal).\n\t\t *\n\t\t * `sortable` (`true`, or an array of column keys) makes the matching\n\t\t * `CnDataTable` headers clickable; clicking re-fetches with that\n\t\t * field's `_order`, same as the fixed `sort` key but user-driven.\n\t\t *\n\t\t * `facet` (`{ field, label? }`) renders one filter chip per distinct\n\t\t * value of that field across the currently loaded rows (an array\n\t\t * field's entries are flattened) and narrows the rendered rows to\n\t\t * those carrying a selected value. Client-side over the fetched page,\n\t\t * like the rest of this widget's row set.\n\t\t *\n\t\t * `createAction` (one action of the `rowActions` shape) replaces the\n\t\t * list's own create form: every Add, here and in the host card's menu,\n\t\t * runs it instead, labelled with its `label`. Its `visibleWhen` is\n\t\t * evaluated against the page record, and while it fails the Add is\n\t\t * hidden and `hiddenNote` (a string, or `[{ when, text }]` where the\n\t\t * first holding entry wins) is shown in its place.\n\t\t *\n\t\t * `summary` (`{ text, counts }`) is a line under the rows: `{total}` is\n\t\t * the number of loaded rows and each `counts` key the number of loaded\n\t\t * rows its `visibleWhen`-shaped condition holds for.\n\t\t *\n\t\t * `hideHeader: true` drops the table's column header row (CnDataTable's\n\t\t * `hideHeader`), for a short list that reads as rows, not as a table.\n\t\t *\n\t\t * `rowIdField` (a dot-path) names the row field holding the id\n\t\t * `rowRoute` opens, for a row that links to another record.\n\t\t * `footnote` is a line under the table, through the host translate.\n\t\t *\n\t\t * `export` (`{ label?, format?, group? }`) offers the filtered list as a\n\t\t * download in the card's header; `group` shows it to one group only.\n\t\t * With `url` (and `params`, tokens such as `@objectId`) the button\n\t\t * downloads that app endpoint instead of OpenRegister's export.\n\t\t *\n\t\t * `endpointSource` (`{ url, params?, responsePath? }`) reads the rows\n\t\t * from an app endpoint instead of OpenRegister, for a list without\n\t\t * `register`/`schema`: rows at `responsePath` (default `results`),\n\t\t * the count at `total`. Such a list is read-only.\n\t\t *\n\t\t * `tableBleed: true` runs the table edge to edge in its card: in the\n\t\t * board look it goes under the card's side padding and its outer cells\n\t\t * take that padding, so the column text lines up with the card title.\n\t\t * Without it the table stays inside the padding.\n\t\t * `headerSubtitle` is a line under the card title with the list's\n\t\t * counts: `{total}` and `{count:<field>=<value>}` (\"{total} vastgelegd\").\n\t\t * `headerNote` takes the same counts beside the card title instead.\n\t\t *\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 * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowIdField?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}, tableBleed?: boolean}}\n\t\t * `children` (`{ register?, schema, filter, columns, reorder, rowActions,\n\t\t * … }`) shows each row's child records under it, as a list of the same\n\t\t * kind: `filter` names the link with an `@row.<field>` token\n\t\t * (`{ courseId: \"@row.id\" }`). The children of every row come from one\n\t\t * query on the list of parent values.\n\t\t *\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 * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowIdField?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}, children?: object}}\n\t\t * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupOrder?: Array<string>, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, rowRouteField?: string, rowRouteMap?: object, rowIdField?: string, rowRouteIdField?: string, createEndpoint?: {url: string, bodyKey?: string, extra?: object}, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean, export?: {label?: string, format?: ('csv'|'excel'), group?: string}}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Rows handed in instead of fetched. The list then reads nothing\n\t\t * itself: where it would read again (after a write, a refused\n\t\t * reorder) it emits `rows-stale` for the owner to hand in new rows.\n\t\t * A `children` list gives each nested list its rows this way, from\n\t\t * one query for all parents.\n\t\t */\n\t\tpresetRows: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped', 'header', 'rows-stale'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The current user's groups once a `footerLinks` entry with `group` asked for them; null before. */\n\t\t\tfooterGroups: null,\n\t\t\t/** What the last reorder did, for the live region: the new place, or the server's refusal. */\n\t\t\treorderStatus: '',\n\t\t\trows: [],\n\t\t\t/** The rows `content.readOnlyRows` adds after the list's own, tagged `_readOnly`. */\n\t\t\treadOnlyRows: [],\n\t\t\tloading: false,\n\t\t\terror: '',\n\t\t\t/** Server-side total for the resolved filter (drives \"View all (N)\"). */\n\t\t\ttotal: 0,\n\t\t\t/** The card header's Download action once `content.export` offered it; null before. */\n\t\t\theaderAction: null,\n\t\t\t/** Whether the header was last told a counts line (`content.headerSubtitle`), so an empty one clears it. */\n\t\t\theaderSubtitleShown: false,\n\t\t\t/** Current 1-based page. Paging is SERVER-side (`_page`). */\n\t\t\tpage: 1,\n\t\t\t/** Rows that fit the host cell; null = unconstrained (dashboards). */\n\t\t\tfitRows: null,\n\t\t\t/**\n\t\t\t * How far the table has been squeezed to the widget's width, so the\n\t\t\t * card never scrolls sideways: 0 = its own auto layout, 1 = one column\n\t\t\t * takes what is left and is cut with an ellipsis while the rest keep\n\t\t\t * their content on one line, 2 = a fixed layout, every column an equal\n\t\t\t * share with its text wrapped. Each step is taken only while the table\n\t\t\t * still scrolls sideways. `content.fitWidth` forces stage 1 on or off.\n\t\t\t */\n\t\t\tfitStage: 0,\n\t\t\t/** Whether the create dialog is open. */\n\t\t\tshowCreate: false,\n\t\t\t/** The row the form edits (`content.editable`), or null while it creates. */\n\t\t\teditingRow: null,\n\t\t\t/**\n\t\t\t * The list's own schema definition, once fetched.\n\t\t\t *\n\t\t\t * Shared by two readers: the create dialog renders its form from it,\n\t\t\t * and `resolvedColumns` takes a column's HEADING from the matching\n\t\t\t * property's `title` when the manifest gave only a key.\n\t\t\t */\n\t\t\tcreateSchema: null,\n\t\t\t/**\n\t\t\t * Depth of nested dragenter/dragleave pairs over the widget.\n\t\t\t * A single counter, because `dragleave` fires when the pointer\n\t\t\t * crosses into a CHILD element: tracking a boolean instead makes\n\t\t\t * the overlay flicker off the moment the drag reaches the table.\n\t\t\t */\n\t\t\tdragDepth: 0,\n\t\t\t/** Selected row ids (`content.selectable`). Shared across groups. */\n\t\t\tselectedIds: [],\n\t\t\t/**\n\t\t\t * The active sort, seeded from `content.sort` and overwritten by an\n\t\t\t * interactive header click (`content.sortable`). Kept as component\n\t\t\t * state rather than read from `content` directly so a click can\n\t\t\t * override the manifest-declared default without mutating a prop.\n\t\t\t */\n\t\t\tlocalSort: { ...(this.content.sort || {}) },\n\t\t\t/** The column-head filters (`content.filterable`), `{ paramKey: values[] }`. */\n\t\t\theaderFilters: {},\n\t\t\t/** Facet values currently toggled on (`content.facet`). */\n\t\t\tfacetSelected: [],\n\t\t\t/** Child rows per parent row id (`content.children`). */\n\t\t\tchildRows: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The class that marks the selected row (`content.rowSelection`), or\n\t\t * null when this list offers no row selection.\n\t\t *\n\t\t * @return {((row: object) => string)|null} The class per row.\n\t\t * @spec openspec/changes/side-card-follows-row/specs/side-card-follows-row/spec.md#requirement-a-list-publishes-its-rows-and-its-selected-row\n\t\t */\n\t\tselectionRowClass() {\n\t\t\tif (this.content.rowSelection !== true || !this.cnWidgetSelection || typeof this.cnHostWidgetId !== 'function') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst selected = this.cnWidgetSelection.entry(this.cnHostWidgetId()).selected\n\t\t\tconst idOf = (row) => String((row && (row.id ?? row['@self']?.id)) ?? '')\n\t\t\treturn (row) => (selected && idOf(row) !== '' && idOf(row) === idOf(selected) ? 'cn-object-list-widget__row--selected' : '')\n\t\t},\n\n\t\t/**\n\t\t * How the table draws its title column (`content.rowTitle`: link, plain,\n\t\t * text or strong), link when unset or unknown.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/board-log-table-plain-cells/specs/board-log-table-plain-cells/spec.md#requirement-a-tables-title-column-can-read-as-plain-text\n\t\t */\n\t\trowTitle() {\n\t\t\treturn ['plain', 'text', 'strong'].includes(this.content.rowTitle) ? this.content.rowTitle : 'link'\n\t\t},\n\n\t\t/**\n\t\t * Whether the table fits the widget's width (stage 1): `content.fitWidth`\n\t\t * when it says, else once the columns were measured not to fit.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfitWidthActive() {\n\t\t\tconst forced = this.content?.fitWidth\n\t\t\treturn typeof forced === 'boolean' ? forced : this.fitStage === 1\n\t\t},\n\n\t\t/**\n\t\t * Whether the table takes a fixed layout, the last resort when even the\n\t\t * fitted table is wider than the widget (not with `content.fitWidth` set).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfixedLayoutActive() {\n\t\t\treturn typeof this.content?.fitWidth !== 'boolean' && this.fitStage >= 2\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped detail-page object context for token resolution, or null\n\t\t * on surfaces (dashboards) that don't provide one.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tobjectCtx() {\n\t\t\tconst c = this.cnObjectContext\n\t\t\tif (!c) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn (typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace context bag (or null). Vue 2.7 `setup`/inject\n\t\t * may hand back a raw ref; unwrap `.value` so token resolution reads the\n\t\t * plain object.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tworkspaceCtx() {\n\t\t\tconst c = this.cnWorkspaceContext\n\t\t\tif (!c) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn (typeof c === 'object' && 'value' in c) ? c.value : c\n\t\t},\n\n\t\t/**\n\t\t * Token-resolution context merged from the detail-page object context\n\t\t * and the page-level workspace bag (`@workspace.<key>`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\ttokenCtx() {\n\t\t\tconst base = this.objectCtx ? { ...this.objectCtx } : {}\n\t\t\tbase.workspace = this.workspaceCtx || {}\n\t\t\treturn base\n\t\t},\n\n\t\t/**\n\t\t * The filter with every `@`-token resolved against `tokenCtx`, then with\n\t\t * any UNRESOLVED OPTIONAL token (`@workspace.<key>?`) dropped — so an\n\t\t * optional queue filter simply shows all rows until a queue is picked,\n\t\t * while a REQUIRED token (`@workspace.selectedClient`) stays to trigger the\n\t\t * prompt.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedFilter() {\n\t\t\treturn dropOptionalUnresolved(resolveFilterTokens(this.content.filter || {}, this.tokenCtx))\n\t\t},\n\n\t\t/**\n\t\t * Whether a context-dependent filter token (e.g. `@workspace.selectedClient`)\n\t\t * is still unresolved — the page state this list depends on isn't set yet,\n\t\t * so the list renders a prompt instead of fetching the whole register.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\twaitingForContext() {\n\t\t\treturn hasUnresolvedTokens(this.resolvedFilter)\n\t\t},\n\n\t\t/**\n\t\t * Prompt shown while a context-bound list has an unresolved REQUIRED\n\t\t * token. A `content.prompt` override always wins. Otherwise the default\n\t\t * is context-aware: the \"Select an item to see related records\"\n\t\t * master-detail copy only fits a DASHBOARD list waiting on a selection —\n\t\t * on a detail page (an object context is present) that copy is wrong, so\n\t\t * fall back to the neutral \"Nothing here yet\" (ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tpromptText() {\n\t\t\tif (this.content.prompt) {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(this.content.prompt)\n\t\t\t}\n\t\t\treturn this.objectCtx\n\t\t\t\t? t('nextcloud-vue', 'Nothing here yet')\n\t\t\t\t: t('nextcloud-vue', 'Select an item to see related records')\n\t\t},\n\n\t\t/**\n\t\t * Quiet, status-code-free label shown when a fetch fails. The real\n\t\t * axios error is logged to the console, never rendered (ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tloadErrorLabel() {\n\t\t\treturn this.content.errorText || t('nextcloud-vue', 'Could not load these records')\n\t\t},\n\n\t\t/**\n\t\t * Column definitions normalised for CnDataTable. A string column becomes\n\t\t * `{ key, label }`; an object column keeps its key/label AND carries the\n\t\t * presentation hints CnDataTable forwards to CnCellRenderer — `format`\n\t\t * (currency / duration / number / percent / date / date-time), `widget`\n\t\t * (badge / link / swatch), `widgetProps`, `formatter`, `align`, and\n\t\t * `width`. Without this pass-through a `relatedCollections` column's\n\t\t * `format: 'currency'` / `'date'` would silently render as plain text.\n\t\t *\n\t\t * `enumLabels` travels alongside `enum` for the same reason `enum`\n\t\t * itself is here: a list column often addresses a DOTTED path into an\n\t\t * `extend`-inlined reference (e.g. `informatieobject.direction`), and\n\t\t * this widget never fetches that reference's own schema to feed\n\t\t * CnDataTable's live `:schema` prop — only `openCreate()`'s create-form\n\t\t * schema is ever fetched. A column that needs its raw enum code\n\t\t * (`\"incoming\"`) to render as a human label (`\"Incoming\"`) has to carry\n\t\t * that mapping itself, the same way it already carries `enum`. This is\n\t\t * the same generic mechanism CnCellRenderer already applies to a real\n\t\t * schema property's `x-enum-labels`, completed here for the\n\t\t * manifest-column path rather than special-cased per column.\n\t\t */\n\t\tresolvedColumns() {\n\t\t\tconst cols = Array.isArray(this.content.columns) ? this.content.columns : []\n\t\t\tconst sortableConfig = this.content.sortable\n\t\t\t// A column may arrive as a bare key. Falling back to that key as the\n\t\t\t// HEADING is what printed `customerName` and `dateReceived` at users\n\t\t\t// on a dashboard, while the index page beside it said \"Customer name\"\n\t\t\t// and \"Date received\" — the same data, labelled twice, differently.\n\t\t\t// The schema already holds the answer, so ask it, and keep the key\n\t\t\t// only when it has nothing to say.\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst headingFor = (key) => {\n\t\t\t\tconst prop = props[key]\n\t\t\t\tconst title = prop && typeof prop.title === 'string' ? prop.title.trim() : ''\n\t\t\t\treturn title !== '' ? title : key\n\t\t\t}\n\t\t\t// CnDataTable gets no `:schema` here, so a column takes the\n\t\t\t// schema property's `format` and enum (with its labels) itself,\n\t\t\t// unless it says its own or draws a widget or formatter.\n\t\t\tconst withSchemaPresentation = (out) => {\n\t\t\t\tconst prop = props[out.key]\n\t\t\t\t// The `label` widget writes the enum's label, so it still takes the enum.\n\t\t\t\tif (!prop || (out.widget !== undefined && out.widget !== 'label') || out.formatter !== undefined) {\n\t\t\t\t\treturn out\n\t\t\t\t}\n\t\t\t\tif (out.format === undefined && typeof prop.format === 'string' && prop.format !== '') {\n\t\t\t\t\tout.format = prop.format\n\t\t\t\t}\n\t\t\t\tif (out.enum === undefined && Array.isArray(prop.enum) && prop.enum.length > 0) {\n\t\t\t\t\tout.enum = prop.enum\n\t\t\t\t\tconst labels = prop.enumLabels || prop['x-enum-labels']\n\t\t\t\t\tif (out.enumLabels === undefined && labels && typeof labels === 'object') {\n\t\t\t\t\t\tout.enumLabels = labels\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t}\n\t\t\tconst mapped = cols.map((c) => {\n\t\t\t\tif (typeof c === 'string') {\n\t\t\t\t\treturn withSchemaPresentation({ key: c, label: headingFor(c) })\n\t\t\t\t}\n\t\t\t\tconst out = { key: c.key, label: c.label || headingFor(c.key) }\n\t\t\t\t// `secondary` (a second, muted line under the cell) rides\n\t\t\t\t// along too: the table draws it, and dropping it here took the\n\t\t\t\t// list rows' second line away (zuiddrecht-pixel-gaps-3).\n\t\t\t\tfor (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass', 'emptyValue']) {\n\t\t\t\t\tif (c[k] !== undefined) {\n\t\t\t\t\t\tout[k] = c[k]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\twithSchemaPresentation(out)\n\t\t\t\t// `content.sortable` (bool = every column, array = the listed\n\t\t\t\t// keys) fills in `sortable` for a column that did not already\n\t\t\t\t// say so on its own definition — an explicit `sortable: false`\n\t\t\t\t// on the column still wins, so one header can opt out of an\n\t\t\t\t// otherwise sortable list.\n\t\t\t\tif (out.sortable === undefined && sortableConfig !== undefined) {\n\t\t\t\t\tout.sortable = sortableConfig === true\n\t\t\t\t\t\t|| (Array.isArray(sortableConfig) && sortableConfig.includes(out.key))\n\t\t\t\t}\n\t\t\t\treturn out\n\t\t\t})\n\n\t\t\t// The registry used to default to `[{ key: 'title' }]`, but plenty of schemas\n\t\t\t// have no `title` — a Barn has `name`. Such a widget rendered a table of\n\t\t\t// em-dashes forever, and nothing told the user why.\n\t\t\t//\n\t\t\t// Don't guess the right key from the data: OpenRegister already guessed, and\n\t\t\t// published the answer as `@self.name`. When NOT ONE configured column\n\t\t\t// resolves on any loaded row, fall back to that single display-name column.\n\t\t\t//\n\t\t\t// Only when none resolve: a partly-matching column set is a deliberate choice\n\t\t\t// (a column can be legitimately empty across the current page of rows), and\n\t\t\t// silently replacing it would override the user.\n\t\t\tif (this.rows.length > 0 && mapped.length > 0) {\n\t\t\t\tconst resolves = mapped.some((c) => this.rows.some((r) => objectFieldValue(r, c.key) !== undefined))\n\t\t\t\tif (resolves === false) {\n\t\t\t\t\treturn [{ key: 'name', label: t('nextcloud-vue', 'Name') }]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn mapped\n\t\t},\n\n\t\t/**\n\t\t * Declared per-row actions mapped onto the CnRowActions shape. Every\n\t\t * entry keeps its label, icon and `destructive` flag and routes its\n\t\t * click back through `runRowAction`, so the dispatcher decides what a\n\t\t * type means and this component does not grow a second action\n\t\t * vocabulary.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmappedRowActions() {\n\t\t\tconst declared = Array.isArray(this.content.rowActions) ? this.content.rowActions : []\n\t\t\tconst mapped = declared\n\t\t\t\t.filter((a) => a && typeof a === 'object')\n\t\t\t\t.map((action) => ({\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\ticon: action.icon,\n\t\t\t\t\tdestructive: action.destructive === true,\n\t\t\t\t\t// CnRowActions renders a link when these are set and skips the handler.\n\t\t\t\t\t...this.rowActionLink(action),\n\t\t\t\t\thandler: (row) => this.runRowAction(action, row),\n\t\t\t\t}))\n\t\t\tif (this.content.editable === true) {\n\t\t\t\tmapped.unshift({ label: t('nextcloud-vue', 'Edit'), icon: 'Pencil', handler: (row) => this.openEdit(row) })\n\t\t\t}\n\t\t\tif (this.content.deletable === true) {\n\t\t\t\tmapped.push({ label: t('nextcloud-vue', 'Delete'), icon: 'TrashCanOutline', destructive: true, handler: (row) => this.deleteRow(row) })\n\t\t\t}\n\t\t\tif (this.reorderField === '') {\n\t\t\t\treturn mapped\n\t\t\t}\n\t\t\t// The keyboard half of the drag: one place up or down.\n\t\t\treturn [\n\t\t\t\t...mapped,\n\t\t\t\t{ label: t('nextcloud-vue', 'Move up'), icon: 'ArrowUp', handler: (row) => this.moveRowBy(row, -1) },\n\t\t\t\t{ label: t('nextcloud-vue', 'Move down'), icon: 'ArrowDown', handler: (row) => this.moveRowBy(row, 1) },\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * The field a reorderable list is ordered by (`content.reorder.field`),\n\t\t * or '' when the list does not reorder. A grouped list does not.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\treorderField() {\n\t\t\tconst reorder = this.content && this.content.reorder\n\t\t\tconst field = reorder && typeof reorder === 'object' ? reorder.field : ''\n\t\t\treturn typeof field === 'string' && field !== '' && !this.isGrouped ? field : ''\n\t\t},\n\n\t\t/**\n\t\t * The declared child list (`content.children`), or null. A grouped\n\t\t * list shows no children.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildrenConfig() {\n\t\t\tconst children = this.content && this.content.children\n\t\t\tif (!children || typeof children !== 'object' || Array.isArray(children) || !children.schema || this.isGrouped) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn children\n\t\t},\n\n\t\t/**\n\t\t * How a child points at its row: the child `filter` key whose value\n\t\t * is an `@row.<field>` token, and that row field. Null without one.\n\t\t *\n\t\t * @return {{key: string, field: string}|null}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildLink() {\n\t\t\tconst filter = (this.childrenConfig && this.childrenConfig.filter) || {}\n\t\t\tfor (const [key, value] of Object.entries(filter)) {\n\t\t\t\tconst field = rowTokenField(value)\n\t\t\t\tif (field !== '') {\n\t\t\t\t\treturn { key, field }\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The declared drop-zone action, or null. A widget without the key\n\t\t * takes no part in a drag at all.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tdropZoneAction() {\n\t\t\tconst dz = this.content.dropZone\n\t\t\treturn (dz && typeof dz === 'object') ? dz : null\n\t\t},\n\n\t\t/** Whether a file drag is currently over a drop-enabled widget. */\n\t\tdropping() {\n\t\t\treturn this.dropZoneAction !== null && this.dragDepth > 0\n\t\t},\n\n\t\t/** Copy shown on the drop overlay (overridable via `dropZone.label`). */\n\t\tdropLabel() {\n\t\t\treturn (this.dropZoneAction && this.dropZoneAction.label)\n\t\t\t\t|| t('nextcloud-vue', 'Drop files here')\n\t\t},\n\n\t\t/**\n\t\t * Whether the click-to-pick upload button renders: a `dropZone` is\n\t\t * declared and the host has not opted out with `content.upload: false`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowUploadButton() {\n\t\t\treturn this.dropZoneAction !== null && this.content.upload !== false\n\t\t},\n\n\t\t/** Pre-translated Upload label (overridable via `content.uploadLabel`). */\n\t\tuploadLabel() {\n\t\t\treturn this.content.uploadLabel || t('nextcloud-vue', 'Upload')\n\t\t},\n\n\t\t/**\n\t\t * Declared bulk actions mapped onto the same `{label, icon, destructive,\n\t\t * handler}` shape `mappedRowActions` uses, so CnRowActions and the bulk\n\t\t * bar render identically — the only difference is what `handler` does:\n\t\t * a row action dispatches with one row, a bulk action with the whole\n\t\t * selection.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tmappedBulkActions() {\n\t\t\tconst declared = Array.isArray(this.content.bulkActions) ? this.content.bulkActions : []\n\t\t\treturn declared\n\t\t\t\t.filter((a) => a && typeof a === 'object')\n\t\t\t\t.map((action) => ({\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\ticon: action.icon,\n\t\t\t\t\tdestructive: action.destructive === true,\n\t\t\t\t\thandler: () => this.runBulkAction(action),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The currently selected row objects, resolved from `rows` (not just\n\t\t * the visible/faceted slice) so a selection made before a facet filter\n\t\t * narrows the view is not silently dropped from a bulk action.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tselectedRows() {\n\t\t\treturn this.rows.filter((row) => this.selectedIds.includes(row.id))\n\t\t},\n\n\t\t/**\n\t\t * Distinct values of `content.facet.field` across the currently loaded\n\t\t * rows, flattening an array-valued field (the \"keywords across the\n\t\t * dossier\" shape this seam was measured against). Sorted for a stable\n\t\t * chip order run to run.\n\t\t *\n\t\t * @return {Array<string>}\n\t\t */\n\t\tfacetOptions() {\n\t\t\tconst facet = this.content.facet\n\t\t\tif (!facet || !facet.field) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst values = new Set()\n\t\t\tfor (const row of this.visibleRows) {\n\t\t\t\tconst raw = objectFieldValue(row, facet.field)\n\t\t\t\tconst list = Array.isArray(raw) ? raw : (raw !== undefined && raw !== null && raw !== '' ? [raw] : [])\n\t\t\t\tfor (const v of list) {\n\t\t\t\t\tif (v !== undefined && v !== null && v !== '') {\n\t\t\t\t\t\tvalues.add(v)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Array.from(values).sort()\n\t\t},\n\n\t\t/** Pre-translated facet label (`content.facet.label`, falls back to a generic \"Filter\"). */\n\t\tfacetLabel() {\n\t\t\tconst facet = this.content.facet\n\t\t\treturn (facet && facet.label) || t('nextcloud-vue', 'Filter')\n\t\t},\n\n\t\t/**\n\t\t * Rows narrowed by the selected facet values. A row matches when at\n\t\t * least one of its (possibly array) values at `facet.field` is among\n\t\t * `facetSelected` — no selection means no narrowing at all.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tfacetedRows() {\n\t\t\tconst facet = this.content.facet\n\t\t\tif (!facet || !facet.field || this.facetSelected.length === 0) {\n\t\t\t\treturn this.visibleRows\n\t\t\t}\n\t\t\treturn this.visibleRows.filter((row) => {\n\t\t\t\tconst raw = objectFieldValue(row, facet.field)\n\t\t\t\tconst list = Array.isArray(raw) ? raw : (raw !== undefined && raw !== null ? [raw] : [])\n\t\t\t\treturn list.some((v) => this.facetSelected.includes(v))\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the fetch returned rows but the active facet selection\n\t\t * matches none of them — the \"clear filter\" empty state, distinct\n\t\t * from \"there are no items at all\".\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowNoFacetMatchState() {\n\t\t\treturn !this.loading && this.rows.length > 0 && this.facetSelected.length > 0 && this.facetedRows.length === 0\n\t\t},\n\n\t\t/** Whether `content.groupBy` is declared, so the template picks the grouped render path. */\n\t\tisGrouped() {\n\t\t\treturn typeof this.content.groupBy === 'string' && this.content.groupBy !== ''\n\t\t},\n\n\t\t/**\n\t\t * `content.groupLabelResolve` (`{register, schema, labelField?}`), or\n\t\t * null. When set, the group heading resolves `group.key` through the\n\t\t * shared object store instead of reading a plain field off the row —\n\t\t * for a `groupBy` whose human label lives a hop further than `extend`\n\t\t * inlined (a reference's own reference, e.g. grouping documents by\n\t\t * type when only the document itself, not its type, was extended).\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tgroupLabelResolveConfig() {\n\t\t\tconst cfg = this.content.groupLabelResolve\n\t\t\treturn (cfg && typeof cfg === 'object' && cfg.register && cfg.schema) ? cfg : null\n\t\t},\n\n\t\t/**\n\t\t * The faceted rows bucketed by `content.groupBy`, one entry per\n\t\t * distinct value: the keys `content.groupOrder` lists first, in that\n\t\t * order, then the others in first-seen order (matching how the rows\n\t\t * already arrived — server-sorted). A `$ref` group key that resolves to an\n\t\t * object (an `extend`-inlined reference) groups by its `id`, so\n\t\t * `groupBy: 'informatieobjecttype'` groups the actual referenced\n\t\t * objects rather than one group per string-ified object.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, rows: Array<object>}>}\n\t\t */\n\t\tgroupedRows() {\n\t\t\tif (!this.isGrouped) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst field = this.content.groupBy\n\t\t\tconst labelField = this.content.groupLabel || field\n\t\t\tconst order = []\n\t\t\tconst byKey = new Map()\n\t\t\tfor (const row of this.facetedRows) {\n\t\t\t\tconst rawKey = objectFieldValue(row, field)\n\t\t\t\tconst key = (rawKey && typeof rawKey === 'object')\n\t\t\t\t\t? (rawKey.id || JSON.stringify(rawKey))\n\t\t\t\t\t: String(rawKey ?? '')\n\t\t\t\tif (!byKey.has(key)) {\n\t\t\t\t\tconst rawLabel = objectFieldValue(row, labelField)\n\t\t\t\t\tconst label = (rawLabel && typeof rawLabel === 'object')\n\t\t\t\t\t\t? (rawLabel.title || rawLabel.name || key)\n\t\t\t\t\t\t: (rawLabel || key || t('nextcloud-vue', 'Ungrouped'))\n\t\t\t\t\tbyKey.set(key, { key, label, rows: [] })\n\t\t\t\t\torder.push(key)\n\t\t\t\t}\n\t\t\t\tbyKey.get(key).rows.push(row)\n\t\t\t}\n\t\t\tconst first = Array.isArray(this.content.groupOrder) ? this.content.groupOrder.map(String) : []\n\t\t\tconst rank = (key) => (first.includes(key) ? first.indexOf(key) : first.length)\n\t\t\treturn order\n\t\t\t\t.map((key, index) => ({ key, index }))\n\t\t\t\t.sort((a, b) => rank(a.key) - rank(b.key) || a.index - b.index)\n\t\t\t\t.map(({ key }) => byKey.get(key))\n\t\t},\n\n\t\t/** Empty-state text (overridable via `content.emptyText`). */\n\t\temptyText() {\n\t\t\treturn this.content.emptyText || t('nextcloud-vue', 'No items')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text run through the host translate function. Used\n\t\t * only by this component's OWN empty state — the raw `emptyText`\n\t\t * still goes to CnDataTable, which translates at its own render\n\t\t * boundary, so the string is never translated twice.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedEmptyText() {\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn this.emptyText ? fn(this.emptyText) : this.emptyText\n\t\t},\n\n\t\t/**\n\t\t * `content.footerLinks` (`[{ label, route?, query?, href?, note?, group? }]`)\n\t\t * ready to draw: labels and notes through the host translate, a `route`\n\t\t * with its token-resolved `query` as a router location and its href, an\n\t\t * `href` as given. A link with `group` shows only to that group's\n\t\t * members, and not before the groups are known.\n\t\t *\n\t\t * @spec openspec/changes/object-list-footer-links/specs/object-list/spec.md#requirement-an-object-list-can-end-with-a-row-of-links\n\t\t * @return {Array<{key: string, label: string, note: string, href: string, location: object|null}>}\n\t\t */\n\t\tfooterLinks() {\n\t\t\tconst links = Array.isArray(this.content.footerLinks) ? this.content.footerLinks : []\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\tconst out = []\n\t\t\tlinks.forEach((link, index) => {\n\t\t\t\tif (!link || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (typeof link.group === 'string' && link.group !== '' && !(this.footerGroups || []).includes(link.group)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst location = typeof link.route === 'string' && link.route !== ''\n\t\t\t\t\t? { name: link.route, query: resolveFilterTokens(link.query || {}, this.tokenCtx) }\n\t\t\t\t\t: null\n\t\t\t\tconst href = location ? resolveHref(location, this.$router) : (typeof link.href === 'string' ? link.href : '')\n\t\t\t\tif (!location && href === '') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: `link-${index}`,\n\t\t\t\t\tlabel: fn(link.label),\n\t\t\t\t\tnote: typeof link.note === 'string' && link.note !== '' ? fn(link.note) : '',\n\t\t\t\t\thref,\n\t\t\t\t\tlocation,\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * `content.footnote` run through the host translate function, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedFootnote() {\n\t\t\tconst note = typeof this.content.footnote === 'string' ? this.content.footnote : ''\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn note ? fn(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * The rows actually rendered: capped to what fits the host grid cell\n\t\t * (ADR-062 — content adapts to the cell, never a nested scrollbar).\n\t\t * Unconstrained on surfaces without a fixed-height cell (dashboards\n\t\t * measure null and render every fetched row, as before).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tvisibleRows() {\n\t\t\tconst rows = this.readOnlyRows.length > 0 ? [...this.rows, ...this.readOnlyRows] : this.rows\n\t\t\treturn this.fitRows ? rows.slice(0, this.fitRows) : rows\n\t\t},\n\n\t\t/**\n\t\t * Rows per page. This is `content.limit` (the fetch cap) — a FIXED\n\t\t * number, deliberately not the measured `fitRows`: a page size derived\n\t\t * from a post-render measurement would change the pagination under the\n\t\t * reader every time the tile resized, and would need a refetch to\n\t\t * settle.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\tpageSize() {\n\t\t\tif (this.presetRows !== null) {\n\t\t\t\treturn Math.max(this.presetRows.length, 1)\n\t\t\t}\n\t\t\treturn Number((this.content || {}).limit) || 25\n\t\t},\n\n\t\t/**\n\t\t * Total pages for the resolved filter, from the SERVER's total.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\ttotalPages() {\n\t\t\treturn Math.max(Math.ceil((this.total || 0) / this.pageSize), 1)\n\t\t},\n\n\t\t/**\n\t\t * Whether the compact pager renders.\n\t\t *\n\t\t * Two conditions, and the second is the load-bearing one. There must\n\t\t * be more than one page (otherwise there is nothing to page), AND the\n\t\t * cell must be able to show a whole page — because `visibleRows` clips\n\t\t * the page to what fits, and a pager over clipped rows would claim to\n\t\t * be showing \"1–25 of 137\" while five rows are on screen. On a cell\n\t\t * that short the existing fit-to-cell \"View all\" is the honest answer.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowPager() {\n\t\t\tif (this.totalPages <= 1) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn this.fitRows === null || this.fitRows >= this.rows.length\n\t\t},\n\n\t\t/** Whether the designed empty state is what the widget is showing. */\n\t\tshowingEmptyState() {\n\t\t\treturn !this.waitingForContext && !this.error && !this.loading && this.rows.length === 0 && this.readOnlyRows.length === 0\n\t\t},\n\n\t\t/**\n\t\t * How many matching objects are NOT rendered (server total minus the\n\t\t * visible slice). Drives the \"View all (N)\" footer.\n\t\t *\n\t\t * @return {number}\n\t\t */\n\t\thiddenCount() {\n\t\t\treturn Math.max((this.total || this.rows.length) - this.visibleRows.length, 0)\n\t\t},\n\n\t\t/** Pre-translated \"View all (N)\" footer label. */\n\t\tviewAllLabel() {\n\t\t\treturn t('nextcloud-vue', 'View all ({total})', { total: this.total || this.rows.length })\n\t\t},\n\n\t\t/**\n\t\t * The \"View all (N)\" router location: `viewAllRoute` with its\n\t\t * token-resolved `viewAllQuery`, or null without a route.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tviewAllLocation() {\n\t\t\tconst route = this.content.viewAllRoute\n\t\t\tif (!route) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn { name: route, query: resolveFilterTokens(this.content.viewAllQuery || {}, this.tokenCtx) }\n\t\t},\n\n\t\t/**\n\t\t * The href \"View all (N)\" links to, or '' when there is no router to\n\t\t * resolve it (the footer then renders as a button).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tviewAllHref() {\n\t\t\treturn resolveHref(this.viewAllLocation, this.$router)\n\t\t},\n\n\t\t/** Pre-translated \"+N more\" footer label (no viewAllRoute configured). */\n\t\tmoreLabel() {\n\t\t\treturn t('nextcloud-vue', '+{count} more', { count: this.hiddenCount })\n\t\t},\n\n\t\t/** Whether the create affordance renders (on by default; `content.allowCreate: false` opts out). */\n\t\tallowCreate() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (this.createState.action) {\n\t\t\t\treturn c.allowCreate !== false && this.createState.visible\n\t\t\t}\n\t\t\treturn c.allowCreate !== false && Boolean(c.register) && Boolean(c.schema)\n\t\t},\n\n\t\t/**\n\t\t * The declared `content.createAction` against the page record: the\n\t\t * action, whether it shows, and the untranslated note when it does not.\n\t\t *\n\t\t * @return {{action: (object|null), visible: boolean, note: string}}\n\t\t */\n\t\tcreateState() {\n\t\t\treturn createActionState(this.content, this.objectCtx ? this.objectCtx.object : null)\n\t\t},\n\n\t\t/**\n\t\t * Why the create action is withheld, through the host translate, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tcreateHiddenNote() {\n\t\t\tconst note = this.createState.note\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn note ? fn(note) : ''\n\t\t},\n\n\t\t/**\n\t\t * `content.summary` filled in: `{total}` is the number of loaded rows\n\t\t * and every `counts` key the number of loaded rows its condition holds\n\t\t * for. The text goes through the host translate first, so a catalogue\n\t\t * entry keeps its placeholders.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * `content.headerSubtitle` filled in (`{total}`, `{count:field=value}`),\n\t\t * or '' without one.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\theaderSubtitleText() {\n\t\t\treturn this.countLine(this.content.headerSubtitle)\n\t\t},\n\n\t\t/**\n\t\t * `content.headerNote` filled in like `headerSubtitle`, for beside the\n\t\t * card title (\"4 vastgelegd\"), or '' without one.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/object-list-header-note/specs/object-list-header-note/spec.md#requirement-an-object-list-can-say-its-counts-beside-its-card-title\n\t\t */\n\t\theaderNoteText() {\n\t\t\treturn this.countLine(this.content.headerNote)\n\t\t},\n\n\t\tresolvedSummary() {\n\t\t\tconst summary = this.content.summary\n\t\t\tif (!summary || typeof summary.text !== 'string' || summary.text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\tconst values = { total: this.rows.length }\n\t\t\tconst counts = summary.counts && typeof summary.counts === 'object' ? summary.counts : {}\n\t\t\tfor (const [key, cond] of Object.entries(counts)) {\n\t\t\t\tvalues[key] = this.rows.filter((row) => evaluateVisibleWhenLocal(cond, row)).length\n\t\t\t}\n\t\t\treturn fn(summary.text).replace(/\\{(\\w+)\\}/g, (match, key) => (key in values ? String(values[key]) : match))\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for the create dialog (`content.formSize`).\n\t\t *\n\t\t * The widget fetches the WHOLE schema to build this form, so a list\n\t\t * scoped to a handful of columns still opened a create dialog asking\n\t\t * every property the schema declares — at `normal` width, in one\n\t\t * column. A case schema with thirty visible properties is unusable\n\t\t * that way, and every consumer that hit it worked around it by\n\t\t * turning the create button off.\n\t\t */\n\t\tformSize() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn c.formSize || 'normal'\n\t\t},\n\n\t\t/** How many columns the create dialog flows its fields into (`content.formColumns`). */\n\t\tformColumns() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn c.formColumns === 2 ? 2 : 1\n\t\t},\n\n\t\t/** Whitelist of fields the create dialog asks for (`content.formIncludeFields`). Null means all. */\n\t\tformIncludeFields() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn Array.isArray(c.formIncludeFields) ? c.formIncludeFields : null\n\t\t},\n\n\t\t/** Fields the create dialog leaves out (`content.formExcludeFields`). */\n\t\tformExcludeFields() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn Array.isArray(c.formExcludeFields) ? c.formExcludeFields : []\n\t\t},\n\n\t\t/**\n\t\t * Create-form seed values: the list's scalar filter values (the parent\n\t\t * this list is scoped to, e.g. `{ lead: '<uuid>' }`), limited to keys\n\t\t * the schema declares, so the new row shows its parent already chosen.\n\t\t *\n\t\t * `content.createDefaults` merges over it for values a filter cannot express.\n\t\t *\n\t\t * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md\n\t\t * @spec openspec/changes/object-list-create-with-initial-data/tasks.md#task-1\n\t\t * @return {object}\n\t\t */\n\t\tcreateInitialData() {\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst out = { ...this.filterSeed }\n\t\t\t// A new row of a reorderable list goes last.\n\t\t\tif (this.reorderField !== '' && this.reorderField in props) {\n\t\t\t\tout[this.reorderField] = Math.max(this.total, this.rows.length) + 1\n\t\t\t}\n\t\t\tconst declared = this.content && this.content.createDefaults\n\t\t\t// Tokens resolve as the filter's do: `@objectId` is the page's object.\n\t\t\tconst defaults = declared && typeof declared === 'object' && !Array.isArray(declared) ? resolveFilterTokens(declared, this.tokenCtx) : null\n\t\t\tif (defaults) {\n\t\t\t\tfor (const [key, value] of Object.entries(defaults)) {\n\t\t\t\t\tif (key in props && value !== undefined) {\n\t\t\t\t\t\tout[key] = value\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The scalar, schema-declared values of the resolved filter: the parent\n\t\t * this list is scoped to. Operator keys (`deadline[lt]`), keys the schema\n\t\t * does not declare and unresolved `@`-tokens are dropped.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tfilterSeed() {\n\t\t\tconst props = (this.createSchema && this.createSchema.properties) || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.resolvedFilter || {})) {\n\t\t\t\tif (!(key in props) || value === null || value === undefined || value === '' || typeof value === 'object') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (typeof value === 'string' && value.charAt(0) === '@') {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tout[key] = value\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The parent keys seeded from the filter are locked (read-only with\n\t\t * their label): a row added to this list belongs to the record the list\n\t\t * is scoped to. `content.lockFilterFields: false` leaves them editable.\n\t\t * `createDefaults` values are never locked.\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tcreateLockedFields() {\n\t\t\tif (this.content && this.content.lockFilterFields === false) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn Object.keys(this.filterSeed)\n\t\t},\n\n\t\t/** Per-field overrides for the create dialog (`content.formFieldOverrides`). */\n\t\tformFieldOverrides() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn (c.formFieldOverrides && typeof c.formFieldOverrides === 'object') ? c.formFieldOverrides : {}\n\t\t},\n\n\t\t/**\n\t\t * The Add label: `content.addLabel` through the app's translate\n\t\t * function, else the library's \"Add\".\n\t\t *\n\t\t * @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-list-add-label/spec.md\n\t\t * @return {string}\n\t\t */\n\t\taddLabel() {\n\t\t\t// `content.addLabel` is manifest copy in the APP's catalogue, so it\n\t\t\t// goes through the host translate function like `emptyText` and\n\t\t\t// `prompt`. It used to print raw, so a Dutch user read the English\n\t\t\t// manifest string. A label an app already translated has no key\n\t\t\t// of its own in the catalogue and comes back unchanged.\n\t\t\tconst action = this.createState.action\n\t\t\tif (action && typeof action.label === 'string' && action.label !== '') {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(action.label)\n\t\t\t}\n\t\t\tif (this.content.addLabel) {\n\t\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\t\treturn fn(this.content.addLabel)\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Add')\n\t\t},\n\n\t\t/** Stable signature of the query so the watcher only refetches on real change. */\n\t\tsourceKey() {\n\t\t\tconst c = this.content || {}\n\t\t\treturn JSON.stringify({\n\t\t\t\tregister: c.register || '',\n\t\t\t\tschema: c.schema || '',\n\t\t\t\t// The RESOLVED filter (workspace + object tokens applied) so the\n\t\t\t\t// watcher refetches when page-level state a token reads changes.\n\t\t\t\tfilter: this.resolvedFilter,\n\t\t\t\t// `localSort`, not `c.sort` — an interactive header click\n\t\t\t\t// (`content.sortable`) overrides the manifest default without\n\t\t\t\t// touching the prop, and the fetch must follow that override.\n\t\t\t\tsort: this.localSort || {},\n\t\t\t\theaderFilters: this.headerFilters,\n\t\t\t\tlimit: c.limit || 25,\n\t\t\t\tobjectId: this.objectCtx ? this.objectCtx.objectId : null,\n\t\t\t})\n\t\t},\n\t},\n\n\twatch: {\n\t\tpresetRows(next) {\n\t\t\tif (next !== null) {\n\t\t\t\tthis.rows = [...next]\n\t\t\t\tthis.total = this.rows.length\n\t\t\t}\n\t\t},\n\n\t\tfacetedRows: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.publishSelectionRows()\n\t\t\t},\n\t\t},\n\n\t\t/** The counts under the card title follow the rows (`content.headerSubtitle`). */\n\t\theaderSubtitleText() {\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\t/** The counts beside the card title follow the rows (`content.headerNote`). */\n\t\theaderNoteText() {\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\tsourceKey() {\n\t\t\t// The query changed, so page 3 of the OLD result set means nothing\n\t\t\t// against the new one — and an out-of-range `_page` returns an empty\n\t\t\t// list, which would read as \"no matches\" rather than \"wrong page\".\n\t\t\tthis.page = 1\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t// The create-dialog schema is cached per instance, and an instance can\n\t\t// be retargeted (CnRelatedCollections keys its children by index, so a\n\t\t// reordered entry reuses the component). A schema resolved for the OLD\n\t\t// register is exactly the wrong-app schema the register scoping above\n\t\t// exists to prevent — drop it so the next open resolves it again.\n\t\t//\n\t\t// The headings read the same cache, and unlike the dialog they have no\n\t\t// later moment that reloads it: without the second call a retargeted\n\t\t// widget goes back to printing `customerName` at the user until\n\t\t// somebody happens to open Add.\n\t\t'content.register': function() {\n\t\t\tthis.forgetCreateSchema()\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t},\n\n\t\t'content.schema': function() {\n\t\t\tthis.forgetCreateSchema()\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.presetRows !== null) {\n\t\t\t// A nested list: its owner reads the rows and keeps them current.\n\t\t\tthis.rows = [...this.presetRows]\n\t\t\tthis.total = this.rows.length\n\t\t\tthis.loadHeadingsIfNeeded()\n\t\t\treturn\n\t\t}\n\t\tthis.fetchRows()\n\t\tthis.loadHeadingsIfNeeded()\n\t\tthis.offerExport()\n\t\tthis.loadFooterGroups()\n\t\t// A table wider than the widget fits its columns to the width instead of\n\t\t// scrolling sideways (no inner scrollbars).\n\t\tif (typeof ResizeObserver !== 'undefined') {\n\t\t\tthis._widthObserver = new ResizeObserver(() => this.measureWidthFit())\n\t\t\tthis._widthObserver.observe(this.$el)\n\t\t}\n\t\t// Re-read on a page-level refresh. This list fetches its own rows from\n\t\t// OpenRegister and subscribed to nothing, so after a write elsewhere on\n\t\t// the page it went on rendering the result set it fetched on mount —\n\t\t// values the backend had already changed. The endpoint-bound widgets\n\t\t// have answered this channel since Wave 2 (useEndpointSource); the\n\t\t// store-backed ones did not.\n\t\t//\n\t\t// Guarded on `loading`: the channel is a broadcast, and a page whose\n\t\t// action fires it more than once (or a dialog that emits alongside the\n\t\t// page's own Refresh) must not turn one write into a queue of\n\t\t// overlapping reads for one list.\n\t\tthis._onPageRefresh = (payload) => {\n\t\t\tif (this.loading) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst done = this.fetchRows()\n\t\t\tpayload?.waitUntil?.(done)\n\t\t}\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t// Observe the host grid cell so the visible row count re-fits on\n\t\t// resize/layout changes. Only detail-grid cells constrain height;\n\t\t// on dashboards the closest() lookup misses and fitRows stays null.\n\t\tconst cell = this.$el.closest && this.$el.closest('.grid-stack-item-content')\n\t\tif (cell && typeof ResizeObserver !== 'undefined') {\n\t\t\tthis._fitObserver = new ResizeObserver(() => this.measureFit())\n\t\t\tthis._fitObserver.observe(cell)\n\t\t}\n\t\tthis.$nextTick(() => this.measureFit())\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this._onPageRefresh) {\n\t\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t\tthis._onPageRefresh = null\n\t\t}\n\t\tif (this._fitObserver) {\n\t\t\tthis._fitObserver.disconnect()\n\t\t}\n\t\tif (this._widthObserver) {\n\t\t\tthis._widthObserver.disconnect()\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Offer `content.export` as a button in the card's header: the list as\n\t\t * it is filtered here, through OpenRegister's export (which applies its\n\t\t * own read rights to every row). `group` keeps the button to the members\n\t\t * of one Nextcloud group, a role such as AI oversight; it hides the\n\t\t * button, and the export endpoint still decides what anyone may read.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/widget-subtitle-and-list-export/specs/object-list-widget/spec.md#requirement-a-list-can-offer-its-rows-as-an-export\n\t\t */\n\t\t/**\n\t\t * The content of one row's child list: the declared `children` with\n\t\t * its `@row` tokens resolved against the row and the parent's\n\t\t * register when it names none.\n\t\t *\n\t\t * @param {object} row The parent row.\n\t\t * @return {object}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\tchildContentFor(row) {\n\t\t\tconst children = this.childrenConfig || {}\n\t\t\treturn {\n\t\t\t\t...children,\n\t\t\t\tregister: children.register || (this.content || {}).register,\n\t\t\t\tfilter: resolveRowFilter(children.filter, row),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The child rows of one row, as last read.\n\t\t *\n\t\t * @param {object} row The parent row.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tchildRowsFor(row) {\n\t\t\tconst link = this.childLink\n\t\t\tif (!link) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst value = resolveRowFilter({ v: `@row.${link.field}` }, row).v\n\t\t\treturn this.childRows[String(value)] || []\n\t\t},\n\n\t\t/**\n\t\t * Read the children of every loaded row in one query (`<link>[]=` the\n\t\t * row values, page by page) and file them under their row.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-children/specs/object-list-widget/spec.md#requirement-a-list-can-show-each-rows-child-records-under-it\n\t\t */\n\t\t/**\n\t\t * Read the rows `content.readOnlyRows` adds after the list's own:\n\t\t * `{ url, responsePath?, where? }`, an app endpoint (tokens such as\n\t\t * `@objectId` in the url), narrowed by `where` (`{ field: value }` or\n\t\t * `{ field: [values] }`). Each row is tagged `_readOnly`. A read that\n\t\t * fails adds nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-read-only-rows/specs/object-list-widget/spec.md#requirement-a-list-can-add-read-only-rows-from-an-endpoint\n\t\t */\n\t\tasync fetchReadOnlyRows() {\n\t\t\tconst config = this.content && this.content.readOnlyRows\n\t\t\tif (!config || typeof config !== 'object' || typeof config.url !== 'string' || config.url === '') {\n\t\t\t\tthis.readOnlyRows = []\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlet data = null\n\t\t\ttry {\n\t\t\t\tdata = await fetchEndpointSource({ url: config.url, responsePath: config.responsePath || '' }, this.tokenCtx)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load the read-only rows:', e)\n\t\t\t}\n\t\t\tconst where = config.where && typeof config.where === 'object' ? Object.entries(config.where) : []\n\t\t\tconst matches = (row) => where.every(([field, want]) => {\n\t\t\t\tconst value = readPath(row, field)\n\t\t\t\treturn Array.isArray(want) ? want.map(String).includes(String(value)) : String(value) === String(want)\n\t\t\t})\n\t\t\tthis.readOnlyRows = (Array.isArray(data) ? data : [])\n\t\t\t\t.filter((row) => row && typeof row === 'object' && matches(row))\n\t\t\t\t.map((row) => ({ ...row, _readOnly: true }))\n\t\t},\n\n\t\tasync fetchChildren() {\n\t\t\tconst children = this.childrenConfig\n\t\t\tconst link = this.childLink\n\t\t\tif (!children || !link) {\n\t\t\t\tthis.childRows = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst values = [...new Set(this.rows\n\t\t\t\t.map((row) => resolveRowFilter({ v: `@row.${link.field}` }, row).v)\n\t\t\t\t.filter((v) => v !== undefined && v !== null && v !== '')\n\t\t\t\t.map(String))]\n\t\t\tif (values.length === 0) {\n\t\t\t\tthis.childRows = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst register = children.register || (this.content || {}).register\n\t\t\tconst params = { [link.key]: values }\n\t\t\tfor (const [k, v] of Object.entries(resolveFilterTokens(children.filter || {}, this.tokenCtx))) {\n\t\t\t\tif (k !== link.key && rowTokenField(v) === '' && v !== '' && v !== null && v !== undefined && typeof v !== 'object') {\n\t\t\t\t\tparams[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst order = children.reorder && children.reorder.field\n\t\t\tif (typeof order === 'string' && order !== '') {\n\t\t\t\tparams[`_order[${order}]`] = 'asc'\n\t\t\t} else if (children.sort && children.sort.field) {\n\t\t\t\tparams[`_order[${children.sort.field}]`] = children.sort.dir === 'desc' ? 'desc' : 'asc'\n\t\t\t}\n\t\t\tif (Array.isArray(children.extend) && children.extend.length > 0) {\n\t\t\t\tparams._extend = children.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t}\n\t\t\tconst limit = Number(children.limit) || 500\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\tconst url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register, schema: children.schema })\n\t\t\t\tconst grouped = {}\n\t\t\t\tfor (let page = 1; ; page++) {\n\t\t\t\t\tconst res = await axios.get(url, { params: { ...params, _limit: limit, _page: page } })\n\t\t\t\t\tconst results = (res && res.data && res.data.results) || []\n\t\t\t\t\tfor (const child of results) {\n\t\t\t\t\t\tconst key = String(objectFieldValue(child, link.key))\n\t\t\t\t\t\tif (values.includes(key)) {\n\t\t\t\t\t\t\t(grouped[key] = grouped[key] || []).push(child)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tconst total = res && res.data && typeof res.data.total === 'number' ? res.data.total : results.length\n\t\t\t\t\tif (results.length < limit || page * limit >= total) {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.childRows = grouped\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load child rows:', e)\n\t\t\t\tthis.childRows = {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The export URL for this list as it is filtered here.\n\t\t *\n\t\t * @param {'csv'|'excel'} format The export format.\n\t\t * @return {string} The URL.\n\t\t */\n\t\texportUrl(format) {\n\t\t\treturn buildExportUrl(this.content.register, this.content.schema, this.resolvedFilter, format)\n\t\t},\n\n\t\t/**\n\t\t * The download URL of an `export` that names an app endpoint: its\n\t\t * `url` and `params` resolved against the record, '' while a required\n\t\t * token is unresolved.\n\t\t *\n\t\t * @param {{url: string, params?: object}} config The export config.\n\t\t * @return {string} The URL under the Nextcloud base, or ''.\n\t\t * @spec openspec/changes/object-list-endpoint-rows/specs/object-list-widget/spec.md#requirement-a-list-can-read-its-rows-from-an-app-endpoint\n\t\t */\n\t\tendpointExportUrl(config) {\n\t\t\tconst request = resolveEndpointRequest({ url: config.url, params: config.params || {} }, this.tokenCtx)\n\t\t\tif (request.blocked) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst base = /^https?:\\/\\//i.test(request.url) ? request.url : generateUrl(request.url)\n\t\t\tconst query = new URLSearchParams(Object.entries(request.params).map(([key, value]) => [key, String(value)])).toString()\n\t\t\treturn query === '' ? base : `${base}${base.includes('?') ? '&' : '?'}${query}`\n\t\t},\n\n\t\t/**\n\t\t * Read the rows from `content.endpointSource`: rows at its\n\t\t * `responsePath` (default `results`), the count at `total`.\n\t\t *\n\t\t * @param {{url: string, params?: object, responsePath?: string}} source The endpoint.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-endpoint-rows/specs/object-list-widget/spec.md#requirement-a-list-can-read-its-rows-from-an-app-endpoint\n\t\t */\n\t\tasync fetchEndpointRows(source) {\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst body = await fetchEndpointSource({ url: source.url, method: source.method, params: source.params || {} }, this.tokenCtx)\n\t\t\t\tconst rows = getByPath(body, typeof source.responsePath === 'string' && source.responsePath !== '' ? source.responsePath : 'results')\n\t\t\t\tthis.rows = Array.isArray(rows) ? rows : []\n\t\t\t\tthis.total = body && typeof body.total === 'number' ? body.total : this.rows.length\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tthis.measureFit()\n\t\t\t\t\tthis.measureWidthFit()\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load the endpoint rows:', e)\n\t\t\t\tthis.error = endpointErrorMessage(e)\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync offerExport() {\n\t\t\tconst config = this.content && this.content.export\n\t\t\tconst endpointUrl = config && typeof config.url === 'string' && config.url !== '' ? this.endpointExportUrl(config) : ''\n\t\t\tif (!config || typeof config !== 'object' || (!endpointUrl && (!this.content.register || !this.content.schema))) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof config.group === 'string' && config.group !== '') {\n\t\t\t\tconst groups = await getCurrentUserGroups().catch(() => [])\n\t\t\t\tif (!groups.includes(config.group)) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst format = config.format === 'excel' ? 'excel' : 'csv'\n\t\t\tconst label = typeof config.label === 'string' && config.label !== '' ? (typeof this.cnTranslate === 'function' ? this.cnTranslate(config.label) : config.label) : t('nextcloud-vue', 'Download')\n\t\t\t/**\n\t\t\t * @event header The card header's Download action for the list as\n\t\t\t * filtered here (`content.export`), handed to the host card.\n\t\t\t * @type {{action: {label: string, icon: string, run: Function}}}\n\t\t\t */\n\t\t\tthis.headerAction = {\n\t\t\t\tlabel,\n\t\t\t\ticon: 'Download',\n\t\t\t\trun: () => window.location.assign(endpointUrl || this.exportUrl(format)),\n\t\t\t}\n\t\t\tthis.emitHeader()\n\t\t},\n\n\t\t/**\n\t\t * A counts template (`{total}`, `{count:<field>=<value>}`) translated and\n\t\t * filled from the rows, or '' without one.\n\t\t *\n\t\t * @param {unknown} text The template.\n\t\t * @return {string}\n\t\t */\n\t\tcountLine(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t\treturn fillCountLabel(fn(text), this.total || this.rows.length, this.rows)\n\t\t},\n\n\t\t/**\n\t\t * Tell the card around this list what its header shows: the Download\n\t\t * action (`content.export`), the counts line under the title\n\t\t * (`content.headerSubtitle`) and the note beside it\n\t\t * (`content.headerNote`). Nothing when the list has none of them.\n\t\t *\n\t\t * @spec openspec/changes/widget-header-subtitle-counts/specs/widget-header-subtitle-counts/spec.md#requirement-a-list-can-say-its-counts-under-its-card-title\n\t\t */\n\t\temitHeader() {\n\t\t\tconst subtitle = this.headerSubtitleText\n\t\t\tconst note = this.headerNoteText\n\t\t\t// A line shown before is cleared with an empty one, so the card does\n\t\t\t// not keep a stale count; a list that never had one stays silent.\n\t\t\tif (!this.headerAction && subtitle === '' && note === '' && !this.headerSubtitleShown) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.headerSubtitleShown = subtitle !== '' || note !== ''\n\t\t\t/**\n\t\t\t * @event header The card header's parts: the Download action for the\n\t\t\t * list as filtered here (`content.export`) and the counts line under\n\t\t\t * the title (`content.headerSubtitle`).\n\t\t\t * @type {{action?: {label: string, icon: string, run: Function}, subtitle: string}}\n\t\t\t */\n\t\t\tthis.$emit('header', {\n\t\t\t\t...(this.headerAction ? { action: this.headerAction } : {}),\n\t\t\t\tsubtitle,\n\t\t\t\tnote,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Move a row one place up or down (the row actions).\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @param {number} delta -1 for up, 1 for down.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\tmoveRowBy(row, delta) {\n\t\t\tconst from = this.rows.findIndex((r) => rowIdOf(r) === rowIdOf(row))\n\t\t\treturn this.reorderTo(from, from + delta)\n\t\t},\n\n\t\t/**\n\t\t * A row dragged to another place in the table.\n\t\t *\n\t\t * @param {{from: number, to: number}} move The old and new index.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tonRowMove({ from, to } = {}) {\n\t\t\treturn this.reorderTo(from, to)\n\t\t},\n\n\t\t/**\n\t\t * Put the row at `from` at `to`, renumber the rows from this page's\n\t\t * first place, and write the order field on the rows whose number\n\t\t * changed, as a PATCH of that field only. A refusal is read out and the\n\t\t * list is read again.\n\t\t *\n\t\t * @param {number} from The index moved from.\n\t\t * @param {number} to The index moved to.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/list-reorder-order-field/specs/object-list-widget/spec.md#requirement-a-list-can-be-reordered-by-its-order-field\n\t\t */\n\t\tasync reorderTo(from, to) {\n\t\t\tconst field = this.reorderField\n\t\t\tif (field === '' || from < 0 || to < 0 || from >= this.rows.length || to >= this.rows.length || from === to) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst moved = this.rows[from]\n\t\t\tconst { rows, changes } = renumberRows(moveIndex(this.rows, from, to), field, (this.page - 1) * this.pageSize + 1)\n\t\t\tthis.rows = rows\n\t\t\tconst c = this.content || {}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\tfor (const change of changes) {\n\t\t\t\t\tawait axios.patch(\n\t\t\t\t\t\tgenerateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id: change.id }),\n\t\t\t\t\t\t{ [field]: change.value },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthis.reorderStatus = t('nextcloud-vue', '{name} moved to place {place} of {total}', {\n\t\t\t\t\tname: objectDisplayName(moved || {}) || t('nextcloud-vue', 'Row'),\n\t\t\t\t\tplace: (this.page - 1) * this.pageSize + to + 1,\n\t\t\t\t\ttotal: Math.max(this.total, this.rows.length),\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tthis.reorderStatus = (data && (data.message || data.error)) || t('nextcloud-vue', 'The new order could not be saved.')\n\t\t\t\tawait this.fetchRows()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch the object rows from OpenRegister. Lazily imports the\n\t\t * axios/router helpers (the library never hard-depends on them at module\n\t\t * load — same pattern as CnStatWidget / CnFilesWidget).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchRows() {\n\t\t\tif (this.presetRows !== null) {\n\t\t\t\t/**\n\t\t\t\t * @event rows-stale The rows handed in through `presetRows` are\n\t\t\t\t * out of date (after a write or a refused reorder); the owner\n\t\t\t\t * reads them again.\n\t\t\t\t */\n\t\t\t\tthis.$emit('rows-stale')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst c = this.content || {}\n\t\t\tif ((!c.register || !c.schema) && c.endpointSource && typeof c.endpointSource.url === 'string' && c.endpointSource.url !== '') {\n\t\t\t\tawait this.fetchEndpointRows(c.endpointSource)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!c.register || !c.schema) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A context token this list depends on (e.g. @workspace.selectedClient)\n\t\t\t// isn't set yet — don't fetch the whole register; the prompt shows.\n\t\t\tif (this.waitingForContext) {\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.error = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.error = ''\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}',\n\t\t\t\t\t{ register: c.register, schema: c.schema },\n\t\t\t\t)\n\t\t\t\t// `limit` is a FETCH CAP (ADR-062), not a render promise — the\n\t\t\t\t// visible count fits the cell; fetch enough to fill big cells.\n\t\t\t\t// It doubles as the PAGE SIZE: one fetch is exactly one page, so\n\t\t\t\t// the pager's \"1–25 of 137\" is the server's arithmetic and not a\n\t\t\t\t// client-side count of an already-capped window.\n\t\t\t\tconst params = { _limit: this.pageSize, _page: this.page }\n\t\t\t\tif (this.reorderField !== '') {\n\t\t\t\t\t// A reorderable list is always shown in its stored order.\n\t\t\t\t\tparams[`_order[${this.reorderField}]`] = 'asc'\n\t\t\t\t} else if (this.localSort && this.localSort.field) {\n\t\t\t\t\tparams[`_order[${this.localSort.field}]`] = (this.localSort.dir === 'desc' ? 'desc' : 'asc')\n\t\t\t\t}\n\t\t\t\t// `content.extend` → OpenRegister's repeated `_extend[]`. axios\n\t\t\t\t// serializes an array value as `_extend[]=a&_extend[]=b`, which\n\t\t\t\t// is the wire form OR reads. This is what turns a dotted column\n\t\t\t\t// key into a real value: the referenced object is inlined on the\n\t\t\t\t// row, so `informatieobject.title` resolves instead of reading a\n\t\t\t\t// uuid string as an object path.\n\t\t\t\tif (Array.isArray(c.extend)) {\n\t\t\t\t\tconst extend = c.extend.filter((e) => typeof e === 'string' && e !== '')\n\t\t\t\t\tif (extend.length > 0) {\n\t\t\t\t\t\tparams._extend = extend\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t// The OpenRegister OBJECT-SEARCH endpoint filters on DIRECT field\n\t\t\t\t// params (`status=open`, `value[gt]=30000`) — unlike the\n\t\t\t\t// aggregation endpoints which use the nested `filter[...]` shape.\n\t\t\t\tconst filter = this.resolvedFilter\n\t\t\t\tif (filter && typeof filter === 'object') {\n\t\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\t\tif (Array.isArray(v)) {\n\t\t\t\t\t\t\t// A list matches any of its values: `field[]=a&field[]=b`.\n\t\t\t\t\t\t\tif (v.length > 0) {\n\t\t\t\t\t\t\t\tparams[k] = v\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (v && typeof v === 'object') {\n\t\t\t\t\t\t\tfor (const [op, ov] of Object.entries(v)) {\n\t\t\t\t\t\t\t\tparams[`${k}[${op}]`] = ov\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\t\tparams[k] = v\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tObject.assign(params, headerFilterParams(this.headerFilters))\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tthis.rows = (res && res.data && res.data.results) || []\n\t\t\t\tthis.total = (res && res.data && typeof res.data.total === 'number') ? res.data.total : this.rows.length\n\t\t\t\tawait this.fetchChildren()\n\t\t\t\tawait this.fetchReadOnlyRows()\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tthis.measureFit()\n\t\t\t\t\tthis.measureWidthFit()\n\t\t\t\t})\n\t\t\t} catch (e) {\n\t\t\t\t// Keep the raw message OUT of the template — log it for\n\t\t\t\t// debugging, surface only the quiet `loadErrorLabel` line.\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnObjectListWidget] failed to load objects:', e)\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t\tthis.rows = []\n\t\t\t\tthis.total = 0\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Squeeze the table to the widget's width while it scrolls sideways: its\n\t\t * `.cn-data-table__scroll` wrapper is wider than it is visible. First the\n\t\t * fit (stage 1), then, if that is still too wide, a fixed layout\n\t\t * (stage 2). It never goes back (a squeezed table does not overflow, so\n\t\t * there is nothing to measure to relax it). No-op when `content.fitWidth`\n\t\t * is set.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureWidthFit() {\n\t\t\tif (this.fitStage >= 2 || typeof this.content?.fitWidth === 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst scroller = this.$el && this.$el.querySelector ? this.$el.querySelector('.cn-data-table__scroll') : null\n\t\t\tif (scroller && scroller.scrollWidth > scroller.clientWidth + 1) {\n\t\t\t\tthis.fitStage += 1\n\t\t\t\tthis.$nextTick(() => this.measureWidthFit())\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fit the visible row count to the host grid cell (ADR-062 — the cell\n\t\t * is the budget). Measures the cell's remaining height below the table\n\t\t * top, reserves room for the \"View all\" footer, and derives the row\n\t\t * budget from the first rendered row's height. No-ops (fitRows null =\n\t\t * render all fetched rows) outside a fixed-height cell and inside a\n\t\t * tab panel, unless the content sets `fit: true`.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tmeasureFit() {\n\t\t\t// `content.fit: false` opts out of the cell budget: render every row\n\t\t\t// fetched and let the container scroll. The budget below assumes the\n\t\t\t// list OWNS its cell, measuring from where the table starts to the\n\t\t\t// cell's bottom. That is wrong for a list stacked below other content\n\t\t\t// in the same cell (a section of a tabbed panel, say): it starts near\n\t\t\t// the bottom, the budget floors to one row, and the rest are clipped.\n\t\t\t// Only an explicit `false` opts out, so a manifest that never sets the\n\t\t\t// key keeps the ADR-062 behaviour unchanged.\n\t\t\tif ((this.content && this.content.fit === false) || this.presetRows !== null) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cell = this.$el && this.$el.closest && this.$el.closest('.grid-stack-item-content')\n\t\t\tif (!cell) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A list inside a tab panel does not own the cell, as on a\n\t\t\t// dashboard: it renders every fetched row unless the content asks\n\t\t\t// for the fit with `fit: true`.\n\t\t\tconst panel = this.$el.closest('[role=\"tabpanel\"]')\n\t\t\tif (panel && cell.contains(panel) && !(this.content && this.content.fit === true)) {\n\t\t\t\tthis.fitRows = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst table = this.$el.querySelector('.cn-object-list-widget__table table')\n\t\t\tif (!table) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cellRect = cell.getBoundingClientRect()\n\t\t\tconst tableRect = table.getBoundingClientRect()\n\t\t\tconst firstRow = table.querySelector('tbody tr')\n\t\t\tconst rowH = (firstRow && firstRow.getBoundingClientRect().height) || 44\n\t\t\tconst head = table.querySelector('thead')\n\t\t\tconst headH = (head && head.getBoundingClientRect().height) || 40\n\t\t\t// Room for the \"View all\" footer AND the Add button (ADR-062).\n\t\t\tconst footerReserve = 68\n\t\t\tconst available = cellRect.bottom - tableRect.top - footerReserve\n\t\t\tconst fit = Math.floor((available - headH) / rowH)\n\t\t\tthis.fitRows = Math.max(fit, 1)\n\t\t},\n\n\t\t/**\n\t\t * Drop the cached create-dialog schema (and close the dialog if it is\n\t\t * open) when the list points at another register or schema.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tforgetCreateSchema() {\n\t\t\tthis.createSchema = null\n\t\t\tthis.schemaRequest = null\n\t\t\tthis.showCreate = false\n\t\t\tthis.editingRow = null\n\t\t},\n\n\t\t/**\n\t\t * Open the create dialog for the list's target schema. PUBLIC — the\n\t\t * host card's Actions-menu \"Add\" entry calls this through a ref, the\n\t\t * widget's own footer button calls it directly (ADR-062: both\n\t\t * affordances, one dialog).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\t/**\n\t\t * Edit a row in the list's own form (`content.editable`): the form the\n\t\t * Add button opens, filled with the row.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync openEdit(row) {\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t\tthis.showCreate = false\n\t\t\t\tthis.editingRow = row ? { ...row } : null\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Delete a row of the list's own schema (`content.deletable`). The\n\t\t * toast offers Undo; a refusal toasts the server's message.\n\t\t *\n\t\t * @param {object} row The row.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync deleteRow(row) {\n\t\t\tconst c = this.content || {}\n\t\t\tconst id = rowIdOf(row)\n\t\t\tif (id === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst pending = (async () => {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\ttry {\n\t\t\t\t\tawait axios.delete(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }))\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\t\tthrow new Error((typeof refusal === 'string' && refusal) || (e && e.message) || t('nextcloud-vue', 'The delete failed'), { cause: e })\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t})()\n\t\t\tawait this.reportDelete(row, pending)\n\t\t\tawait pending.catch(() => {})\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t/** Close the form, whether it created or edited. */\n\t\tcloseForm() {\n\t\t\tthis.showCreate = false\n\t\t\tthis.editingRow = null\n\t\t},\n\n\t\t/**\n\t\t * Save an edited row: a PATCH of the keys the form changed, so every\n\t\t * key it does not show keeps its stored value; nothing when nothing\n\t\t * changed. A refusal shows the server's message in the dialog.\n\t\t *\n\t\t * @param {object} formData The edited values.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/object-list-edit-in-place/specs/object-list-widget/spec.md#requirement-a-list-can-edit-a-row-in-its-own-form\n\t\t */\n\t\tasync onEditConfirm(formData) {\n\t\t\tconst c = this.content || {}\n\t\t\tconst row = this.editingRow\n\t\t\tconst id = rowIdOf(row)\n\t\t\tconst changes = changedKeys(row, formData)\n\t\t\ttry {\n\t\t\t\tif (id !== '' && Object.keys(changes).length > 0) {\n\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\t\tawait axios.patch(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}', { register: c.register, schema: c.schema, id }), changes)\n\t\t\t\t}\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event updated Emitted after a row was edited, with the keys that changed.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('updated', { id, changes })\n\t\t\t\tthis.fetchRows()\n\t\t\t} catch (e) {\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ error: (typeof refusal === 'string' && refusal) || (e && e.message) || 'error' })\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tasync openCreate() {\n\t\t\tconst c = this.content || {}\n\t\t\tconst { action, visible } = this.createState\n\t\t\tif (action) {\n\t\t\t\tif (visible) {\n\t\t\t\t\tthis.dispatch(action)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!c.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t\tthis.showCreate = true\n\t\t\t} catch (e) {\n\t\t\t\tthis.error = (e && e.message) || 'error'\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch this list's schema once, for whoever needs it.\n\t\t *\n\t\t * Two readers share it: the create dialog builds its form from the\n\t\t * properties, and `resolvedColumns` reads each property's `title` to\n\t\t * label a column the manifest gave only a key for. A request in flight\n\t\t * for the same register and schema is shared, and an answer for a list\n\t\t * that has since been retargeted is dropped.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSchema() {\n\t\t\tconst c = this.content || {}\n\t\t\tif (this.createSchema || !c.schema) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst key = `${c.register || ''}/${c.schema}`\n\t\t\tif (this.schemaRequest && this.schemaRequest.key === key) {\n\t\t\t\treturn this.schemaRequest.promise\n\t\t\t}\n\t\t\tconst promise = this.fetchSchema(c)\n\t\t\tthis.schemaRequest = { key, promise }\n\t\t\ttry {\n\t\t\t\tconst schema = await promise\n\t\t\t\tif (this.schemaRequest && this.schemaRequest.promise === promise) {\n\t\t\t\t\tthis.createSchema = schema\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tif (this.schemaRequest && this.schemaRequest.promise === promise) {\n\t\t\t\t\tthis.schemaRequest = null\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read a list's schema definition from OpenRegister.\n\t\t *\n\t\t * @param {object} c The list config (`register`, `schema`).\n\t\t * @return {Promise<object|null>} The schema, or null.\n\t\t */\n\t\tasync fetchSchema(c) {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\t// A slug is NOT a namespace (same rule as useObjectStore.fetchSchema,\n\t\t\t// #725): on an instance where two apps both own a `page` schema the\n\t\t\t// bare endpoint resolves instance-wide and serves the OTHER app's\n\t\t\t// schema into this form — the dialog then asks for fields the\n\t\t\t// register-scoped POST does not know and refuses the ones it\n\t\t\t// requires (portaliq + opencatalogi, WOO-564: a 400 on every\n\t\t\t// \"Add page\"/\"Add menu\"). Name the list's own register whenever it\n\t\t\t// has one; a backend that does not know the parameter ignores it.\n\t\t\tconst url = generateUrl('/apps/openregister/api/schemas/{sch}', { sch: c.schema })\n\t\t\tconst params = c.register ? { register: c.register } : undefined\n\t\t\tconst res = await axios.get(url, params ? { params } : undefined)\n\t\t\treturn (res && res.data) || null\n\t\t},\n\n\t\t/**\n\t\t * Load the schema when, and only when, a column needs it: for its\n\t\t * heading (no `label`), or for its presentation (a top-level key\n\t\t * without its own `format`, `enum`, `widget` or `formatter`, which\n\t\t * takes the property's format and enum labels).\n\t\t *\n\t\t * A widget whose every column says its label and presentation, or\n\t\t * reads a dotted path the list's schema does not hold, never makes\n\t\t * this request.\n\t\t *\n\t\t * Failure is deliberately silent: an unreachable schema costs the nicer\n\t\t * heading, and the column still renders under its key. It must never\n\t\t * cost the list itself.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadHeadingsIfNeeded() {\n\t\t\tconst cols = Array.isArray(this.content.columns) ? this.content.columns : []\n\t\t\tconst needsSchema = cols.some((c) => (typeof c === 'string')\n\t\t\t\t|| !c.label\n\t\t\t\t|| (typeof c.key === 'string' && !c.key.includes('.') && !c.key.startsWith('@')\n\t\t\t\t\t&& c.format === undefined && c.enum === undefined && c.widget === undefined && c.formatter === undefined))\n\t\t\tif (needsSchema === false) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tawait this.ensureSchema()\n\t\t\t} catch {\n\t\t\t\t// Headings only. Swallowed on purpose; see the docblock.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist the create-dialog form: the resolved scalar filter values\n\t\t * are merged in as defaults (an FK-scoped list creates PRE-LINKED\n\t\t * children — a task added on a case detail already carries the case).\n\t\t *\n\t\t * @param {object} formData Confirmed form values.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onCreateConfirm(formData) {\n\t\t\tconst c = this.content || {}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst payload = { ...formData }\n\t\t\t\tconst filter = this.resolvedFilter || {}\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v && typeof v !== 'object' && (payload[k] === undefined || payload[k] === null || payload[k] === '')) {\n\t\t\t\t\t\tpayload[k] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst endpoint = c.createEndpoint && typeof c.createEndpoint === 'object' && c.createEndpoint.url ? c.createEndpoint : null\n\t\t\t\tconst url = endpoint ? generateUrl(endpoint.url) : generateUrl('/apps/openregister/api/objects/{register}/{schema}', { register: c.register, schema: c.schema })\n\t\t\t\tconst body = endpoint && endpoint.bodyKey ? { ...(endpoint.extra || {}), [endpoint.bodyKey]: payload } : { ...((endpoint && endpoint.extra) || {}), ...payload }\n\t\t\t\tconst response = await axios.post(url, body)\n\t\t\t\tdispatchObjectCreated({ register: c.register, schema: c.schema, object: (response && response.data) || payload })\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event created Emitted after a successful create with the sent payload.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('created', payload)\n\t\t\t\tthis.fetchRows()\n\t\t\t} catch (e) {\n\t\t\t\t// The server's own refusal (\"a case type keeps one final status\")\n\t\t\t\t// says more than the transport's \"Request failed with status 422\".\n\t\t\t\tconst data = e && e.response && e.response.data\n\t\t\t\tconst refusal = data && (data.message || data.error)\n\t\t\t\tif (this.$refs.createDialog) {\n\t\t\t\t\tthis.$refs.createDialog.setResult({ error: (typeof refusal === 'string' && refusal) || (e && e.message) || 'error' })\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pager click — refetch that page from the server. Paging is never\n\t\t * client-side over an already-capped window: that would silently drop\n\t\t * every row past the cap while still displaying a total that counted\n\t\t * them.\n\t\t *\n\t\t * @param {number} next The 1-based page to load.\n\t\t * @return {void}\n\t\t */\n\t\tonPageChange(next) {\n\t\t\tconst target = Math.min(Math.max(Number(next) || 1, 1), this.totalPages)\n\t\t\tif (target === this.page) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.page = target\n\t\t\tthis.fetchRows()\n\t\t},\n\n\t\t/**\n\t\t * Load the current user's groups when a `footerLinks` entry names one.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFooterGroups() {\n\t\t\tconst links = Array.isArray(this.content.footerLinks) ? this.content.footerLinks : []\n\t\t\tif (this.footerGroups !== null || !links.some((link) => link && typeof link.group === 'string' && link.group !== '')) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.footerGroups = await getCurrentUserGroups().catch(() => [])\n\t\t},\n\n\t\t/**\n\t\t * A footer link click: a route link goes through the router (a\n\t\t * modified click is left to the browser), an `href` is a plain link.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @param {{location: object|null}} link The link.\n\t\t * @return {void}\n\t\t */\n\t\tonFooterLink(event, link) {\n\t\t\tif (link.location) {\n\t\t\t\tfollowLinkClick(event, link.location, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * \"View all (N)\" footer click: emits `view-all` and, when the content\n\t\t * blob names a `viewAllRoute`, navigates there with `viewAllQuery`\n\t\t * (its values token-resolved, so `{\"case\": \"@objectId\"}` carries the\n\t\t * current object scope into the target index page). A modified click\n\t\t * on the link is left to the browser.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonViewAll(event) {\n\t\t\t/**\n\t\t\t * @event view-all Emitted when the \"View all (N)\" footer is clicked.\n\t\t\t * @type {{ total: number }}\n\t\t\t */\n\t\t\tthis.$emit('view-all', { total: this.total })\n\t\t\tif (this.viewAllLocation) {\n\t\t\t\tfollowLinkClick(event, this.viewAllLocation, this.$router)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Navigate to a configured detail route on row click (when `rowRoute`\n\t\t * is set and a router is available); a ctrl/cmd/shift click opens it\n\t\t * in a new tab.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} [event] The originating click event.\n\t\t * @return {void}\n\t\t */\n\t\tonRowClick(row, event) {\n\t\t\tif (row && row._readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.rowSelection === true && this.cnWidgetSelection && typeof this.cnHostWidgetId === 'function') {\n\t\t\t\tthis.cnWidgetSelection.select(this.cnHostWidgetId(), row)\n\t\t\t}\n\t\t\tthis.openRowRoute(row, event)\n\t\t\t/**\n\t\t\t * @event row-click Emitted with the clicked object (for hosts that\n\t\t\t * want to handle navigation themselves). Payload: `(row, event)`, the\n\t\t\t * second being the native click event. A middle click emits\n\t\t\t * `row-aux-click` instead.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('row-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Publish the rows this list shows under its widget id, for a card that\n\t\t * follows them (`content.rowSelection`).\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/side-card-follows-row/specs/side-card-follows-row/spec.md#requirement-a-list-publishes-its-rows-and-its-selected-row\n\t\t */\n\t\tpublishSelectionRows() {\n\t\t\tif (this.content.rowSelection !== true || !this.cnWidgetSelection || typeof this.cnHostWidgetId !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.cnWidgetSelection.publishRows(this.cnHostWidgetId(), this.facetedRows)\n\t\t},\n\n\t\t/**\n\t\t * A row middle click: opens the configured detail route in a new tab\n\t\t * and emits `row-aux-click`, not `row-click`.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} event The originating auxclick event.\n\t\t * @return {void}\n\t\t */\n\t\tonRowAuxClick(row, event) {\n\t\t\tthis.openRowRoute(row, event)\n\t\t\t/**\n\t\t\t * @event row-aux-click Emitted with the middle-clicked object, for\n\t\t\t * opening it in a new tab. Payload: `(row, event)`, the second being\n\t\t\t * the native auxclick event.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('row-aux-click', row, event)\n\t\t},\n\n\t\t/**\n\t\t * Open `content.rowRoute` for a row: in place, or in a new tab on a\n\t\t * ctrl/cmd/shift or middle click.\n\t\t *\n\t\t * @param {object} row The clicked object row.\n\t\t * @param {MouseEvent} [event] The originating event.\n\t\t * @return {void}\n\t\t */\n\t\topenRowRoute(row, event) {\n\t\t\tconst byField = this.content.rowRouteField && row ? (this.content.rowRouteMap || {})[String(row[this.content.rowRouteField])] : undefined\n\t\t\tconst route = byField || this.content.rowRoute\n\t\t\tconst field = this.content.rowIdField || this.content.rowRouteIdField\n\t\t\tconst raw = field ? readPath(row, field) : row && (row.id || (row['@self'] && row['@self'].id))\n\t\t\tconst id = typeof raw === 'string' || typeof raw === 'number' ? String(raw) : ''\n\t\t\tif (route && id && this.$router) {\n\t\t\t\tmarkNewTabHandled(event, openRowTarget(event, { name: route, params: { id } }, this.$router))\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch one declared row action. A `handler` action gets the row\n\t\t * appended as its last argument, the same convention CnWidgetObjectTable\n\t\t * uses, so a registry function reads its row from where it already\n\t\t * expects one. An `open-modal` action gets the row merged onto its\n\t\t * `props` as `row` — mirroring how a drop hands a modal `props.files`\n\t\t * and a bulk action hands it `props.selectedIds` — because a \"Versions\"\n\t\t * or \"Delete\" row action is meaningless without knowing WHICH row it\n\t\t * was clicked on, and `open-modal`'s `action.props` is forwarded\n\t\t * verbatim otherwise (it carries no per-click information at all).\n\t\t *\n\t\t * @param {object} action The declared action.\n\t\t * @param {object|null} row The row the action was triggered on.\n\t\t * @return {void}\n\t\t */\n\t\trunRowAction(action, row) {\n\t\t\tconst pending = this.dispatch(actionForRow(action, row), [row], { row })\n\t\t\tif (action && action.type === 'object-op' && action.op === 'delete') {\n\t\t\t\tthis.reportDelete(row, pending)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Toast a row delete, with an Undo for ten seconds that restores the row\n\t\t * from the OpenRegister trash and refreshes the list. `feedback: false` in\n\t\t * the widget `content` suppresses it. A create needs nothing here: the\n\t\t * create dialog already toasts, so the widget stays quiet to give one.\n\t\t *\n\t\t * @param {object} row The deleted row.\n\t\t * @param {Promise<unknown>|unknown} pending What the dispatcher returned.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync reportDelete(row, pending) {\n\t\t\tif ((this.content || {}).feedback === false) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst feedback = useWriteFeedback()\n\t\t\tlet outcome\n\t\t\ttry {\n\t\t\t\toutcome = await pending\n\t\t\t} catch (e) {\n\t\t\t\tfeedback.error((e && e.message) || t('nextcloud-vue', 'The delete failed'))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (outcome === false) {\n\t\t\t\tfeedback.error(t('nextcloud-vue', 'The delete failed'))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = row && (row.id || (row['@self'] && row['@self'].id))\n\t\t\tconst title = objectDisplayName(row || {})\n\t\t\tconst message = title !== '' ? t('nextcloud-vue', 'Deleted {title}', { title }) : t('nextcloud-vue', 'Deleted')\n\t\t\tfeedback.success(message, id\n\t\t\t\t? {\n\t\t\t\t\t\tundo: async () => {\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([import('@nextcloud/axios'), import('@nextcloud/router')])\n\t\t\t\t\t\t\t\tawait axios.post(generateUrl('/apps/openregister/api/deleted/{id}/restore', { id }))\n\t\t\t\t\t\t\t\tthis.fetchRows()\n\t\t\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\t\t\tfeedback.error((e && e.message) || t('nextcloud-vue', 'The row could not be restored'))\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t: {})\n\t\t},\n\n\t\t/**\n\t\t * Link fields for a declared row action that only navigates, so\n\t\t * CnRowActions renders it as a link. A target with tokens is left to\n\t\t * dispatch, which resolves them against the page.\n\t\t *\n\t\t * @param {object} action The declared row action.\n\t\t * @return {object} `{ href, linkTarget }`, `{ to }`, or `{}`.\n\t\t */\n\t\trowActionLink(action) {\n\t\t\t// A route with a query: a router location per row, the query's row\n\t\t\t// tokens filled raw (the router encodes) and its page tokens resolved.\n\t\t\tif (action && action.type === 'navigate' && !action.confirm && typeof action.route === 'string' && action.route !== '') {\n\t\t\t\tconst locationFor = (row) => ({\n\t\t\t\t\tname: action.route,\n\t\t\t\t\tquery: resolveFilterTokens(actionForRow(action, row).query || {}, this.tokenCtx),\n\t\t\t\t})\n\t\t\t\treturn {\n\t\t\t\t\thref: () => '',\n\t\t\t\t\tto: (row) => locationFor(row),\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (action && action.type === 'navigate' && !action.confirm && hasRowTokens(action.target)) {\n\t\t\t\tconst linkFor = (row) => actionLink(actionForRow(action, row), { router: this.$router || null, tokenCtx: this.tokenCtx })\n\t\t\t\t// An in-app link answers `to` (followed by the router), an external one `href`.\n\t\t\t\treturn {\n\t\t\t\t\thref: (row) => {\n\t\t\t\t\t\tconst link = linkFor(row)\n\t\t\t\t\t\treturn link && link.external ? link.href : ''\n\t\t\t\t\t},\n\n\t\t\t\t\tto: (row) => {\n\t\t\t\t\t\tconst link = linkFor(row)\n\t\t\t\t\t\treturn link && !link.external ? link.to : null\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (hasActionTargetTokens(action.target)) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst link = actionLink(action, { router: this.$router || null })\n\t\t\tif (!link) {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn link.external ? { href: link.href, linkTarget: '_blank' } : { to: link.to }\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a declared action through the page's pre-bound dispatcher,\n\t\t * falling back to a bare `dispatchAction` when this widget is mounted\n\t\t * outside a CnPageRenderer tree.\n\t\t *\n\t\t * `extraArgs` is appended to a `handler` action's `args`; it is NOT\n\t\t * merged into any other type, because only `handler` has an argument\n\t\t * list. A drop's `File[]` additionally rides an `open-modal` action's\n\t\t * `props.files`, since that is how a modal receives anything at all.\n\t\t *\n\t\t * @param {object} action The declared action.\n\t\t * @param {Array} extraArgs Arguments appended for a `handler` action.\n\t\t * @param {object} extraProps Props merged for an `open-modal` action.\n\t\t * @return {unknown} Whatever the dispatcher returns (a promise for a write).\n\t\t */\n\t\tdispatch(action, extraArgs = [], extraProps = {}) {\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst type = action.type || 'handler'\n\t\t\tlet wrapped = action\n\t\t\tif (type === 'handler') {\n\t\t\t\twrapped = { ...action, args: [...(action.args || []), ...extraArgs] }\n\t\t\t} else if (type === 'open-modal' && Object.keys(extraProps).length > 0) {\n\t\t\t\twrapped = { ...action, props: { ...(action.props || {}), ...extraProps } }\n\t\t\t}\n\t\t\tif (typeof this.cnDispatchAction === 'function') {\n\t\t\t\treturn this.cnDispatchAction(wrapped)\n\t\t\t}\n\t\t\treturn dispatchAction(wrapped, { router: this.$router || null })\n\t\t},\n\n\t\t/**\n\t\t * Whether a drag event is carrying files. A drag of text or of a row\n\t\t * from another table is not a file drop and must not paint the\n\t\t * overlay; `dataTransfer.types` is the only thing readable during a\n\t\t * dragover (the items themselves are not).\n\t\t *\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {boolean}\n\t\t */\n\t\tdragHasFiles(event) {\n\t\t\tconst types = event && event.dataTransfer && event.dataTransfer.types\n\t\t\tif (!types) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Array.prototype.indexOf.call(types, 'Files') !== -1\n\t\t},\n\n\t\t/**\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {void}\n\t\t */\n\t\tonDragEnter(event) {\n\t\t\tif (!this.dropZoneAction || !this.dragHasFiles(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.dragDepth += 1\n\t\t},\n\n\t\t/**\n\t\t * `dragover` must preventDefault or the browser refuses the drop and\n\t\t * navigates to the file instead.\n\t\t *\n\t\t * @param {DragEvent} event The drag event.\n\t\t * @return {void}\n\t\t */\n\t\tonDragOver(event) {\n\t\t\tif (!this.dropZoneAction || !this.dragHasFiles(event)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t},\n\n\t\t/**\n\t\t * Leaving one element of the widget. The event is not read: a\n\t\t * `dragleave` fires for the widget AND for every child crossed, so the\n\t\t * counter — not this event — decides whether the drag is still over us.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonDragLeave() {\n\t\t\tif (!this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.dragDepth = Math.max(0, this.dragDepth - 1)\n\t\t},\n\n\t\t/**\n\t\t * Hand the dropped files to the declared `dropZone` action. Nothing is\n\t\t * uploaded here: this component reads no file content and calls no\n\t\t * write endpoint, so whoever receives the files decides where they go\n\t\t * — the same split the form file field keeps.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tif (!this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.dragHasFiles(event)) {\n\t\t\t\tthis.dragDepth = 0\n\t\t\t\treturn\n\t\t\t}\n\t\t\tevent.preventDefault()\n\t\t\tthis.dragDepth = 0\n\t\t\tconst files = Array.from((event.dataTransfer && event.dataTransfer.files) || [])\n\t\t\tif (files.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event files-dropped Emitted with the dropped files, for a host\n\t\t\t * that wants to handle the drop itself rather than declare an action.\n\t\t\t * @type {Array<File>}\n\t\t\t */\n\t\t\tthis.$emit('files-dropped', files)\n\t\t\tthis.dispatch(this.dropZoneAction, [files], { files })\n\t\t},\n\n\t\t/**\n\t\t * Open the native file picker for the click-to-upload button — the\n\t\t * same `dropZoneAction` a drop dispatches, so a reader who never drags\n\t\t * a file reaches the identical outcome.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\ttriggerUpload() {\n\t\t\tif (this.$refs.uploadInput) {\n\t\t\t\tthis.$refs.uploadInput.click()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle files chosen via the upload button's file picker.\n\t\t *\n\t\t * @param {Event} event The change event.\n\t\t * @return {void}\n\t\t */\n\t\tonUploadFilesSelected(event) {\n\t\t\tconst files = Array.from((event.target && event.target.files) || [])\n\t\t\tif (files.length === 0 || !this.dropZoneAction) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('files-dropped', files)\n\t\t\tthis.dispatch(this.dropZoneAction, [files], { files })\n\t\t\tevent.target.value = ''\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `select` handler — replaces the whole selected-ids\n\t\t * array (it already computed the add/remove/select-all arithmetic).\n\t\t *\n\t\t * @param {Array<string>} ids The new selection.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(ids) {\n\t\t\tthis.selectedIds = Array.isArray(ids) ? ids : []\n\t\t},\n\n\t\t/**\n\t\t * Clear the current selection. Called after a bulk action fires, and\n\t\t * from the bulk bar's own \"Clear selection\" button.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearSelection() {\n\t\t\tthis.selectedIds = []\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a declared bulk action against the current selection, then\n\t\t * clear it — the action owns its own success/error feedback and page\n\t\t * refresh (same convention as a row action), this widget only owns the\n\t\t * checkbox state.\n\t\t *\n\t\t * @param {object} action The declared bulk action.\n\t\t * @return {void}\n\t\t */\n\t\trunBulkAction(action) {\n\t\t\tthis.dispatch(action, [this.selectedRows], { selectedIds: [...this.selectedIds] })\n\t\t\tthis.clearSelection()\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `sort` handler for an interactively-sortable column\n\t\t * (`content.sortable`). Overwrites `localSort`, which both the fetch\n\t\t * params and the table's own sort-indicator props read — a cleared\n\t\t * sort (`key: null`, a third click on a two-state header) goes back to\n\t\t * the unordered default rather than sticking on the last field.\n\t\t *\n\t\t * @param {{key: string|null, order: string|null}} payload The new sort.\n\t\t * @return {void}\n\t\t */\n\t\tonSort({ key, order }) {\n\t\t\tthis.localSort = key ? { field: key, dir: order || 'asc' } : {}\n\t\t},\n\n\t\t/**\n\t\t * CnDataTable's `column-filter` handler (`content.filterable`): the\n\t\t * filter joins the others and the list fetches again from page one.\n\t\t *\n\t\t * @param {{key: string, params: object}} payload The column's filter.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/table-widget-header-controls/specs/table-widget-header-controls/spec.md\n\t\t */\n\t\tonHeaderFilter(payload) {\n\t\t\tthis.headerFilters = nextHeaderFilters(this.headerFilters, payload)\n\t\t},\n\n\t\t/**\n\t\t * Toggle one facet value on/off (`content.facet`).\n\t\t *\n\t\t * @param {string} value The facet value clicked.\n\t\t * @return {void}\n\t\t */\n\t\ttoggleFacetValue(value) {\n\t\t\tconst index = this.facetSelected.indexOf(value)\n\t\t\tif (index === -1) {\n\t\t\t\tthis.facetSelected = [...this.facetSelected, value]\n\t\t\t} else {\n\t\t\t\tthis.facetSelected = this.facetSelected.filter((v) => v !== value)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clear the facet filter, so every loaded row shows again.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tclearFacet() {\n\t\t\tthis.facetSelected = []\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The row a following card describes (content.rowSelection). */\n.cn-object-list-widget :deep(.cn-object-list-widget__row--selected) {\n\tbackground: var(--color-primary-element-light, #eaf0f7);\n}\n\n/* The widget already lives inside CnWidgetWrapper's padded card chrome. The\n table is rendered `borderless` so CnDataTable drops its own border/shadow/\n background AND its bottom margin (the `.cn-table-container--borderless`\n modifier) — otherwise it reads as a card-in-a-card with dead space below.\n Horizontal overflow is owned by `.cn-table-container` (overflow-x: auto); a\n second scroll container here would produce a nested scrollbar, so this host\n stays plain. */\n.cn-object-list-widget {\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tmin-height: 0;\n}\n\n/* The overlay covers the widget while a file drag is over it. `position:\n relative` is set only in the dropping state so a widget without a drop zone\n keeps the containing block it had — an unconditional rule here would\n re-parent any absolutely-positioned descendant a host has placed. */\n.cn-object-list-widget--dropping {\n\tposition: relative;\n}\n\n.cn-object-list-widget__drop-overlay {\n\tposition: absolute;\n\tinset: 0;\n\tz-index: 2;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\ttext-align: center;\n\tpadding: var(--cn-spacing-m, 12px);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large, 12px);\n\tbackground-color: var(--color-primary-element-light);\n\tcolor: var(--color-main-text);\n\t/* The overlay is a painted state, not a target: letting it swallow the\n\t pointer would fire dragleave the instant it appeared under the cursor,\n\t and the drop would land on nothing. */\n\tpointer-events: none;\n}\n\n.cn-object-list-widget__table {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: hidden;\n}\n\n/* The empty state is CnWidgetEmptyState now — it brings its own layout, so\n all this rule still owns is letting it claim the cell's leftover height\n (ADR-062: it fits the cell, it never forces one). */\n.cn-object-list-widget__empty {\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The pager sits flush at the bottom of the card, above \"View all\". */\n.cn-object-list-widget__pager {\n\tflex-shrink: 0;\n}\n\n.cn-object-list-widget__view-all {\n\talign-self: flex-start;\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tfont: inherit;\n\tmargin-top: 4px;\n\tpadding: 4px;\n\ttext-decoration: none;\n}\n\n.cn-object-list-widget__view-all:hover,\n.cn-object-list-widget__view-all:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-object-list-widget__more {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tmargin: 4px 0 0;\n\tpadding: 4px;\n}\n\n.cn-object-list-widget__links {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 16px;\n\tmargin: 12px 0 0;\n\tfont-size: 14px;\n}\n\n.cn-object-list-widget__link {\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__link-note {\n\tmargin-inline-start: 6px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-object-list-widget__footnote {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tmargin: 8px 0 0;\n}\n\n/* Footer Add — same pattern as the integration leaves' footer action\n (\"Open in Calendar\"): full-width, centered, divider-topped, pinned to\n the card bottom. */\n.cn-object-list-widget__add {\n\talign-self: stretch;\n\tborder-top: 1px solid var(--color-border);\n\t/* Bleed through the host card's 16px content padding so the divider\n\t spans edge-to-edge, exactly like the integration leaves' footer. */\n\tmargin: auto -16px -16px;\n\tpadding: 4px;\n}\n\n.cn-object-list-widget__error {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tmargin: 4px 0 0;\n}\n\n.cn-object-list-widget__prompt {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n\tpadding: 16px 4px;\n\tmargin: 0;\n\ttext-align: center;\n}\n\n.cn-object-list-widget__facet {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-wrap: wrap;\n\tgap: 6px;\n\tpadding: 0 4px 8px;\n}\n\n.cn-object-list-widget__facet-label {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__facet-chip {\n\tbackground-color: var(--color-background-hover);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-pill, 16px);\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n\tfont: inherit;\n\tfont-size: 0.85em;\n\tpadding: 2px 10px;\n}\n\n.cn-object-list-widget__facet-chip--active {\n\tbackground-color: var(--color-primary-element-light);\n\tborder-color: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text, var(--color-main-text));\n}\n\n.cn-object-list-widget__facet-clear,\n.cn-object-list-widget__bulk-clear {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tcursor: pointer;\n\tfont: inherit;\n\tfont-size: 0.85em;\n\tpadding: 2px 4px;\n}\n\n.cn-object-list-widget__facet-clear:hover,\n.cn-object-list-widget__bulk-clear:hover {\n\ttext-decoration: underline;\n}\n\n.cn-object-list-widget__bulk-bar {\n\talign-items: center;\n\tbackground-color: var(--color-primary-element-light);\n\tborder-radius: var(--border-radius, 8px);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tpadding: 6px 10px;\n}\n\n.cn-object-list-widget__bulk-count {\n\tfont-weight: 600;\n}\n\n.cn-object-list-widget__bulk-action {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tcursor: pointer;\n\tfont: inherit;\n\tpadding: 4px 10px;\n}\n\n.cn-object-list-widget__bulk-action--destructive {\n\tborder-color: var(--color-error);\n\tcolor: var(--color-error);\n}\n\n.cn-object-list-widget__groups {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 16px;\n\tmin-height: 0;\n\toverflow: auto;\n}\n\n.cn-object-list-widget__group-heading {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tmargin: 0 0 4px;\n\ttext-transform: uppercase;\n}\n\n.cn-object-list-widget__upload {\n\tbackground-color: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius, 8px);\n\tcursor: pointer;\n\tfont: inherit;\n\tmargin: 8px 0 0;\n\tpadding: 6px 12px;\n}\n\n.cn-object-list-widget__upload-input {\n\tdisplay: none;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_createBlock","_createVNode","_createTextVNode","_createElementVNode","_Fragment","_renderList","_openBlock","_normalizeClass","_resolveDynamicComponent","_withCtx"],"mappings":"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"}