@conduction/nextcloud-vue 2.62.0 → 2.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +425 -39
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
  10. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
  14. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  25. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  26. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  27. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  35. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  36. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  37. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
  39. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
  41. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  50. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  52. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  54. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  65. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  66. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  67. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
  68. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  72. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  73. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  74. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  76. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  78. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  84. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
  88. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  99. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  100. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  110. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  111. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  112. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  113. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  114. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  115. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  116. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  117. package/dist/esm/composables/useDependencyCheck.js +65 -0
  118. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  119. package/dist/esm/composables/useWalkthrough.js +218 -6
  120. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  121. package/dist/esm/index.js +2 -0
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/l10n/en.json.js +45 -1
  124. package/dist/esm/l10n/en.json.js.map +1 -1
  125. package/dist/esm/l10n/nl.json.js +45 -1
  126. package/dist/esm/l10n/nl.json.js.map +1 -1
  127. package/dist/esm/nextcloud-vue.css +780 -81
  128. package/dist/esm/store/useObjectStore.js +8 -0
  129. package/dist/esm/store/useObjectStore.js.map +1 -1
  130. package/dist/esm/utils/columnFilters.js +311 -0
  131. package/dist/esm/utils/columnFilters.js.map +1 -0
  132. package/dist/esm/utils/groupAutocomplete.js +123 -0
  133. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  134. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  135. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  136. package/dist/esm/utils/pickerOptions.js +198 -0
  137. package/dist/esm/utils/pickerOptions.js.map +1 -0
  138. package/dist/esm/utils/schema.js +83 -7
  139. package/dist/esm/utils/schema.js.map +1 -1
  140. package/dist/esm/utils/schemaRefSlug.js +19 -3
  141. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  142. package/dist/esm/utils/timelineEvents.js +174 -0
  143. package/dist/esm/utils/timelineEvents.js.map +1 -0
  144. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  145. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  146. package/dist/esm/utils/walkthroughSignals.js +88 -0
  147. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  148. package/dist/nextcloud-vue.cjs.js +13088 -7937
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +780 -81
  151. package/l10n/en.json +45 -0
  152. package/l10n/nl.json +45 -0
  153. package/package.json +1 -1
  154. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  155. package/src/components/CnAdminActionCard/index.js +4 -0
  156. package/src/components/CnAppNav/CnAppNav.vue +568 -43
  157. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  158. package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
  159. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  160. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  161. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  162. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  163. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
  164. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  165. package/src/components/CnDataTable/CnDataTable.md +3 -0
  166. package/src/components/CnDataTable/CnDataTable.vue +305 -5
  167. package/src/components/CnDetailPage/CnDetailPage.md +4 -0
  168. package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
  169. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  170. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  171. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  172. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
  173. package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
  174. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  175. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
  176. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  177. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  178. package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
  179. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  180. package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  182. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  183. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  184. package/src/components/CnTimelineWidget/index.js +4 -0
  185. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  186. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  187. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  188. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  189. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  190. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  191. package/src/components/index.js +2 -0
  192. package/src/composables/useDependencyCheck.js +61 -0
  193. package/src/composables/useWalkthrough.js +217 -5
  194. package/src/css/dashboard.css +49 -0
  195. package/src/css/detail-page.css +29 -4
  196. package/src/css/kpi-card.css +24 -1
  197. package/src/css/table.css +99 -6
  198. package/src/index.js +2 -0
  199. package/src/schemas/app-manifest-v2.schema.json +135 -3
  200. package/src/store/useObjectStore.js +8 -0
  201. package/src/utils/columnFilters.js +324 -0
  202. package/src/utils/groupAutocomplete.js +119 -0
  203. package/src/utils/libraryWidgetKeys.js +2 -0
  204. package/src/utils/pickerOptions.js +194 -0
  205. package/src/utils/schema.js +83 -7
  206. package/src/utils/schemaRefSlug.js +19 -3
  207. package/src/utils/timelineEvents.js +171 -0
  208. package/src/utils/validateManifestV2.compiled.js +2 -2
  209. package/src/utils/walkthroughSignals.js +94 -0
