@conduction/nextcloud-vue 2.56.0-beta.29 → 2.56.0-beta.30

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 (197) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
  5. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  6. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
  7. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
  28. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/index.js +30 -0
  32. package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
  34. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
  35. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
  39. package/dist/esm/components/CnFieldListWidget/index.js +29 -0
  40. package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
  46. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  48. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
  49. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
  53. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
  54. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
  55. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
  56. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
  57. package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
  58. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
  59. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  60. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
  66. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRecordListWidget/index.js +31 -0
  68. package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
  69. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
  70. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
  71. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
  72. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
  73. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
  74. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
  75. package/dist/esm/components/CnSectionsWidget/index.js +35 -0
  76. package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
  84. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  85. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
  86. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  87. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  88. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
  92. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  93. package/dist/esm/composables/useFormDraft.js +140 -9
  94. package/dist/esm/composables/useFormDraft.js.map +1 -1
  95. package/dist/esm/index.js +4 -0
  96. package/dist/esm/index.js.map +1 -1
  97. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
  98. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
  99. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
  100. package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
  101. package/dist/esm/integrations/builtin/field-inspection.js +21 -0
  102. package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
  103. package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
  104. package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
  105. package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
  106. package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
  107. package/dist/esm/integrations/offline/index.js +2 -1
  108. package/dist/esm/integrations/offline/index.js.map +1 -1
  109. package/dist/esm/integrations/offline/offlineDb.js +6 -1
  110. package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
  111. package/dist/esm/integrations/offline/syncReplayService.js +8 -2
  112. package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
  113. package/dist/esm/l10n/en.json.js +12 -1
  114. package/dist/esm/l10n/en.json.js.map +1 -1
  115. package/dist/esm/l10n/nl.json.js +16 -2
  116. package/dist/esm/l10n/nl.json.js.map +1 -1
  117. package/dist/esm/nextcloud-vue.css +1000 -59
  118. package/dist/esm/utils/detailActionModel.js +60 -3
  119. package/dist/esm/utils/detailActionModel.js.map +1 -1
  120. package/dist/esm/utils/fillTemplate.js +114 -0
  121. package/dist/esm/utils/fillTemplate.js.map +1 -0
  122. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  123. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  124. package/dist/esm/utils/queryPresets.js +96 -0
  125. package/dist/esm/utils/queryPresets.js.map +1 -0
  126. package/dist/esm/utils/resolveFilterTokens.js +11 -0
  127. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  128. package/dist/esm/utils/schema.js +21 -0
  129. package/dist/esm/utils/schema.js.map +1 -1
  130. package/dist/esm/utils/sentinelTokens.js +3 -3
  131. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  132. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  133. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  134. package/dist/esm/utils/widgetDispatch.js +7 -1
  135. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  136. package/dist/nextcloud-vue.cjs.js +12368 -7717
  137. package/dist/nextcloud-vue.cjs.js.map +2 -2
  138. package/dist/nextcloud-vue.css +1000 -59
  139. package/l10n/en.json +12 -1
  140. package/l10n/nl.json +16 -2
  141. package/package.json +1 -1
  142. package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
  143. package/src/components/CnActionsBar/CnActionsBar.md +4 -0
  144. package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
  145. package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
  146. package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
  147. package/src/components/CnDataTable/CnDataTable.vue +1 -0
  148. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  149. package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
  150. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
  151. package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
  152. package/src/components/CnFactsWidget/index.js +29 -0
  153. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
  154. package/src/components/CnFieldListWidget/index.js +28 -0
  155. package/src/components/CnFormDialog/CnFormDialog.md +9 -3
  156. package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
  157. package/src/components/CnFormPage/CnFormPage.vue +18 -3
  158. package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
  159. package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
  160. package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
  161. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
  162. package/src/components/CnRecordListWidget/index.js +30 -0
  163. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
  164. package/src/components/CnSectionsWidget/index.js +34 -0
  165. package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
  166. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  167. package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
  169. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
  170. package/src/components/index.js +4 -0
  171. package/src/composables/__tests__/useFormDraft.spec.js +69 -0
  172. package/src/composables/useFormDraft.js +139 -8
  173. package/src/css/form-field.css +14 -0
  174. package/src/css/index-page.css +71 -0
  175. package/src/css/kpi-card.css +32 -0
  176. package/src/css/look-board-detail.css +77 -0
  177. package/src/css/look-board-index.css +51 -0
  178. package/src/css/look-board.css +37 -2
  179. package/src/css/table.css +19 -0
  180. package/src/index.js +4 -0
  181. package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
  182. package/src/integrations/builtin/field-inspection.js +21 -0
  183. package/src/integrations/offline/checklistSubmission.js +137 -0
  184. package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
  185. package/src/integrations/offline/index.js +3 -0
  186. package/src/integrations/offline/offlineDb.js +6 -1
  187. package/src/integrations/offline/syncReplayService.js +8 -2
  188. package/src/schemas/app-manifest-v2.schema.json +104 -5
  189. package/src/utils/detailActionModel.js +59 -2
  190. package/src/utils/fillTemplate.js +110 -0
  191. package/src/utils/libraryWidgetKeys.js +4 -0
  192. package/src/utils/queryPresets.js +93 -0
  193. package/src/utils/resolveFilterTokens.js +11 -0
  194. package/src/utils/schema.js +21 -0
  195. package/src/utils/sentinelTokens.js +3 -3
  196. package/src/utils/validateManifestV2.compiled.js +3 -3
  197. package/src/utils/widgetDispatch.js +7 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CnWorkspaceFilterWidget.vue2.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-workspace-filter-widget\" data-testid=\"cn-workspace-filter-widget\">\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<div v-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, default?: (string|number)}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnWorkspaceFilterWidget.vue2.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-workspace-filter-widget\"\n\t\t:class=\"{ 'cn-workspace-filter-widget--start': isStart }\"\n\t\tdata-testid=\"cn-workspace-filter-widget\">\n\t\t<!-- The start card (`layout: \"start\"`): a title and one line of\n\t\t guidance beside the control, as the \"Contact starten\" card. -->\n\t\t<div v-if=\"isStart && (title || description)\" class=\"cn-workspace-filter-widget__text\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-workspace-filter-widget__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-workspace-filter-widget__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<!-- An object picker (`objectSource`): searches OpenRegister on the\n\t\t server, so it reaches every object, not a fetched page. -->\n\t\t<CnResourceSelect\n\t\t\tv-if=\"objectSource\"\n\t\t\tclass=\"cn-workspace-filter-widget__control\"\n\t\t\t:register=\"objectSource.register\"\n\t\t\t:schema=\"objectSource.schema\"\n\t\t\t:labelField=\"objectSource.labelField || 'title'\"\n\t\t\t:modelValue=\"selected === undefined || selected === null ? '' : String(selected)\"\n\t\t\t:placeholder=\"tr(objectSource.placeholder || '')\"\n\t\t\t:ariaLabelCombobox=\"tr(objectSource.ariaLabel || '') || title || label || writeKey\"\n\t\t\t:preload=\"objectSource.preload !== false\"\n\t\t\t:clearable=\"Boolean(content.allLabel)\"\n\t\t\t:allowCreate=\"false\"\n\t\t\tdata-testid=\"cn-workspace-filter-object\"\n\t\t\t@update:modelValue=\"applySelection($event || '')\" />\n\n\t\t<div v-else-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<component\n\t\t\t:is=\"startLink.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"startLink\"\n\t\t\t:to=\"startLink.to || undefined\"\n\t\t\t:href=\"startLink.href || undefined\"\n\t\t\tclass=\"cn-workspace-filter-widget__link\"\n\t\t\tdata-testid=\"cn-workspace-filter-link\">\n\t\t\t{{ startLink.label }}\n\t\t</component>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** Host translate function, for the start card's texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * Start card keys (abstract-start-card): `layout: \"start\"` draws\n\t\t * `title` and `description` beside the control, and `link`\n\t\t * (`{ label, route? | href? }`) after it. `objectSource`\n\t\t * (`{ register, schema, labelField?, placeholder?, ariaLabel?, preload? }`)\n\t\t * makes the control an OpenRegister object picker that searches on the\n\t\t * server and writes the picked object's id; without a selection and\n\t\t * without `allLabel` it selects the first object.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Whether the widget draws the start card. */\n\t\tisStart() {\n\t\t\treturn this.content.layout === 'start'\n\t\t},\n\n\t\t/** The start card's title, translated. */\n\t\ttitle() {\n\t\t\treturn this.tr(this.content.title || '')\n\t\t},\n\n\t\t/** The start card's guidance line, translated. */\n\t\tdescription() {\n\t\t\treturn this.tr(this.content.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The link after the control, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tstartLink() {\n\t\t\tconst link = this.content.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route && typeof link.route === 'object' && link.route.name) {\n\t\t\t\treturn { label: this.tr(link.label), to: link.route }\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), href: link.href }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The object picker declaration, or null.\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tobjectSource() {\n\t\t\tconst src = this.content.objectSource\n\t\t\treturn src && typeof src === 'object' && src.register && src.schema ? src : null\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.objectSource) {\n\t\t\t\tawait this.seedFirstObject()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The object picker's default: with nothing selected, no `default` and\n\t\t * no `allLabel`, select the first object the source lists, so the page\n\t\t * always has one in focus. Fail-safe: a failed read selects nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync seedFirstObject() {\n\t\t\tif ((this.selected !== undefined && this.selected !== '') || this.content.allLabel || this.content.default !== undefined) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst src = this.objectSource\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('/apps/openregister/api/objects/{register}/{schema}', { register: src.register, schema: src.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1 } })\n\t\t\t\tconst data = res && res.data\n\t\t\t\tconst rows = Array.isArray(data) ? data : ((data && data.results) || [])\n\t\t\t\tconst first = rows[0]\n\t\t\t\tconst id = first && (first.id || (first['@self'] && first['@self'].id))\n\t\t\t\tif (id && (this.selected === undefined || this.selected === '') && this.readWorkspace() === undefined) {\n\t\t\t\t\tthis.selected = String(id)\n\t\t\t\t\tthis.writeWorkspace(String(id))\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Nothing selected: the siblings wait for a pick.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n/* The start card: text left, control (and link) right, as PqDashboard's \"Contact starten\". */\n.cn-workspace-filter-widget--start {\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n}\n\n.cn-workspace-filter-widget__text {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-workspace-filter-widget__title {\n\tmargin: 0 0 4px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__description {\n\tmargin: 0;\n\tmax-width: 420px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-workspace-filter-widget--start .cn-workspace-filter-widget__control,\n.cn-workspace-filter-widget--start > .v-select {\n\tflex: 0 1 480px;\n\tmin-width: 240px;\n}\n\n.cn-workspace-filter-widget__link {\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n\twhite-space: nowrap;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["CnResourceSelect","t"],"mappings":"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"}
