@conduction/nextcloud-vue 2.61.0 → 2.63.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 (186) 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 +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -19,6 +19,36 @@
19
19
  </span>
20
20
  </div>
21
21
 
22
+ <!-- Active header filters, one removable chip each. -->
23
+ <ul
24
+ v-if="!isLoading && activeColumnFilterChips.length > 0"
25
+ class="cn-data-table__filter-chips"
26
+ :aria-label="activeFiltersLabel"
27
+ data-testid="cn-table-filter-chips">
28
+ <li v-for="chip in activeColumnFilterChips" :key="chip.key" class="cn-data-table__filter-chip">
29
+ <span>{{ chip.text }}</span>
30
+ <button
31
+ type="button"
32
+ class="cn-data-table__filter-chip-remove"
33
+ :aria-label="chip.removeLabel"
34
+ data-testid="cn-table-filter-chip-remove"
35
+ @click="clearColumnFilter(chip.col)">
36
+ ×
37
+ </button>
38
+ </li>
39
+ </ul>
40
+
41
+ <CnColumnFilterPopover
42
+ v-if="openFilterColumn"
43
+ :key="openFilterKey"
44
+ :def="filterDefFor(openFilterColumn)"
45
+ :state="columnFilterStateFor(openFilterColumn)"
46
+ :label="translateLabel(openFilterColumn.label)"
47
+ :anchorRect="filterAnchorRect"
48
+ :register="filterRegister"
49
+ @apply="(state) => applyColumnFilter(openFilterColumn, state)"
50
+ @close="closeColumnFilter" />
51
+
22
52
  <!-- Loading State -->
23
53
  <div v-if="isLoading" class="cn-table-loading" data-testid="cn-object-list-loading">
24
54
  <!-- Decorative: the adjacent <p> already carries the accessible
@@ -92,6 +122,7 @@
92
122
  :style="col.width ? { width: col.width } : {}"
93
123
  :tabindex="col.sortable ? 0 : null"
94
124
  :aria-sort="ariaSortFor(col)"
125
+ :data-filtered="isColumnFiltered(col) ? 'true' : null"
95
126
  :title="translateLabel(col.description) || null"
96
127
  @click="col.sortable ? onHeaderClick(col.key, $event) : null"
97
128
  @keydown.enter="col.sortable ? onHeaderKeydown(col.key, $event) : null">
@@ -104,10 +135,27 @@
104
135
  {{ effectiveSortKeys[sortKeyIndex(col.key)].order === 'asc' ? '▲' : '▼' }}
105
136
  </span>
106
137
  <span
107
- v-if="col.sortable && effectiveSortKeys.length > 1 && sortKeyIndex(col.key) !== -1"
138
+ v-if="col.sortable && sortBadgeFor(col.key) !== null"
108
139
  class="cn-table-sort-badge">
109
- {{ sortKeyIndex(col.key) + 1 }}
140
+ {{ sortBadgeFor(col.key) }}
110
141
  </span>
142
+ <!-- Header filter: a real button, so it is reachable with Tab
143
+ and its click and Enter never also sort the column. -->
144
+ <button
145
+ v-if="filterDefFor(col)"
146
+ :ref="'filterButton-' + col.key"
147
+ type="button"
148
+ class="cn-table-header__filter"
149
+ :class="{ 'cn-table-header__filter--active': isColumnFiltered(col) }"
150
+ :aria-label="filterButtonLabel(col)"
151
+ aria-haspopup="dialog"
152
+ :aria-expanded="openFilterKey === col.key ? 'true' : 'false'"
153
+ data-testid="cn-table-header-filter"
154
+ @click.stop="toggleColumnFilter(col, $event)"
155
+ @keydown.enter.stop>
156
+ <FilterIcon v-if="isColumnFiltered(col)" :size="16" />
157
+ <FilterOutline v-else :size="16" />
158
+ </button>
111
159
  </th>
112
160
 
113
161
  <!-- Actions column -->
@@ -283,10 +331,14 @@
283
331
 
284
332
  <script>
285
333
  import axios from '@nextcloud/axios'
286
- import { translate as t } from '@nextcloud/l10n'
334
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
287
335
  import { generateUrl } from '@nextcloud/router'