@@ -0,0 +1,524 @@
1
+ <!-- SPDX-License-Identifier: EUPL-1.2 -->
2
+ <!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->
3
+ <template>
4
+ <Teleport to="body" :disabled="!teleport">
5
+ <div
6
+ ref="panel"
7
+ class="cn-column-filter"
8
+ role="dialog"
9
+ :aria-labelledby="headingId"
10
+ :style="panelStyle"
11
+ data-testid="cn-column-filter"
12
+ @keydown.esc.stop.prevent="close"
13
+ @keydown.enter="onEnter">
14
+ <p :id="headingId" class="cn-column-filter__heading">
15
+ {{ headingText }}
16
+ </p>
17
+
18
+ <!-- enum and reference: a checkbox list of real inputs -->
19
+ <template v-if="def.kind === 'enum' || def.kind === 'reference'">
20
+ <label v-if="def.kind === 'reference'" class="cn-column-filter__field">
21
+ <span class="cn-column-filter__label">{{ searchLabel }}</span>
22
+ <input
23
+ v-model="query"
24
+ class="cn-column-filter__input"
25
+ type="search"
26
+ data-testid="cn-column-filter-search"
27
+ @input="onSearchInput">
28
+ </label>
29
+ <p v-if="def.kind === 'reference' && referenceLoading" class="cn-column-filter__hint" role="status">
30
+ {{ loadingLabel }}
31
+ </p>
32
+ <p v-else-if="def.kind === 'reference' && referenceError" class="cn-column-filter__hint" role="status">
33
+ {{ referenceError }}
34
+ </p>
35
+ <fieldset class="cn-column-filter__options">
36
+ <legend class="cn-column-filter__sr-only">
37
+ {{ headingText }}
38
+ </legend>
39
+ <label
40
+ v-for="option in listOptions"
41
+ :key="option.value"
42
+ class="cn-column-filter__option">
43
+ <input
44
+ type="checkbox"
45
+ :value="option.value"
46
+ :checked="draft.values.includes(option.value)"
47
+ data-testid="cn-column-filter-option"
48
+ @change="toggleValue(option.value)">
49
+ <span>{{ option.label }}</span>
50
+ </label>
51
+ <p v-if="listOptions.length === 0 && !referenceLoading" class="cn-column-filter__hint">
52
+ {{ noOptionsLabel }}
53
+ </p>
54
+ </fieldset>
55
+ </template>
56
+
57
+ <!-- boolean: yes, no, any -->
58
+ <fieldset v-else-if="def.kind === 'boolean'" class="cn-column-filter__options">
59
+ <legend class="cn-column-filter__sr-only">
60
+ {{ headingText }}
61
+ </legend>
62
+ <label v-for="option in booleanOptions" :key="option.value" class="cn-column-filter__option">
63
+ <input
64
+ type="radio"
65
+ :name="headingId + '-bool'"
66
+ :value="option.value"
67
+ :checked="draft.value === option.value"
68
+ data-testid="cn-column-filter-option"
69
+ @change="draft.value = option.value">
70
+ <span>{{ option.label }}</span>
71
+ </label>
72
+ </fieldset>
73
+
74
+ <!-- number and date: from and to -->
75
+ <div v-else-if="def.kind === 'number' || def.kind === 'date'" class="cn-column-filter__range">
76
+ <label class="cn-column-filter__field">
77
+ <span class="cn-column-filter__label">{{ fromLabel }}</span>
78
+ <input
79
+ v-model="draft.from"
80
+ class="cn-column-filter__input"
81
+ :type="def.kind === 'date' ? 'date' : 'number'"
82
+ data-testid="cn-column-filter-from">
83
+ </label>
84
+ <label class="cn-column-filter__field">
85
+ <span class="cn-column-filter__label">{{ toLabel }}</span>
86
+ <input
87
+ v-model="draft.to"
88
+ class="cn-column-filter__input"
89
+ :type="def.kind === 'date' ? 'date' : 'number'"
90
+ data-testid="cn-column-filter-to">
91
+ </label>
92
+ </div>
93
+
94
+ <!-- string: equals -->
95
+ <label v-else class="cn-column-filter__field">
96
+ <span class="cn-column-filter__label">{{ equalsLabel }}</span>
97
+ <input
98
+ v-model="draft.value"
99
+ class="cn-column-filter__input"
100
+ type="text"
101
+ data-testid="cn-column-filter-text">
102
+ </label>
103
+
104
+ <div class="cn-column-filter__actions">
105
+ <button
106
+ type="button"
107
+ class="cn-column-filter__button"
108
+ data-testid="cn-column-filter-clear"
109
+ @click="clear">
110
+ {{ clearLabel }}
111
+ </button>
112
+ <button
113
+ type="button"
114
+ class="cn-column-filter__button cn-column-filter__button--primary"
115
+ data-testid="cn-column-filter-apply"
116
+ @click="apply">
117
+ {{ applyLabel }}
118
+ </button>
119
+ </div>
120
+ </div>
121
+ </Teleport>
122
+ </template>
123
+
124
+ <script>
125
+ import { translate as t } from '@nextcloud/l10n'
126
+
127
+ let popoverSeq = 0
128
+
129
+ /**
130
+ * CnColumnFilterPopover — the small panel a table header's filter button opens.
131
+ *
132
+ * Renders the control that fits the column: a checkbox list for an enum, yes,
133
+ * no or any for a boolean, from and to for a number or a date, a searchable
134
+ * list of referenced objects for a reference, and an equals box for text.
135
+ * Real inputs throughout, so every control works from the keyboard. Escape
136
+ * closes it and the host returns focus to the filter button.
137
+ *
138
+ * Internal to CnDataTable. It emits a state object; CnDataTable turns that
139
+ * into query parameters with `columnFilterParams`.
140
+ *
141
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
142
+ */
143
+ export default {
144
+ name: 'CnColumnFilterPopover',
145
+
146
+ props: {
147
+ /** The filter definition from `columnFilterDef`. */
148
+ def: {
149
+ type: Object,
150
+ required: true,
151
+ },
152
+
153
+ /** The column's current state from `columnFilterState`. */
154
+ state: {
155
+ type: Object,
156
+ default: () => ({}),
157
+ },
158
+
159
+ /** The translated column label. */
160
+ label: {
161
+ type: String,
162
+ default: '',
163
+ },
164
+
165
+ /** The filter button's bounding rect, to place the panel under it. */
166
+ anchorRect: {
167
+ type: Object,
168
+ default: null,
169
+ },
170
+
171
+ /** Register slug a reference column's objects live in, when the column names none. */
172
+ register: {
173
+ type: String,
174
+ default: '',
175
+ },
176
+
177
+ /** Render into `<body>`, so a scrolling table cannot clip the panel. */
178
+ teleport: {
179
+ type: Boolean,
180
+ default: true,
181
+ },
182
+ },
183
+
184
+ emits: ['apply', 'close'],
185
+
186
+ data() {
187
+ popoverSeq += 1
188
+ const s = this.state || {}
189
+ return {
190
+ headingId: 'cn-column-filter-' + popoverSeq,
191
+ draft: {
192
+ values: Array.isArray(s.values) ? [...s.values] : [],
193
+ value: s.value !== undefined && s.value !== null ? String(s.value) : '',
194
+ from: s.from || '',
195
+ to: s.to || '',
196
+ },
197
+
198
+ query: '',
199
+ referenceOptions: [],
200
+ referenceLabels: {},
201
+ referenceLoading: false,
202
+ referenceError: '',
203
+ searchTimer: null,
204
+ }
205
+ },
206
+
207
+ computed: {
208
+ headingText() {
209
+ return t('nextcloud-vue', 'Filter {column}', { column: this.label })
210
+ },
211
+
212
+ /**
213
+ * The options a list filter shows. For a reference, the search results
214
+ * plus any picked object that is not among them, so a selection never
215
+ * disappears from view.
216
+ *
217
+ * @return {Array<{value: string, label: string}>}
218
+ */
219
+ listOptions() {
220
+ if (this.def.kind !== 'reference') {
221
+ return this.def.options || []
222
+ }
223
+ const shown = new Set(this.referenceOptions.map((o) => o.value))
224
+ const picked = this.draft.values
225
+ .filter((v) => !shown.has(v))
226
+ .map((v) => ({ value: v, label: this.referenceLabels[v] || v }))
227
+ return [...picked, ...this.referenceOptions]
228
+ },
229
+
230
+ booleanOptions() {
231
+ return [
232
+ { value: '', label: t('nextcloud-vue', 'Any') },
233
+ { value: 'true', label: t('nextcloud-vue', 'Yes') },
234
+ { value: 'false', label: t('nextcloud-vue', 'No') },
235
+ ]
236
+ },
237
+
238
+ panelStyle() {
239
+ if (!this.teleport || !this.anchorRect) {
240
+ return {}
241
+ }
242
+ const width = 280
243
+ const viewport = typeof window !== 'undefined' ? window.innerWidth : 1024
244
+ const left = Math.max(8, Math.min(this.anchorRect.left, viewport - width - 8))
245
+ return { position: 'fixed', top: `${this.anchorRect.bottom + 4}px`, left: `${left}px`, width: `${width}px` }
246
+ },
247
+
248
+ searchLabel() {
249
+ return t('nextcloud-vue', 'Search')
250
+ },
251
+
252
+ loadingLabel() {
253
+ return t('nextcloud-vue', 'Loading…')
254
+ },
255
+
256
+ noOptionsLabel() {
257
+ return t('nextcloud-vue', 'Nothing to choose from here.')
258
+ },
259
+
260
+ fromLabel() {
261
+ return t('nextcloud-vue', 'From')
262
+ },
263
+
264
+ toLabel() {
265
+ return t('nextcloud-vue', 'To')
266
+ },
267
+
268
+ equalsLabel() {
269
+ return t('nextcloud-vue', 'Equals')
270
+ },
271
+
272
+ clearLabel() {
273
+ return t('nextcloud-vue', 'Clear')
274
+ },
275
+
276
+ applyLabel() {
277
+ return t('nextcloud-vue', 'Apply')
278
+ },
279
+ },
280
+
281
+ mounted() {
282
+ document.addEventListener('mousedown', this.onOutsideMouseDown, true)
283
+ if (this.def.kind === 'reference') {
284
+ this.loadReferenceOptions('')
285
+ }
286
+ this.$nextTick(() => {
287
+ const first = this.$refs.panel && this.$refs.panel.querySelector('input, button')
288
+ if (first && typeof first.focus === 'function') {
289
+ first.focus()
290
+ }
291
+ })
292
+ },
293
+
294
+ beforeUnmount() {
295
+ document.removeEventListener('mousedown', this.onOutsideMouseDown, true)
296
+ clearTimeout(this.searchTimer)
297
+ },
298
+
299
+ methods: {
300
+ toggleValue(value) {
301
+ this.draft.values = this.draft.values.includes(value)
302
+ ? this.draft.values.filter((v) => v !== value)
303
+ : [...this.draft.values, value]
304
+ },
305
+
306
+ /**
307
+ * The state this panel applies, in the shape `columnFilterParams` reads.
308
+ *
309
+ * @return {object}
310
+ */
311
+ draftState() {
312
+ if (this.def.kind === 'enum' || this.def.kind === 'reference') {
313
+ return { values: [...this.draft.values] }
314
+ }
315
+ if (this.def.kind === 'number' || this.def.kind === 'date') {
316
+ return { from: this.draft.from, to: this.draft.to }
317
+ }
318
+ return { value: this.draft.value }
319
+ },
320
+
321
+ apply() {
322
+ /**
323
+ * @event apply The person applied the filter.
324
+ * @type {object} The column filter state.
325
+ */
326
+ this.$emit('apply', this.draftState())
327
+ },
328
+
329
+ clear() {
330
+ this.$emit('apply', this.def.kind === 'enum' || this.def.kind === 'reference'
331
+ ? { values: [] }
332
+ : (this.def.kind === 'number' || this.def.kind === 'date') ? { from: '', to: '' } : { value: '' })
333
+ },
334
+
335
+ close() {
336
+ /**
337
+ * @event close The panel should close without applying.
338
+ */
339
+ this.$emit('close')
340
+ },
341
+
342
+ onEnter(event) {
343
+ // Enter in a text, number or date box applies, like a form submit.
344
+ // On a checkbox or a button it keeps its own meaning.
345
+ const target = event && event.target
346
+ if (target && target.tagName === 'INPUT' && ['text', 'number', 'date', 'search'].includes(target.type)) {
347
+ event.preventDefault()
348
+ this.apply()
349
+ }
350
+ },
351
+
352
+ onOutsideMouseDown(event) {
353
+ const panel = this.$refs.panel
354
+ if (panel && event && event.target && !panel.contains(event.target)) {
355
+ this.close()
356
+ }
357
+ },
358
+
359
+ onSearchInput() {
360
+ clearTimeout(this.searchTimer)
361
+ this.searchTimer = setTimeout(() => this.loadReferenceOptions(this.query), 250)
362
+ },
363
+
364
+ /**
365
+ * Search the referenced objects. The value is the object's uuid,
366
+ * because that is what the server filters a reference on.
367
+ *
368
+ * @param {string} search The search term.
369
+ * @return {Promise<void>}
370
+ */
371
+ async loadReferenceOptions(search) {
372
+ const ref = this.def.reference || {}
373
+ const register = ref.register || this.register
374
+ if (!register || !ref.schema) {
375
+ this.referenceError = t('nextcloud-vue', 'This column cannot list its objects.')
376
+ return
377
+ }
378
+ this.referenceLoading = true
379
+ this.referenceError = ''
380
+ try {
381
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
382
+ import('@nextcloud/axios'),
383
+ import('@nextcloud/router'),
384
+ ])
385
+ const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', {
386
+ register,
387
+ schema: String(ref.schema),
388
+ })
389
+ const params = { _limit: 50 }
390
+ if (search) {
391
+ params._search = search
392
+ }
393
+ const response = await axios.get(url, { params })
394
+ const rows = (response && response.data && (response.data.results || response.data)) || []
395
+ const list = Array.isArray(rows) ? rows : []
396
+ const options = list
397
+ .map((row) => {
398
+ const self = (row && row['@self']) || {}
399
+ const value = String(self.id || self.uuid || row.id || row.uuid || '')
400
+ const label = String((ref.labelField && row[ref.labelField]) || row.name || row.title || self.name || value)
401
+ return { value, label }
402
+ })
403
+ .filter((o) => o.value !== '')
404
+ this.referenceOptions = options
405
+ this.referenceLabels = { ...this.referenceLabels, ...Object.fromEntries(options.map((o) => [o.value, o.label])) }
406
+ } catch {
407
+ this.referenceOptions = []
408
+ this.referenceError = t('nextcloud-vue', 'The list could not load.')
409
+ } finally {
410
+ this.referenceLoading = false
411
+ }
412
+ },
413
+ },
414
+ }
415
+ </script>
416
+
417
+ <style scoped>
418
+ .cn-column-filter {
419
+ z-index: 10000;
420
+ display: flex;
421
+ flex-direction: column;
422
+ gap: 8px;
423
+ box-sizing: border-box;
424
+ max-height: 60vh;
425
+ overflow-y: auto;
426
+ padding: 12px;
427
+ background: var(--color-main-background);
428
+ color: var(--color-main-text);
429
+ border: 1px solid var(--color-border-dark);
430
+ border-radius: var(--border-radius-large, 8px);
431
+ box-shadow: 0 2px 8px var(--color-box-shadow);
432
+ font-weight: normal;
433
+ text-align: start;
434
+ }
435
+
436
+ .cn-column-filter__heading {
437
+ margin: 0;
438
+ font-weight: bold;
439
+ }
440
+
441
+ .cn-column-filter__options {
442
+ display: flex;
443
+ flex-direction: column;
444
+ gap: 4px;
445
+ margin: 0;
446
+ padding: 0;
447
+ border: none;
448
+ }
449
+
450
+ .cn-column-filter__option {
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 8px;
454
+ min-height: 32px;
455
+ cursor: pointer;
456
+ }
457
+
458
+ .cn-column-filter__range {
459
+ display: flex;
460
+ gap: 8px;
461
+ }
462
+
463
+ .cn-column-filter__field {
464
+ display: flex;
465
+ flex: 1 1 0;
466
+ flex-direction: column;
467
+ gap: 4px;
468
+ min-width: 0;
469
+ }
470
+
471
+ .cn-column-filter__label {
472
+ font-size: var(--font-size-small, 13px);
473
+ color: var(--color-text-maxcontrast);
474
+ }
475
+
476
+ .cn-column-filter__input {
477
+ width: 100%;
478
+ box-sizing: border-box;
479
+ margin: 0;
480
+ }
481
+
482
+ .cn-column-filter__hint {
483
+ margin: 0;
484
+ color: var(--color-text-maxcontrast);
485
+ }
486
+
487
+ .cn-column-filter__actions {
488
+ display: flex;
489
+ justify-content: flex-end;
490
+ gap: 8px;
491
+ }
492
+
493
+ .cn-column-filter__button {
494
+ min-height: 34px;
495
+ padding: 0 12px;
496
+ border: 1px solid var(--color-border-dark);
497
+ border-radius: var(--border-radius-element, 8px);
498
+ background: var(--color-main-background);
499
+ color: var(--color-main-text);
500
+ cursor: pointer;
501
+ }
502
+
503
+ .cn-column-filter__button--primary {
504
+ border-color: var(--color-primary-element);
505
+ background: var(--color-primary-element);
506
+ color: var(--color-primary-element-text);
507
+ }
508
+
509
+ .cn-column-filter__button:focus-visible,
510
+ .cn-column-filter__input:focus-visible,
511
+ .cn-column-filter__option input:focus-visible {
512
+ outline: 2px solid var(--color-primary-element);
513
+ outline-offset: 2px;
514
+ }
515
+
516
+ .cn-column-filter__sr-only {
517
+ position: absolute;
518
+ width: 1px;
519
+ height: 1px;
520
+ overflow: hidden;
521
+ clip-path: inset(50%);
522
+ white-space: nowrap;
523
+ }
524
+ </style>
@@ -201,6 +201,9 @@ export default {
201
201
  | `hideHeader` | Boolean | `false` | Hide the column-header row (`<thead>`) — for compact dashboard list widgets that want a plain bordered-row list without column labels |
202
202
  | `fixedLayout` | Boolean | `false` | Switch to `table-layout: fixed` so each column's `width` is authoritative rather than a hint the browser may override from cell content. Long unbreakable values (a PHP FQCN, a UUID) then wrap inside their cell instead of widening the column or painting past it, and no single unsized column soaks up all remaining width. Size every column when using it — percentages summing to 100 are easiest to reason about |
203
203
  | `fillHeight` | Boolean | `false` | Fill the parent's height (a flex-column card / widget content area) so an optional `#footer` is pushed to the bottom instead of floating under a short list; the footer stays pinned via its sticky rule when the list overflows. No-op outside a height-constrained parent — opt-in |
204
+ | `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
205
+ | `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
206
+ | `filterRegister` | String | `''` | Register slug a reference column's filter searches in when the column names none. |
204
207
 
205
208
  ## Slots
206
209