@@ -1,76 +1,128 @@
1
- import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createBlock, Fragment, renderList, createVNode, withCtx, createElementVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, toDisplayString, createCommentVNode, createBlock, Fragment, renderList, createVNode, withCtx, createElementVNode, resolveDynamicComponent, createTextVNode } from 'vue';
2
2
 
3
3
  const _hoisted_1 = {
4
- class: "cn-workspace-filter-widget",
5
- "data-testid": "cn-workspace-filter-widget"
4
+ key: 0,
5
+ class: "cn-workspace-filter-widget__text"
6
6
  };
7
7
  const _hoisted_2 = {
8
8
  key: 0,
9
- class: "cn-workspace-filter-widget__label"
9
+ class: "cn-workspace-filter-widget__title"
10
10
  };
11
11
  const _hoisted_3 = {
12
12
  key: 1,
13
+ class: "cn-workspace-filter-widget__description"
14
+ };
15
+ const _hoisted_4 = {
16
+ key: 1,
17
+ class: "cn-workspace-filter-widget__label"
18
+ };
19
+ const _hoisted_5 = {
20
+ key: 3,
13
21
  class: "cn-workspace-filter-widget__status"
14
22
  };
15
- const _hoisted_4 = ["aria-label"];
16
- const _hoisted_5 = { class: "cn-workspace-filter-widget__option-label" };
17
- const _hoisted_6 = {
23
+ const _hoisted_6 = ["aria-label"];
24
+ const _hoisted_7 = { class: "cn-workspace-filter-widget__option-label" };
25
+ const _hoisted_8 = {
18
26
  key: 0,
19
27
  class: "cn-workspace-filter-widget__count"
20
28
  };
21
29
 
22
30
  function render(_ctx, _cache, $props, $setup, $data, $options) {
31
+ const _component_CnResourceSelect = resolveComponent("CnResourceSelect");
23
32
  const _component_NcSelect = resolveComponent("NcSelect");
24
33
  const _component_NcCheckboxRadioSwitch = resolveComponent("NcCheckboxRadioSwitch");
25
34
 
26
- return (openBlock(), createElementBlock("div", _hoisted_1, [
35
+ return (openBlock(), createElementBlock("div", {
36
+ class: normalizeClass(["cn-workspace-filter-widget", { 'cn-workspace-filter-widget--start': $options.isStart }]),
37
+ "data-testid": "cn-workspace-filter-widget"
38
+ }, [
39
+ ($options.isStart && ($options.title || $options.description))
40
+ ? (openBlock(), createElementBlock("div", _hoisted_1, [
41
+ ($options.title)
42
+ ? (openBlock(), createElementBlock("h3", _hoisted_2, toDisplayString($options.title), 1 /* TEXT */))
43
+ : createCommentVNode("v-if", true),
44
+ ($options.description)
45
+ ? (openBlock(), createElementBlock("p", _hoisted_3, toDisplayString($options.description), 1 /* TEXT */))
46
+ : createCommentVNode("v-if", true)
47
+ ]))
48
+ : createCommentVNode("v-if", true),
27
49
  ($options.label)
28
- ? (openBlock(), createElementBlock("p", _hoisted_2, toDisplayString($options.label), 1 /* TEXT */))
50
+ ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.label), 1 /* TEXT */))
29
51
  : createCommentVNode("v-if", true),
30
- ($data.loading)
31
- ? (openBlock(), createElementBlock("div", _hoisted_3, toDisplayString($options.loadingLabel), 1 /* TEXT */))
32
- : ($options.style === 'select')
33
- ? (openBlock(), createBlock(_component_NcSelect, {
34
- key: 2,
35
- modelValue: $options.selectedOption,
36
- options: $options.displayOptions,
37
- clearable: false,
38
- inputLabel: $options.label || $options.writeKey,
39
- label: "label",
40
- "data-testid": "cn-workspace-filter-select",
41
- "onUpdate:modelValue": $options.onSelect
42
- }, null, 8 /* PROPS */, ["modelValue", "options", "inputLabel", "onUpdate:modelValue"]))
43
- : (openBlock(), createElementBlock("ul", {
44
- key: 3,
45
- class: "cn-workspace-filter-widget__list",
46
- role: "radiogroup",
47
- "aria-label": $options.label || $options.writeKey
48
- }, [
49
- (openBlock(true), createElementBlock(Fragment, null, renderList($options.displayOptions, (option) => {
50
- return (openBlock(), createElementBlock("li", {
51
- key: String(option.value),
52
- class: "cn-workspace-filter-widget__item"
53
- }, [
54
- createVNode(_component_NcCheckboxRadioSwitch, {
55
- type: "radio",
56
- name: $options.radioGroupName,
57
- modelValue: $options.isActive(option) ? String(option.value) : '',
58
- value: String(option.value),
59
- "data-testid": `cn-workspace-filter-option-${option.value}`,
60
- "onUpdate:modelValue": $event => ($options.onRadioPick(option))
61
- }, {
62
- default: withCtx(() => [
63
- createElementVNode("span", _hoisted_5, toDisplayString(option.label), 1 /* TEXT */),
64
- ($options.showCounts && option.count !== undefined && option.count !== null)
65
- ? (openBlock(), createElementBlock("span", _hoisted_6, toDisplayString(option.count), 1 /* TEXT */))
66
- : createCommentVNode("v-if", true)
67
- ]),
68
- _: 2 /* DYNAMIC */
69
- }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["name", "modelValue", "value", "data-testid", "onUpdate:modelValue"])
70
- ]))
71
- }), 128 /* KEYED_FRAGMENT */))
72
- ], 8 /* PROPS */, _hoisted_4))
73
- ]))
52
+ ($options.objectSource)
53
+ ? (openBlock(), createBlock(_component_CnResourceSelect, {
54
+ key: 2,
55
+ class: "cn-workspace-filter-widget__control",
56
+ register: $options.objectSource.register,
57
+ schema: $options.objectSource.schema,
58
+ labelField: $options.objectSource.labelField || 'title',
59
+ modelValue: $data.selected === undefined || $data.selected === null ? '' : String($data.selected),
60
+ placeholder: $options.tr($options.objectSource.placeholder || ''),
61
+ ariaLabelCombobox: $options.tr($options.objectSource.ariaLabel || '') || $options.title || $options.label || $options.writeKey,
62
+ preload: $options.objectSource.preload !== false,
63
+ clearable: Boolean($props.content.allLabel),
64
+ allowCreate: false,
65
+ "data-testid": "cn-workspace-filter-object",
66
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ($options.applySelection($event || '')))
67
+ }, null, 8 /* PROPS */, ["register", "schema", "labelField", "modelValue", "placeholder", "ariaLabelCombobox", "preload", "clearable"]))
68
+ : ($data.loading)
69
+ ? (openBlock(), createElementBlock("div", _hoisted_5, toDisplayString($options.loadingLabel), 1 /* TEXT */))
70
+ : ($options.style === 'select')
71
+ ? (openBlock(), createBlock(_component_NcSelect, {
72
+ key: 4,
73
+ modelValue: $options.selectedOption,
74
+ options: $options.displayOptions,
75
+ clearable: false,
76
+ inputLabel: $options.label || $options.writeKey,
77
+ label: "label",
78
+ "data-testid": "cn-workspace-filter-select",
79
+ "onUpdate:modelValue": $options.onSelect
80
+ }, null, 8 /* PROPS */, ["modelValue", "options", "inputLabel", "onUpdate:modelValue"]))
81
+ : (openBlock(), createElementBlock("ul", {
82
+ key: 5,
83
+ class: "cn-workspace-filter-widget__list",
84
+ role: "radiogroup",
85
+ "aria-label": $options.label || $options.writeKey
86
+ }, [
87
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.displayOptions, (option) => {
88
+ return (openBlock(), createElementBlock("li", {
89
+ key: String(option.value),
90
+ class: "cn-workspace-filter-widget__item"
91
+ }, [
92
+ createVNode(_component_NcCheckboxRadioSwitch, {
93
+ type: "radio",
94
+ name: $options.radioGroupName,
95
+ modelValue: $options.isActive(option) ? String(option.value) : '',
96
+ value: String(option.value),
97
+ "data-testid": `cn-workspace-filter-option-${option.value}`,
98
+ "onUpdate:modelValue": $event => ($options.onRadioPick(option))
99
+ }, {
100
+ default: withCtx(() => [
101
+ createElementVNode("span", _hoisted_7, toDisplayString(option.label), 1 /* TEXT */),
102
+ ($options.showCounts && option.count !== undefined && option.count !== null)
103
+ ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(option.count), 1 /* TEXT */))
104
+ : createCommentVNode("v-if", true)
105
+ ]),
106
+ _: 2 /* DYNAMIC */
107
+ }, 1032 /* PROPS, DYNAMIC_SLOTS */, ["name", "modelValue", "value", "data-testid", "onUpdate:modelValue"])
108
+ ]))
109
+ }), 128 /* KEYED_FRAGMENT */))
110
+ ], 8 /* PROPS */, _hoisted_6)),
111
+ ($options.startLink)
112
+ ? (openBlock(), createBlock(resolveDynamicComponent($options.startLink.to ? 'router-link' : 'a'), {
113
+ key: 6,
114
+ to: $options.startLink.to || undefined,
115
+ href: $options.startLink.href || undefined,
116
+ class: "cn-workspace-filter-widget__link",
117
+ "data-testid": "cn-workspace-filter-link"
118
+ }, {
119
+ default: withCtx(() => [
120
+ createTextVNode(toDisplayString($options.startLink.label), 1 /* TEXT */)
121
+ ]),
122
+ _: 1 /* STABLE */
123
+ }, 8 /* PROPS */, ["to", "href"]))
124
+ : createCommentVNode("v-if", true)
125
+ ], 2 /* CLASS */))
74
126
  }