288
336
  import { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
337
+ import FilterIcon from 'vue-material-design-icons/Filter.vue'
338
+ import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
339
+ import CnColumnFilterPopover from './CnColumnFilterPopover.vue'
289
340
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
341
+ import { clearedColumnFilterParams, columnFilterDef, columnFilterParams, columnFilterState, isColumnFilterActive, isColumnSortable } from '../../utils/columnFilters.js'
290
342
  import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
291
343
  import { nextSortState } from '../../utils/multiColumnSort.js'
292
344
  import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
@@ -309,15 +361,12 @@ import '../../css/table.css'
309
361
  * row selection, custom cell rendering via scoped slots, loading states,
310
362
  * and empty states.
311
363
  *
312
- * Sorting: a plain click on a sortable header is single-sort (cycle asc →
313
- * desc → cleared), unchanged from before. Shift+click (or Shift+Enter on a
314
- * focused header) appends the column as a secondary/tertiary sort key —
315
- * capped at 3 — with numbered priority badges (1, 2, 3) once more than one
316
- * key is active. Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls
317
- * back to the legacy `sortKey`/`sortOrder` props when empty). The `sort`
318
- * event payload is extended, not replaced: `{key, order}` still mirrors the
319
- * primary key exactly as before; a new `keys` field carries the full
320
- * ordered list. See `src/utils/multiColumnSort.js` for the state machine.
364
+ * Sorting: a plain click on a sortable header is single-sort (cycle asc → desc → cleared), unchanged from before.
365
+ * Shift+click (or Shift+Enter on a focused header) appends the column as a secondary/tertiary sort key, capped at 3, with numbered priority badges (1, 2, 3) once two or more rendered, sortable columns are sorted.
366
+ * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered.
367
+ * Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls back to the legacy `sortKey`/`sortOrder` props when empty).
368
+ * The `sort` event payload is extended, not replaced: `{key, order}` still mirrors the primary key exactly as before; a new `keys` field carries the full ordered list.
369
+ * See `src/utils/multiColumnSort.js` for the state machine.
321
370
  *
322
371
  * When a `schema` prop is provided, columns are auto-generated from schema
323
372
  * properties and cells render through CnCellRenderer for type-aware formatting
@@ -360,8 +409,11 @@ export default {
360
409
  NcLoadingIcon,
361
410
  NcCheckboxRadioSwitch,
362
411
  CnCellRenderer,
412
+ CnColumnFilterPopover,
363
413
  CnIcon,
364
414
  CnLockIndicator,
415
+ FilterIcon,
416
+ FilterOutline,
365
417
  },
366
418
 
367
419
  inject: {
@@ -730,9 +782,39 @@ export default {
730
782
  type: Boolean,
731
783
  default: false,
732
784
  },
785
+
786
+ /**
787
+ * Show a filter button in every header whose column can filter (see
788
+ * `columnFilterDef`): enum, boolean, text, number, date and reference
789
+ * columns backed by a schema property. A column opts out with
790
+ * `filterable: false`. Applying a filter emits `column-filter`; the
791
+ * host owns the filter state and passes it back as `activeFilters`.
792
+ *
793
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
794
+ */
795
+ filterable: {
796
+ type: Boolean,
797
+ default: false,
798
+ },
799
+
800
+ /**
801
+ * The active filter map, `{ paramKey: values[] }`: the same map the
802
+ * facet sidebar writes, so a header filter and a sidebar filter on one
803
+ * field show the same state.
804
+ */
805
+ activeFilters: {
806
+ type: Object,
807
+ default: () => ({}),
808
+ },
809
+
810
+ /** Register slug a reference column's filter searches in when the column names none. */
811
+ filterRegister: {
812
+ type: String,
813
+ default: '',
814
+ },
733
815
  },
734
816
 
735
- emits: ['row-click', 'row-aux-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all'],
817
+ emits: ['row-click', 'row-aux-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all', 'column-filter'],
736
818
 