75
127
 
76
128
  export { render };
@@ -1 +1 @@
1
- {"version":3,"file":"CnWorkspaceFilterWidget.vue3.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue?vue&type=template&id=f0d5c1c6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-workspace-filter-widget\" data-testid=\"cn-workspace-filter-widget\">\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<div v-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, default?: (string|number)}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_createElementVNode","_openBlock"],"mappings":";;;AAWK,EAAA,KAAM,EAAC,4BAA4B;AAAC,EAAA,aAAW,EAAC,4BAAW;;;;OAC5C,EAAA,mCAAA;;;;AAIV,EAAA,KAAA,EAAA,oCAAA;;;AA+BH,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,0CAAA,GAAA;;;;;;;;;;;KAnCI,QAAK,CAAA,KAAA;sBAAbA,wBAAA,4BACG,QAAC,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;;KAGG,KAAA,CAAA,OAAA;sBAARA,kBAEM,CAAA,KAAA,EAFN,UAEM,EAAAC,eAAA,CADF,QAAY,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;SAKJ,QAAC,CAAA,KAAA,KAAA,QAAA;mCAOI,CAAA,mBAAA,EAAA;;AANf,YAAA,UAAU,EAAE,QAAc,CAAA,cAAA;AAC1B,YAAA,OAAO,EAAE,QAAc,CAAA,cAAA;;YAEvB,UAAU,EAAE,QAAK,CAAA,KAAA,IAAI,QAAS,CAAA,QAAA;AAC/B,YAAA,KAAK,EAAC,OAAE;AACR,YAAA,aAAW,EAAC,4BAA2B;AACtC,YAAA,qBAAe,EAAA,QAAA,CAAA,QAAA;;;;iBAIX,EAAC,kCAAqB;AAC1B,YAAA,IAAG,EAAC,YAAY;YAChB,YAAU,EAAE,QAAK,CAAA,KAAA,IAAI,QAAA,CAAA,QAAA;;4EAEJ,QAAK,CAAA,cAAA,EAAA,CAAf,MAAM,KAAA;;AACZ,gBAAA,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;qBACpB,EAAC,kCAAuB;;;AAE5B,kBAAA,IAAI,EAAC,OAAO;AACX,kBAAA,IAAG,EAAG,QAAc,CAAA,cAAA;kBACpB,UAAG,EAAA,iBAAA,CAAA,MAAA,IAAA,MAAA,CAAA,MAAA,CAAA,KAAA,CAAA,GAAA,EAAA;AACH,kBAAA,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;kBAC1B,aAAW,EAAA,CAAA,2BAAA,EAAgC,MAAM,CAAC,KAAK,CAAA,CAAA;kBACvD,qBAAiB,EAAA,MAAA,KAAE,QAAE,CAAA,WAAA,CAAA,MAAA,CAAA,CAAA;;mCACtB,MAAE;AAAF,oBAAAC,kBAAA,CAAE,MAAF,EAAA,UAAE,EAAAD,eAAA,CAAA,MAAA,CAAA,KAAA,CAAA,EAAA,CAAA,YAAA;qBAED,QAAA,CAAA,UAAA,IAAA,MAAA,CAAA,KAAA,KAAA,SAAA,IAAA,MAAA,CAAA,KAAA,KAAA,IAAA;AADD,yBAAAE,SAAA,EAAA,EAAAH,kBAAA,OAAA,EAAA;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnWorkspaceFilterWidget.vue3.js","sources":["../../../../src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue?vue&type=template&id=f0d5c1c6&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-workspace-filter-widget\"\n\t\t:class=\"{ 'cn-workspace-filter-widget--start': isStart }\"\n\t\tdata-testid=\"cn-workspace-filter-widget\">\n\t\t<!-- The start card (`layout: \"start\"`): a title and one line of\n\t\t guidance beside the control, as the \"Contact starten\" card. -->\n\t\t<div v-if=\"isStart && (title || description)\" class=\"cn-workspace-filter-widget__text\">\n\t\t\t<h3 v-if=\"title\" class=\"cn-workspace-filter-widget__title\">\n\t\t\t\t{{ title }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"description\" class=\"cn-workspace-filter-widget__description\">\n\t\t\t\t{{ description }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p v-if=\"label\" class=\"cn-workspace-filter-widget__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\n\t\t<!-- An object picker (`objectSource`): searches OpenRegister on the\n\t\t server, so it reaches every object, not a fetched page. -->\n\t\t<CnResourceSelect\n\t\t\tv-if=\"objectSource\"\n\t\t\tclass=\"cn-workspace-filter-widget__control\"\n\t\t\t:register=\"objectSource.register\"\n\t\t\t:schema=\"objectSource.schema\"\n\t\t\t:labelField=\"objectSource.labelField || 'title'\"\n\t\t\t:modelValue=\"selected === undefined || selected === null ? '' : String(selected)\"\n\t\t\t:placeholder=\"tr(objectSource.placeholder || '')\"\n\t\t\t:ariaLabelCombobox=\"tr(objectSource.ariaLabel || '') || title || label || writeKey\"\n\t\t\t:preload=\"objectSource.preload !== false\"\n\t\t\t:clearable=\"Boolean(content.allLabel)\"\n\t\t\t:allowCreate=\"false\"\n\t\t\tdata-testid=\"cn-workspace-filter-object\"\n\t\t\t@update:modelValue=\"applySelection($event || '')\" />\n\n\t\t<div v-else-if=\"loading\" class=\"cn-workspace-filter-widget__status\">\n\t\t\t{{ loadingLabel }}\n\t\t</div>\n\n\t\t<!-- Select style -->\n\t\t<NcSelect\n\t\t\tv-else-if=\"style === 'select'\"\n\t\t\t:modelValue=\"selectedOption\"\n\t\t\t:options=\"displayOptions\"\n\t\t\t:clearable=\"false\"\n\t\t\t:inputLabel=\"label || writeKey\"\n\t\t\tlabel=\"label\"\n\t\t\tdata-testid=\"cn-workspace-filter-select\"\n\t\t\t@update:modelValue=\"onSelect\" />\n\n\t\t<!-- Radio-list style (default) -->\n\t\t<ul v-else\n\t\t\tclass=\"cn-workspace-filter-widget__list\"\n\t\t\trole=\"radiogroup\"\n\t\t\t:aria-label=\"label || writeKey\">\n\t\t\t<li\n\t\t\t\tv-for=\"option in displayOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-workspace-filter-widget__item\">\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\ttype=\"radio\"\n\t\t\t\t\t:name=\"radioGroupName\"\n\t\t\t\t\t:modelValue=\"isActive(option) ? String(option.value) : ''\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:data-testid=\"`cn-workspace-filter-option-${option.value}`\"\n\t\t\t\t\t@update:modelValue=\"onRadioPick(option)\">\n\t\t\t\t\t<span class=\"cn-workspace-filter-widget__option-label\">{{ option.label }}</span>\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"showCounts && option.count !== undefined && option.count !== null\"\n\t\t\t\t\t\tclass=\"cn-workspace-filter-widget__count\">{{ option.count }}</span>\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t</li>\n\t\t</ul>\n\n\t\t<component\n\t\t\t:is=\"startLink.to ? 'router-link' : 'a'\"\n\t\t\tv-if=\"startLink\"\n\t\t\t:to=\"startLink.to || undefined\"\n\t\t\t:href=\"startLink.href || undefined\"\n\t\t\tclass=\"cn-workspace-filter-widget__link\"\n\t\t\tdata-testid=\"cn-workspace-filter-link\">\n\t\t\t{{ startLink.label }}\n\t\t</component>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'\nimport CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'\nimport { fetchEndpointSource } from '../../composables/useEndpointSource.js'\nimport { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'\n\n/**\n * CnWorkspaceFilterWidget — a dashboard choice-list that writes the selected\n * value into the page workspace context so sibling widgets refetch (#91\n * Wave 3, the `workspace-filter` widget).\n *\n * Options come from ONE of three sources (checked in order):\n * 1. static `options[]` — each `{ value, label, count? }`.\n * 2. an OpenRegister `source` — `{ register, schema, groupBy, filter? }`,\n * fetched from OR's `/grouped` facet so each distinct value becomes an\n * option with its object `count`.\n * 3. an `endpointSource` — `{ url, params?, responsePath? }` returning an\n * array of `{ value, label, count? }` (or `{ id, name }` / bare strings,\n * normalised).\n *\n * The chosen value is written to the workspace context key named by `writes`\n * (`\"@workspace.<key>\"` or a bare `\"<key>\"`). CnDashboardPage provides\n * `cnWorkspaceContext`; every sibling widget whose declarative source\n * interpolates `@workspace.<key>` re-resolves its tokens and refetches when\n * the selection changes — the pipelinq WerkplekQueueFilter contract.\n *\n * An optional leading \"All\" option (`allLabel`) clears the key (writes the\n * empty string), so an optional `@workspace.<key>?` token drops and siblings\n * show the unfiltered set.\n *\n * ```js\n * content: {\n * label: 'Queue',\n * writes: '@workspace.queue',\n * style: 'radio', // 'radio' (default) | 'select'\n * allLabel: 'All',\n * showCounts: true,\n * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnWorkspaceFilterWidget',\n\n\tcomponents: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },\n\n\tinject: {\n\t\t/**\n\t\t * Page-level workspace context. CnDashboardPage provides a `ref({})`;\n\t\t * Vue 2.7's Options-API inject AUTO-UNWRAPS it, so this is the plain\n\t\t * reactive bag (the raw-ref `{ value }` shape is also handled on write).\n\t\t * Null on pages without one (the widget then just emits `@change`).\n\t\t */\n\t\tcnWorkspaceContext: { default: null },\n\t\t/** Host translate function, for the start card's texts. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Persisted configuration blob.\n\t\t *\n\t\t * Start card keys (abstract-start-card): `layout: \"start\"` draws\n\t\t * `title` and `description` beside the control, and `link`\n\t\t * (`{ label, route? | href? }`) after it. `objectSource`\n\t\t * (`{ register, schema, labelField?, placeholder?, ariaLabel?, preload? }`)\n\t\t * makes the control an OpenRegister object picker that searches on the\n\t\t * server and writes the picked object's id; without a selection and\n\t\t * without `allLabel` it selects the first object.\n\t\t *\n\t\t * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Fetched options (from source / endpointSource); static options use content.options directly. */\n\t\t\tfetchedOptions: null,\n\t\t\t/** True while options are being fetched. */\n\t\t\tloading: false,\n\t\t\t/** The currently-selected value (mirrors the workspace key). */\n\t\t\tselected: undefined,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Section label. */\n\t\tlabel() {\n\t\t\treturn this.content.label || ''\n\t\t},\n\n\t\t/** Whether the widget draws the start card. */\n\t\tisStart() {\n\t\t\treturn this.content.layout === 'start'\n\t\t},\n\n\t\t/** The start card's title, translated. */\n\t\ttitle() {\n\t\t\treturn this.tr(this.content.title || '')\n\t\t},\n\n\t\t/** The start card's guidance line, translated. */\n\t\tdescription() {\n\t\t\treturn this.tr(this.content.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The link after the control, or null.\n\t\t *\n\t\t * @return {{label: string, to?: object, href?: string}|null}\n\t\t */\n\t\tstartLink() {\n\t\t\tconst link = this.content.link\n\t\t\tif (!link || typeof link !== 'object' || !link.label) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (link.route && typeof link.route === 'object' && link.route.name) {\n\t\t\t\treturn { label: this.tr(link.label), to: link.route }\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn { label: this.tr(link.label), href: link.href }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The object picker declaration, or null.\n\t\t *\n\t\t * @return {{register: string, schema: string, labelField?: string}|null}\n\t\t */\n\t\tobjectSource() {\n\t\t\tconst src = this.content.objectSource\n\t\t\treturn src && typeof src === 'object' && src.register && src.schema ? src : null\n\t\t},\n\n\t\t/** Choice style: 'select' or 'radio' (default). */\n\t\tstyle() {\n\t\t\treturn this.content.style === 'select' ? 'select' : 'radio'\n\t\t},\n\n\t\t/** Whether to render per-option counts. */\n\t\tshowCounts() {\n\t\t\treturn this.content.showCounts !== false\n\t\t},\n\n\t\t/** The bare workspace key the selection writes (strips a `@workspace.` / `@page.` prefix). */\n\t\twriteKey() {\n\t\t\tconst w = this.content.writes || ''\n\t\t\treturn String(w).replace(/^@(workspace|page)\\./, '')\n\t\t},\n\n\t\t/** A stable radiogroup name so the radios are mutually exclusive. */\n\t\tradioGroupName() {\n\t\t\treturn `cn-wsf-${this.writeKey || 'filter'}`\n\t\t},\n\n\t\t/** The option list actually rendered — the optional \"All\" entry prepended. */\n\t\tdisplayOptions() {\n\t\t\tconst base = this.normalisedOptions\n\t\t\tif (this.content.allLabel) {\n\t\t\t\treturn [{ value: '', label: this.content.allLabel }, ...base]\n\t\t\t}\n\t\t\treturn base\n\t\t},\n\n\t\t/** Normalised `{ value, label, count? }` options from whichever source is configured. */\n\t\tnormalisedOptions() {\n\t\t\tconst raw = this.fetchedOptions !== null\n\t\t\t\t? this.fetchedOptions\n\t\t\t\t: (Array.isArray(this.content.options) ? this.content.options : [])\n\t\t\treturn raw.map((o) => this.normaliseOption(o)).filter((o) => o.value !== undefined && o.value !== null)\n\t\t},\n\n\t\t/** The selected option object (for NcSelect). */\n\t\tselectedOption() {\n\t\t\treturn this.displayOptions.find((o) => String(o.value) === String(this.selected)) || null\n\t\t},\n\n\t\t/**\n\t\t * The reactive workspace value for this key — watched so an external\n\t\t * write (another widget) syncs the local selection.\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\tworkspaceValue() {\n\t\t\treturn this.readWorkspace()\n\t\t},\n\n\t\tloadingLabel() {\n\t\t\treturn t('nextcloud-vue', 'Loading…')\n\t\t},\n\t},\n\n\twatch: {\n\t\t/** Re-fetch options when a dynamic source changes. */\n\t\tcontent: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadOptions()\n\t\t\t},\n\t\t},\n\n\t\t/**\n\t\t * Track external workspace changes (another widget writing the same key).\n\t\t *\n\t\t * @param {unknown} val The new workspace value for this key.\n\t\t */\n\t\tworkspaceValue(val) {\n\t\t\tif (val !== undefined) {\n\t\t\t\tthis.selected = val\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Seed the selection: existing workspace value → content.default → the\n\t\t// \"All\" clear option (empty) when present, else the first option once\n\t\t// loaded (handled in loadOptions).\n\t\tconst existing = this.readWorkspace()\n\t\tif (existing !== undefined) {\n\t\t\tthis.selected = existing\n\t\t} else if (this.content.default !== undefined) {\n\t\t\tthis.selected = this.content.default\n\t\t\tthis.writeWorkspace(this.content.default)\n\t\t}\n\t\tthis.loadOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Normalise a raw option into `{ value, label, count? }`. Accepts\n\t\t * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR\n\t\t * facet), or a bare string/number.\n\t\t *\n\t\t * @param {unknown} o The raw option.\n\t\t * @return {{value: unknown, label: string, count?: number}}\n\t\t */\n\t\tnormaliseOption(o) {\n\t\t\tif (o === null || o === undefined) {\n\t\t\t\treturn { value: undefined, label: '' }\n\t\t\t}\n\t\t\tif (typeof o !== 'object') {\n\t\t\t\treturn { value: o, label: String(o) }\n\t\t\t}\n\t\t\tconst value = o.value !== undefined ? o.value : (o.id !== undefined ? o.id : o.key)\n\t\t\tconst label = o.label !== undefined ? o.label : (o.name !== undefined ? o.name : String(value))\n\t\t\tconst out = { value, label: String(label) }\n\t\t\tif (o.count !== undefined) {\n\t\t\t\tout.count = o.count\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Load options for the active source (static content.options need no\n\t\t * fetch). Seeds the first option as the selection when nothing is set\n\t\t * and no \"All\" clear option exists.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadOptions() {\n\t\t\tif (this.objectSource) {\n\t\t\t\tawait this.seedFirstObject()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.content.endpointSource && this.content.endpointSource.url) {\n\t\t\t\tawait this.loadFromEndpoint()\n\t\t\t} else if (this.content.source && this.content.source.schema) {\n\t\t\t\tawait this.loadFromOrSource()\n\t\t\t} else {\n\t\t\t\tthis.fetchedOptions = null\n\t\t\t}\n\t\t\t// Default selection: when nothing is selected and there's no \"All\"\n\t\t\t// clear affordance, adopt the first option so siblings have a value.\n\t\t\tif ((this.selected === undefined || this.selected === '') && !this.content.allLabel) {\n\t\t\t\tconst first = this.normalisedOptions[0]\n\t\t\t\tif (first && this.readWorkspace() === undefined && this.content.default === undefined) {\n\t\t\t\t\tthis.selected = first.value\n\t\t\t\t\tthis.writeWorkspace(first.value)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate a manifest string through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The object picker's default: with nothing selected, no `default` and\n\t\t * no `allLabel`, select the first object the source lists, so the page\n\t\t * always has one in focus. Fail-safe: a failed read selects nothing.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync seedFirstObject() {\n\t\t\tif ((this.selected !== undefined && this.selected !== '') || this.content.allLabel || this.content.default !== undefined) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst src = this.objectSource\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('/apps/openregister/api/objects/{register}/{schema}', { register: src.register, schema: src.schema })\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1 } })\n\t\t\t\tconst data = res && res.data\n\t\t\t\tconst rows = Array.isArray(data) ? data : ((data && data.results) || [])\n\t\t\t\tconst first = rows[0]\n\t\t\t\tconst id = first && (first.id || (first['@self'] && first['@self'].id))\n\t\t\t\tif (id && (this.selected === undefined || this.selected === '') && this.readWorkspace() === undefined) {\n\t\t\t\t\tthis.selected = String(id)\n\t\t\t\t\tthis.writeWorkspace(String(id))\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Nothing selected: the siblings wait for a pick.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fetch options from an `endpointSource` (array of options at\n\t\t * responsePath). Fail-safe: a failed fetch leaves an empty list.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromEndpoint() {\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst payload = await fetchEndpointSource(this.content.endpointSource, {\n\t\t\t\t\tworkspace: this.readWorkspaceBag(),\n\t\t\t\t})\n\t\t\t\tthis.fetchedOptions = Array.isArray(payload) ? payload : []\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Fetch options from an OpenRegister `/grouped` facet — each distinct\n\t\t * `groupBy` value becomes an option with its object count. Fail-safe.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadFromOrSource() {\n\t\t\tconst src = this.content.source\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/aggregations/{register}/{schema}/grouped',\n\t\t\t\t\t{ register: src.register, schema: src.schema },\n\t\t\t\t)\n\t\t\t\tconst params = { groupBy: src.groupBy, metric: 'count' }\n\t\t\t\tconst filter = resolveFilterTokens(src.filter || {}, { workspace: this.readWorkspaceBag() })\n\t\t\t\tfor (const [k, v] of Object.entries(filter)) {\n\t\t\t\t\tif (v !== '' && v !== null && v !== undefined) {\n\t\t\t\t\t\tparams[`filter[${k}]`] = v\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst res = await axios.get(url, { params })\n\t\t\t\tconst groups = (res && res.data && res.data.groups) || []\n\t\t\t\tthis.fetchedOptions = groups\n\t\t\t\t\t.filter((g) => g.key !== null && g.key !== undefined && g.key !== '')\n\t\t\t\t\t.map((g) => ({ value: g.key, label: String(g.key), count: Number(g.value) || 0 }))\n\t\t\t} catch {\n\t\t\t\tthis.fetchedOptions = []\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 * Whether an option is the active selection.\n\t\t *\n\t\t * @param {object} option The option.\n\t\t * @return {boolean}\n\t\t */\n\t\tisActive(option) {\n\t\t\treturn String(option.value) === String(this.selected ?? '')\n\t\t},\n\n\t\t/**\n\t\t * Radio-list pick handler.\n\t\t *\n\t\t * @param {object} option The picked option.\n\t\t * @return {void}\n\t\t */\n\t\tonRadioPick(option) {\n\t\t\tthis.applySelection(option.value)\n\t\t},\n\n\t\t/**\n\t\t * NcSelect input handler.\n\t\t *\n\t\t * @param {object|null} option The picked option object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelect(option) {\n\t\t\tthis.applySelection(option ? option.value : '')\n\t\t},\n\n\t\t/**\n\t\t * Apply a new selection: mirror it locally, write it into the workspace\n\t\t * context (so sibling widgets refetch), and emit `@change`.\n\t\t *\n\t\t * @param {unknown} value The new value.\n\t\t * @return {void}\n\t\t */\n\t\tapplySelection(value) {\n\t\t\tthis.selected = value\n\t\t\tthis.writeWorkspace(value)\n\t\t\t/**\n\t\t\t * @event change Emitted when the selection changes. Payload: `{ key, value }`.\n\t\t\t */\n\t\t\tthis.$emit('change', { key: this.writeKey, value })\n\t\t},\n\n\t\t/**\n\t\t * The unwrapped workspace bag (or `{}`).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\treadWorkspaceBag() {\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\treturn ('value' in holder) ? (holder.value || {}) : holder\n\t\t},\n\n\t\t/**\n\t\t * Read the current workspace value for this widget's key (undefined\n\t\t * when unset).\n\t\t *\n\t\t * @return {unknown}\n\t\t */\n\t\treadWorkspace() {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tconst bag = this.readWorkspaceBag()\n\t\t\treturn bag[this.writeKey]\n\t\t},\n\n\t\t/**\n\t\t * Write a value into the reactive workspace bag under this widget's key\n\t\t * (the CnInteractionFormWidget write pattern — replace-in-place for the\n\t\t * unwrapped bag, `.value` for the raw-ref shape).\n\t\t *\n\t\t * @param {unknown} value The value to write.\n\t\t * @return {void}\n\t\t */\n\t\twriteWorkspace(value) {\n\t\t\tif (!this.writeKey) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst holder = this.cnWorkspaceContext\n\t\t\tif (!holder || typeof holder !== 'object') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('value' in holder) {\n\t\t\t\tholder.value = { ...(holder.value || {}), [this.writeKey]: value }\n\t\t\t\treturn\n\t\t\t}\n\t\t\tholder[this.writeKey] = value\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-workspace-filter-widget {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\twidth: 100%;\n}\n\n.cn-workspace-filter-widget__label {\n\tmargin: 0;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__status {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-workspace-filter-widget__list {\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n/* The start card: text left, control (and link) right, as PqDashboard's \"Contact starten\". */\n.cn-workspace-filter-widget--start {\n\tflex-direction: row;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n}\n\n.cn-workspace-filter-widget__text {\n\tflex: 1 1 320px;\n\tmin-width: 0;\n}\n\n.cn-workspace-filter-widget__title {\n\tmargin: 0 0 4px;\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tcolor: var(--color-main-text);\n}\n\n.cn-workspace-filter-widget__description {\n\tmargin: 0;\n\tmax-width: 420px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-workspace-filter-widget--start .cn-workspace-filter-widget__control,\n.cn-workspace-filter-widget--start > .v-select {\n\tflex: 0 1 480px;\n\tmin-width: 240px;\n}\n\n.cn-workspace-filter-widget__link {\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: underline;\n\twhite-space: nowrap;\n}\n\n.cn-workspace-filter-widget__count {\n\tmargin-inline-start: 6px;\n\tpadding: 0 6px;\n\tborder-radius: 999px;\n\tbackground: var(--color-background-hover);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n</style>\n"],"names":["_normalizeClass","_openBlock","_createElementBlock","_toDisplayString","_createBlock","_Fragment","_renderList","_createElementVNode"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { getCurrentUser } from '@nextcloud/auth';
1
2
  import { translate } from '@nextcloud/l10n';
2
3
 
3
4
  /**
@@ -35,6 +36,9 @@ const DRAFT_DEBOUNCE_MS = 500;
35
36
  /** The prefix every draft key carries, so a sweep can find them. */
36
37
  const DRAFT_KEY_PREFIX = 'cn-form-draft';
37
38
 
39
+ /** The user part of a draft key when nobody is signed in. */
40
+ const DRAFT_ANONYMOUS_USER = 'anonymous';
41
+
38
42
  /**
39
43
  * The storage key one form's draft lives under.
40
44
  *
@@ -55,10 +59,124 @@ function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {})
55
59
  String(appId || 'app'),
56
60
  String(schema || 'schema'),
57
61
  String(objectId || 'new'),
58
- String(userId || 'anonymous'),
62
+ String(userId || DRAFT_ANONYMOUS_USER),
59
63
  ].join(':')
60
64
  }
61
65
 
66
+ /**
67
+ * Who a draft belongs to: the id the host passed, else the signed-in
68
+ * Nextcloud user, else '' (which {@link draftKey} stores as `anonymous`).
69
+ *
70
+ * 🔴 A HOST THAT PASSES NOTHING STILL GETS PER-USER DRAFTS. Leaving the user
71
+ * to every host meant no host passed it, and every draft on a shared device
72
+ * was keyed `anonymous`: the next person at the counter was offered what the
73
+ * last one typed.
74
+ *
75
+ * @param {string} [explicit] The id the host passed (`draftUserId`).
76
+ * @return {string} The user id, or '' when nobody is signed in.
77
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
78
+ */
79
+ function resolveDraftUserId(explicit = '') {
80
+ if (explicit) {
81
+ return String(explicit)
82
+ }
83
+ try {
84
+ const uid = getCurrentUser()?.uid;
85
+ return uid ? String(uid) : ''
86
+ } catch {
87
+ // No Nextcloud page around us (a test, an embed): nobody is signed in.
88
+ return ''
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Remove every draft stored under the anonymous user.
94
+ *
95
+ * Called once a signed-in user is known. Such a draft was written by a page
96
+ * that did not know who was typing, so it may be somebody else's, and it is
97
+ * deleted rather than moved: moving it would hand it to whoever opened the
98
+ * form next.
99
+ *
100
+ * @return {number} How many drafts were removed.
101
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user
102
+ */
103
+ function clearAnonymousDrafts() {
104
+ let removed = 0;
105
+ try {
106
+ const storage = window.localStorage;
107
+ const suffix = ':' + DRAFT_ANONYMOUS_USER;
108
+ const doomed = [];
109
+ for (let i = 0; i < storage.length; i++) {
110
+ const key = storage.key(i);
111
+ if (key && key.startsWith(DRAFT_KEY_PREFIX + ':') && key.endsWith(suffix)) {
112
+ doomed.push(key);
113
+ }
114
+ }
115
+ for (const key of doomed) {
116
+ storage.removeItem(key);
117
+ removed++;
118
+ }
119
+ } catch {
120
+ // Blocked storage holds no drafts to leak.
121
+ }
122
+ return removed
123
+ }
124
+
125
+ /**
126
+ * Whether a value counts as empty for a draft: null, undefined, '' and [].
127
+ *
128
+ * @param {unknown} value The value.
129
+ * @return {boolean} True when empty.
130
+ */
131
+ function isEmptyDraftValue(value) {
132
+ return value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0)
133
+ }
134
+
135
+ /**
136
+ * Whether two field values are the same for the purpose of a draft.
137
+ *
138
+ * Empty values are equal to each other: an input reports a missing value back
139
+ * as '', and that is the control normalising, not a person typing.
140
+ *
141
+ * @param {unknown} a One value.
142
+ * @param {unknown} b The other.
143
+ * @return {boolean} True when they are the same.
144
+ */
145
+ function sameDraftValue(a, b) {
146
+ if (isEmptyDraftValue(a) && isEmptyDraftValue(b)) {
147
+ return true
148
+ }
149
+ if (a === b) {
150
+ return true
151
+ }
152
+ try {
153
+ return JSON.stringify(a) === JSON.stringify(b)
154
+ } catch {
155
+ return false
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Whether a person changed the form: one of the fields they edited holds a
161
+ * value different from the one the form opened with.
162
+ *
163
+ * 🔴 ONLY EDITED FIELDS COUNT. A form fills in values by itself after it
164
+ * opens (defaults, normalised dates, the active organisation, options that
165
+ * arrive later). Counting those wrote a draft and said "Saved just now" on a
166
+ * dialog nobody had touched.
167
+ *
168
+ * @param {object} values The current values.
169
+ * @param {object} baseline The values the form opened with.
170
+ * @param {string[]} editedKeys The fields a person edited.
171
+ * @return {boolean} True when there is something worth keeping.
172
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
173
+ */
174
+ function hasDraftChanges(values, baseline, editedKeys) {
175
+ const now = values || {};
176
+ const base = baseline || {};
177
+ return (editedKeys || []).some((key) => !sameDraftValue(now[key], base[key]))
178
+ }
179
+
62
180
  /**
63
181
  * Read one draft back, or null when there is nothing usable.
64
182
  *
@@ -219,13 +337,6 @@ function formDraftMixin() {
219
337
  draftSavedAt: 0,
220
338
  /** Non-reactive handle for the debounce. */
221
339
  draftTimeout: null,
222
- /**
223
- * Whether the form's own initial population has happened.
224
- *
225
- * The first change to the values is the component building
226
- * them, not a person typing. See the watcher that reads this.
227
- */
228
- draftSeeded: false,
229
340
  }
230
341
  },
231
342
 
@@ -266,6 +377,26 @@ function formDraftMixin() {
266
377
  }, DRAFT_DEBOUNCE_MS);
267
378
  },
268
379
 
380
+ /**
381
+ * The form is back to what it opened with: cancel a pending write,
382
+ * remove a draft written earlier and stop announcing anything.
383
+ *
384
+ * @param {string} key The storage key.
385
+ * @return {void}
386
+ * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-is-written-only-after-a-person-changes-a-field
387
+ */
388
+ settleUnchangedDraft(key) {
389
+ if (this.draftTimeout) {
390
+ clearTimeout(this.draftTimeout);
391
+ this.draftTimeout = null;
392
+ }
393
+ if (this.draftState !== 'idle') {
394
+ clearDraft(key);
395
+ this.draftState = 'idle';
396
+ this.draftSavedAt = 0;
397
+ }
398
+ },
399
+
269
400
  /**
270
401
  * Drop the draft and stop announcing anything about it.
271
402
  *
@@ -286,5 +417,5 @@ function formDraftMixin() {
286
417
  }
287
418
  }
288
419
 
289
- export { DRAFT_DEBOUNCE_MS, DRAFT_KEY_PREFIX, DRAFT_MAX_AGE_MS, clearDraft, draftIndicatorText, draftKey, formDraftMixin, readDraft, writeDraft };
420
+ export { DRAFT_ANONYMOUS_USER, DRAFT_DEBOUNCE_MS, DRAFT_KEY_PREFIX, DRAFT_MAX_AGE_MS, clearAnonymousDrafts, clearDraft, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId, sameDraftValue, writeDraft };
290
421
  //# sourceMappingURL=useFormDraft.js.map