737
819
  setup() {
738
820
  // Tell a deliberate row click apart from a text-selection drag.
@@ -741,6 +823,10 @@ export default {
741
823
 
742
824
  data() {
743
825
  return {
826
+ /** Key of the column whose filter panel is open, or ''. */
827
+ openFilterKey: '',
828
+ /** The open filter button's bounding rect, to place the panel. */
829
+ filterAnchorRect: null,
744
830
  /**
745
831
  * Resolved aggregate-column values, keyed by `String(row[rowKey])`
746
832
  * then by column key. Populated by `loadAggregates()` for columns
@@ -858,8 +944,12 @@ export default {
858
944
  overrides: this.columnOverrides,
859
945
  })
860
946
  : this.columns
947
+ // Every column backed by a schema property sorts unless it says
948
+ // `sortable: false`; object columns from a manifest used to need an
949
+ // explicit flag and so could not sort at all.
950
+ const withSort = (c) => (c && typeof c === 'object' ? { ...c, sortable: isColumnSortable(c, this.schema) } : c)
861
951
  if (!(cols || []).some((c) => typeof c === 'string')) {
862
- return cols || []
952
+ return (cols || []).map(withSort)
863
953
  }
864
954
  const schemaCols = this.schema
865
955
  ? columnsFromSchema(this.schema, { overrides: this.columnOverrides })
@@ -867,7 +957,7 @@ export default {
867
957
  const byKey = new Map(schemaCols.map((c) => [c.key, c]))
868
958
  return (cols || []).map((c) => {
869
959
  if (typeof c !== 'string') {
870
- return c
960
+ return withSort(c)
871
961
  }
872
962
  return byKey.get(c) || { key: c, label: c, sortable: true }
873
963
  })
@@ -908,6 +998,40 @@ export default {
908
998
  return links
909
999
  },
910
1000
 
1001
+ /**
1002
+ * The column whose filter panel is open, or null.
1003
+ *
1004
+ * @return {object|null}
1005
+ */
1006
+ openFilterColumn() {
1007
+ if (!this.openFilterKey) {
1008
+ return null
1009
+ }
1010
+ return this.effectiveColumns.find((c) => c && c.key === this.openFilterKey && this.filterDefFor(c)) || null
1011
+ },
1012
+
1013
+ /**
1014
+ * One chip per column with an active header filter.
1015
+ *
1016
+ * @return {Array<{key: string, col: object, text: string, removeLabel: string}>}
1017
+ */
1018
+ activeColumnFilterChips() {
1019
+ if (!this.filterable) {
1020
+ return []
1021
+ }
1022
+ return this.effectiveColumns
1023
+ .filter((c) => c && this.isColumnFiltered(c))
1024
+ .map((col) => {
1025
+ const label = this.translateLabel(col.label)
1026
+ const text = t('nextcloud-vue', '{column}: {value}', { column: label, value: this.columnFilterSummary(col) })
1027
+ return { key: col.key, col, text, removeLabel: t('nextcloud-vue', 'Remove filter {column}', { column: label }) }
1028
+ })
1029
+ },
1030
+
1031
+ activeFiltersLabel() {
1032
+ return t('nextcloud-vue', 'Active filters')
1033
+ },
1034
+
911
1035
  effectiveSortKeys() {
912
1036
  if (this.sortKeys && this.sortKeys.length > 0) {
913
1037
  return this.sortKeys
@@ -918,6 +1042,23 @@ export default {
918
1042
  return []
919
1043
  },
920
1044
 
1045
+ /**
1046
+ * The active sort keys whose column renders a sort indicator, in priority order and deduplicated by first occurrence. Only these are counted and numbered by the priority badge.
1047
+ *
1048
+ * @return {Array<{key: string, order: 'asc'|'desc'}>}
1049
+ */
1050
+ renderedSortKeys() {
1051
+ const shown = new Set(this.effectiveColumns.filter((c) => c && c.sortable).map((c) => c.key))
1052
+ const seen = new Set()
1053
+ return this.effectiveSortKeys.filter((k) => {
1054
+ if (!k || !shown.has(k.key) || seen.has(k.key)) {
1055
+ return false
1056
+ }
1057
+ seen.add(k.key)
1058
+ return true
1059
+ })
1060
+ },
1061
+
921
1062
  totalColumns() {
922
1063
  let count = this.effectiveColumns.length
923
1064
  if (this.selectable) {
@@ -1513,7 +1654,7 @@ export default {
1513
1654
 
1514
1655
  /**
1515
1656
  * Index of `key` within `effectiveSortKeys`, or -1 when not active.
1516
- * Used by the template for the arrow, the numbered badge, and aria-sort.
1657
+ * Used by the template for the arrow.
1517
1658
  *
1518
1659
  * @param {string} key Column key.
1519
1660
  * @return {number}
@@ -1523,11 +1664,22 @@ export default {
1523
1664
  },
1524
1665
 
1525
1666
  /**
1526
- * `aria-sort` value for a column header: `'ascending'`/`'descending'`
1527
- * for the PRIMARY (index 0) active sort key only — secondary/tertiary
1528
- * keys carry the visible numbered badge instead, per WCAG guidance
1529
- * that `aria-sort` describes single-column sort state. `null` omits
1530
- * the attribute entirely (unsorted / not sortable).
1667
+ * The 1-based priority badge for `key` among the rendered sort keys, or null when fewer than two rendered columns are sorted or `key` is not one of them.
1668
+ *
1669
+ * @param {string} key Column key.
1670
+ * @return {number|null}
1671
+ */
1672
+ sortBadgeFor(key) {
1673
+ if (this.renderedSortKeys.length < 2) {
1674
+ return null
1675
+ }
1676
+ const index = this.renderedSortKeys.findIndex((k) => k.key === key)
1677
+ return index === -1 ? null : index + 1
1678
+ },
1679
+
1680
+ /**
1681
+ * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the PRIMARY (index 0) active sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1682
+ * `null` omits the attribute entirely (unsorted, not sortable, or not the primary key).
1531
1683
  *
1532
1684
  * @param {object} col Column definition.
1533
1685
  * @return {string|null}
@@ -1551,6 +1703,135 @@ export default {
1551
1703
  * @param {string} key Column key.
1552
1704
  * @param {MouseEvent} [event] The originating click event.
1553
1705
  */
1706
+ /**
1707
+ * The filter definition for a column, or null when it does not filter
1708
+ * (header filters off, `filterable: false`, or nothing to filter on).
1709
+ *
1710
+ * @param {object} col The column.
1711
+ * @return {object|null}
1712
+ */
1713
+ filterDefFor(col) {
1714
+ if (!this.filterable || !col) {
1715
+ return null
1716
+ }
1717
+ return columnFilterDef(col, this.schema)
1718
+ },
1719
+
1720
+ columnFilterStateFor(col) {
1721
+ return columnFilterState(this.filterDefFor(col), this.activeFilters)
1722
+ },
1723
+
1724
+ isColumnFiltered(col) {
1725
+ const def = this.filterDefFor(col)
1726
+ return !!def && isColumnFilterActive(def, this.activeFilters)
1727
+ },
1728
+
1729
+ filterButtonLabel(col) {
1730
+ const column = this.translateLabel(col.label)
1731
+ return this.isColumnFiltered(col)
1732
+ ? t('nextcloud-vue', 'Filter {column}, active', { column })
1733
+ : t('nextcloud-vue', 'Filter {column}', { column })
1734
+ },
1735
+
1736
+ /**
1737
+ * Short text for an active filter: the picked labels, or the range.
1738
+ *
1739
+ * @param {object} col The column.
1740
+ * @return {string}
1741
+ */
1742
+ columnFilterSummary(col) {
1743
+ const def = this.filterDefFor(col)
1744
+ const state = this.columnFilterStateFor(col)
1745
+ if (!def) {
1746
+ return ''
1747
+ }
1748
+ if (def.kind === 'enum') {
1749
+ const labels = Object.fromEntries((def.options || []).map((o) => [o.value, o.label]))
1750
+ return state.values.map((v) => this.translateLabel(labels[v] || v)).join(', ')
1751
+ }
1752
+ if (def.kind === 'reference') {
1753
+ return n('nextcloud-vue', '{count} selected', '{count} selected', state.values.length, { count: state.values.length })
1754
+ }
1755
+ if (def.kind === 'number' || def.kind === 'date') {
1756
+ if (state.from && state.to) {
1757
+ return t('nextcloud-vue', '{from} to {to}', { from: state.from, to: state.to })
1758
+ }
1759
+ return state.from
1760
+ ? t('nextcloud-vue', 'from {value}', { value: state.from })
1761
+ : t('nextcloud-vue', 'up to {value}', { value: state.to })
1762
+ }
1763
+ if (def.kind === 'boolean') {
1764
+ return state.value === 'true' ? t('nextcloud-vue', 'Yes') : t('nextcloud-vue', 'No')
1765
+ }
1766
+ return state.value
1767
+ },
1768
+
1769
+ /**
1770
+ * Open or close a column's filter panel under its button.
1771
+ *
1772
+ * @param {object} col The column.
1773
+ * @param {Event} event The click.
1774
+ * @return {void}
1775
+ */
1776
+ toggleColumnFilter(col, event) {
1777
+ if (this.openFilterKey === col.key) {
1778
+ this.closeColumnFilter()
1779
+ return
1780
+ }
1781
+ const target = event && event.currentTarget
1782
+ this.filterAnchorRect = target && typeof target.getBoundingClientRect === 'function'
1783
+ ? target.getBoundingClientRect()
1784
+ : null
1785
+ this.openFilterKey = col.key
1786
+ },
1787
+
1788
+ /**
1789
+ * Close the open panel and give focus back to its button.
1790
+ *
1791
+ * @return {void}
1792
+ */
1793
+ closeColumnFilter() {
1794
+ const key = this.openFilterKey
1795
+ this.openFilterKey = ''
1796
+ if (!key) {
1797
+ return
1798
+ }
1799
+ this.$nextTick(() => {
1800
+ const ref = this.$refs['filterButton-' + key]
1801
+ const button = Array.isArray(ref) ? ref[0] : ref
1802
+ if (button && typeof button.focus === 'function') {
1803
+ button.focus()
1804
+ }
1805
+ })
1806
+ },
1807
+
1808
+ /**
1809
+ * Apply a panel's state: emit the query parameters it stands for.
1810
+ *
1811
+ * @param {object} col The column.
1812
+ * @param {object} state The panel state.
1813
+ * @return {void}
1814
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-a-header-filter-speaks-the-sidebars-query-language
1815
+ */
1816
+ applyColumnFilter(col, state) {
1817
+ const def = this.filterDefFor(col)
1818
+ if (def) {
1819
+ /**
1820
+ * @event column-filter A header filter was applied or cleared.
1821
+ * @type {{ key: string, params: object }} `params` maps each query parameter the column owns to its values; an empty list clears it.
1822
+ */
1823
+ this.$emit('column-filter', { key: col.key, params: columnFilterParams(def, state) })
1824
+ }
1825
+ this.closeColumnFilter()
1826
+ },
1827
+
1828
+ clearColumnFilter(col) {
1829
+ const def = this.filterDefFor(col)
1830
+ if (def) {
1831
+ this.$emit('column-filter', { key: col.key, params: clearedColumnFilterParams(def) })
1832
+ }
1833
+ },
1834
+
1554
1835
  onHeaderClick(key, event) {
1555
1836
  this.applySort(key, !!(event && event.shiftKey))
1556
1837
  },
@@ -74,9 +74,9 @@
74
74
  <template v-if="widget && widget.icon" #title-icon>
75
75
  <CnIcon :name="widget.icon" :size="20" />
76
76
  </template>
77
- <NcEmptyContent :name="missingAppName" :description="missingAppDescription">
77
+ <NcEmptyContent class="cn-requires-app" :name="missingAppName" :description="missingAppDescription">
78
78
  <template #icon>
79
- <CnIcon :name="(widget && widget.icon) || 'PuzzleOutline'" :size="44" />
79
+ <CnIcon :name="(widget && widget.icon) || 'PuzzleOutline'" :size="32" />
80
80
  </template>
81
81
  </NcEmptyContent>
82
82
  </CnWidgetWrapper>
@@ -2,7 +2,7 @@
2
2
  <!-- Keeps the legacy `cn-form-dialog__helper` class alongside its own so
3
3
  consumer stylesheets that target the old helper span still apply. -->
4
4
  <span
5
- v-if="error || text"
5
+ v-if="error || text || more"
6
6
  class="cn-field-helper cn-form-dialog__helper"
7
7
  :class="{ 'cn-field-helper--error': !!error, 'cn-form-dialog__helper--error': !!error }">
8
8
  {{ error || text }}
@@ -42,7 +42,9 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
42
42
  * `fieldsFromSchema` splits it and puts the full text on the field's
43
43
  * `descriptionLong`; pass that as `more` and this renders an ⓘ button that
44
44
  * reveals it in a popover, so a paragraph-length schema description cannot
45
- * push the rest of the form off screen.
45
+ * push the rest of the form off screen. A schema property's `x-help` text
46
+ * also lands on `descriptionLong`, so the ⓘ shows even when the description
47
+ * is short or absent.
46
48
  *
47
49
  * `CnFormDialog` uses this for every auto-generated field. Use it directly when
48
50
  * rendering your own fields through the `#form-fields` or `#field-<key>` slots,