@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
@@ -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">
@@ -108,6 +139,23 @@
108
139
  class="cn-table-sort-badge">
109
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 -->
@@ -230,6 +278,14 @@
230
278
  :row="row"
231
279
  :rowKey="rowKey" />
232
280
  </slot>
281
+ <!-- A second, muted line under the value, from `col.secondary`:
282
+ a field key, a "{field} · {other}" template, or a function
283
+ of the row. Lets one column read "title" over "number ·
284
+ requester" without a custom cell. -->
285
+ <span
286
+ v-if="secondaryValue(row, col)"
287
+ class="cn-table-cell__secondary"
288
+ data-testid="cn-cell-secondary">{{ secondaryValue(row, col) }}</span>
233
289
  </td>
234
290
 
235
291
  <!-- Row actions -->
@@ -283,10 +339,14 @@
283
339
 
284
340
  <script>
285
341
  import axios from '@nextcloud/axios'
286
- import { translate as t } from '@nextcloud/l10n'
342
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
287
343
  import { generateUrl } from '@nextcloud/router'
288
344
  import { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
345
+ import FilterIcon from 'vue-material-design-icons/Filter.vue'
346
+ import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
347
+ import CnColumnFilterPopover from './CnColumnFilterPopover.vue'
289
348
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
349
+ import { clearedColumnFilterParams, columnFilterDef, columnFilterParams, columnFilterState, isColumnFilterActive, isColumnSortable } from '../../utils/columnFilters.js'
290
350
  import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
291
351
  import { nextSortState } from '../../utils/multiColumnSort.js'
292
352
  import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
@@ -357,8 +417,11 @@ export default {
357
417
  NcLoadingIcon,
358
418
  NcCheckboxRadioSwitch,
359
419
  CnCellRenderer,
420
+ CnColumnFilterPopover,
360
421
  CnIcon,
361
422
  CnLockIndicator,
423
+ FilterIcon,
424
+ FilterOutline,
362
425
  },
363
426
 
364
427
  inject: {
@@ -389,7 +452,13 @@ export default {
389
452
  * reader is looking. `columnsFromSchema` fills it from the JSON Schema property
390
453
  * description automatically, so schema-driven tables get it for free.
391
454
  *
392
- * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string}|string>}
455
+ * `secondary` draws a second, muted line under the cell's value: a field
456
+ * key (`"number"`), a template with `{field}` placeholders
457
+ * (`"{number} · {requester}"`, dotted paths allowed), or a function of
458
+ * the row. Empty fields leave their placeholder blank; a line that
459
+ * resolves to nothing is not drawn.
460
+ *
461
+ * @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string, secondary: (string|((row: object) => string))}|string>}
393
462
  */
394
463
  columns: {
395
464
  type: Array,
@@ -727,9 +796,39 @@ export default {
727
796
  type: Boolean,
728
797
  default: false,
729
798
  },
799
+
800
+ /**
801
+ * Show a filter button in every header whose column can filter (see
802
+ * `columnFilterDef`): enum, boolean, text, number, date and reference
803
+ * columns backed by a schema property. A column opts out with
804
+ * `filterable: false`. Applying a filter emits `column-filter`; the
805
+ * host owns the filter state and passes it back as `activeFilters`.
806
+ *
807
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
808
+ */
809
+ filterable: {
810
+ type: Boolean,
811
+ default: false,
812
+ },
813
+
814
+ /**
815
+ * The active filter map, `{ paramKey: values[] }`: the same map the
816
+ * facet sidebar writes, so a header filter and a sidebar filter on one
817
+ * field show the same state.
818
+ */
819
+ activeFilters: {
820
+ type: Object,
821
+ default: () => ({}),
822
+ },
823
+
824
+ /** Register slug a reference column's filter searches in when the column names none. */
825
+ filterRegister: {
826
+ type: String,
827
+ default: '',
828
+ },
730
829
  },
731
830
 
732
- emits: ['row-click', 'row-aux-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all'],
831
+ emits: ['row-click', 'row-aux-click', 'row-context-menu', 'select', 'select-all', 'sort', 'view-all', 'column-filter'],
733
832
 
734
833
  setup() {
735
834
  // Tell a deliberate row click apart from a text-selection drag.
@@ -738,6 +837,10 @@ export default {
738
837
 
739
838
  data() {
740
839
  return {
840
+ /** Key of the column whose filter panel is open, or ''. */
841
+ openFilterKey: '',
842
+ /** The open filter button's bounding rect, to place the panel. */
843
+ filterAnchorRect: null,
741
844
  /**
742
845
  * Resolved aggregate-column values, keyed by `String(row[rowKey])`
743
846
  * then by column key. Populated by `loadAggregates()` for columns
@@ -855,8 +958,12 @@ export default {
855
958
  overrides: this.columnOverrides,
856
959
  })
857
960
  : this.columns
961
+ // Every column backed by a schema property sorts unless it says
962
+ // `sortable: false`; object columns from a manifest used to need an
963
+ // explicit flag and so could not sort at all.
964
+ const withSort = (c) => (c && typeof c === 'object' ? { ...c, sortable: isColumnSortable(c, this.schema) } : c)
858
965
  if (!(cols || []).some((c) => typeof c === 'string')) {
859
- return cols || []
966
+ return (cols || []).map(withSort)
860
967
  }
861
968
  const schemaCols = this.schema
862
969
  ? columnsFromSchema(this.schema, { overrides: this.columnOverrides })
@@ -864,7 +971,7 @@ export default {
864
971
  const byKey = new Map(schemaCols.map((c) => [c.key, c]))
865
972
  return (cols || []).map((c) => {
866
973
  if (typeof c !== 'string') {
867
- return c
974
+ return withSort(c)
868
975
  }
869
976
  return byKey.get(c) || { key: c, label: c, sortable: true }
870
977
  })
@@ -905,6 +1012,40 @@ export default {
905
1012
  return links
906
1013
  },
907
1014
 
1015
+ /**
1016
+ * The column whose filter panel is open, or null.
1017
+ *
1018
+ * @return {object|null}
1019
+ */
1020
+ openFilterColumn() {
1021
+ if (!this.openFilterKey) {
1022
+ return null
1023
+ }
1024
+ return this.effectiveColumns.find((c) => c && c.key === this.openFilterKey && this.filterDefFor(c)) || null
1025
+ },
1026
+
1027
+ /**
1028
+ * One chip per column with an active header filter.
1029
+ *
1030
+ * @return {Array<{key: string, col: object, text: string, removeLabel: string}>}
1031
+ */
1032
+ activeColumnFilterChips() {
1033
+ if (!this.filterable) {
1034
+ return []
1035
+ }
1036
+ return this.effectiveColumns
1037
+ .filter((c) => c && this.isColumnFiltered(c))
1038
+ .map((col) => {
1039
+ const label = this.translateLabel(col.label)
1040
+ const text = t('nextcloud-vue', '{column}: {value}', { column: label, value: this.columnFilterSummary(col) })
1041
+ return { key: col.key, col, text, removeLabel: t('nextcloud-vue', 'Remove filter {column}', { column: label }) }
1042
+ })
1043
+ },
1044
+
1045
+ activeFiltersLabel() {
1046
+ return t('nextcloud-vue', 'Active filters')
1047
+ },
1048
+
908
1049
  effectiveSortKeys() {
909
1050
  if (this.sortKeys && this.sortKeys.length > 0) {
910
1051
  return this.sortKeys
@@ -1288,6 +1429,36 @@ export default {
1288
1429
  * @param {object} col The column definition.
1289
1430
  * @return {unknown} The value handed to the slot / CnCellRenderer.
1290
1431
  */
1432
+ /**
1433
+ * The secondary line of a cell, from `col.secondary`: a function is
1434
+ * called with the row; a string with `{field}` placeholders is a
1435
+ * template filled from the row (dotted paths allowed); any other
1436
+ * string is a field key. Returns '' when there is nothing to draw.
1437
+ *
1438
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-table-column-carries-a-secondary-line
1439
+ * @param {object} row The row.
1440
+ * @param {object} col The column definition.
1441
+ * @return {string} The secondary text, or ''.
1442
+ */
1443
+ secondaryValue(row, col) {
1444
+ const spec = col?.secondary
1445
+ if (typeof spec === 'function') {
1446
+ const out = spec(row)
1447
+ return out === null || out === undefined ? '' : String(out)
1448
+ }
1449
+ if (typeof spec !== 'string' || spec === '') {
1450
+ return ''
1451
+ }
1452
+ const asText = (v) => (v === null || v === undefined ? '' : String(v))
1453
+ if (spec.includes('{')) {
1454
+ const filled = spec.replace(/\{([^}]+)\}/g, (_, key) => asText(this.getCellValue(row, key.trim())))
1455
+ // A template whose every field is empty leaves only separators:
1456
+ // nothing worth a line.
1457
+ return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''
1458
+ }
1459
+ return asText(this.getCellValue(row, spec))
1460
+ },
1461
+
1291
1462
  cellValue(row, col) {
1292
1463
  if (col && col.aggregate) {
1293
1464
  const cached = this.aggregateValues[String(row[this.rowKey])]
@@ -1576,6 +1747,135 @@ export default {
1576
1747
  * @param {string} key Column key.
1577
1748
  * @param {MouseEvent} [event] The originating click event.
1578
1749
  */
1750
+ /**
1751
+ * The filter definition for a column, or null when it does not filter
1752
+ * (header filters off, `filterable: false`, or nothing to filter on).
1753
+ *
1754
+ * @param {object} col The column.
1755
+ * @return {object|null}
1756
+ */
1757
+ filterDefFor(col) {
1758
+ if (!this.filterable || !col) {
1759
+ return null
1760
+ }
1761
+ return columnFilterDef(col, this.schema)
1762
+ },
1763
+
1764
+ columnFilterStateFor(col) {
1765
+ return columnFilterState(this.filterDefFor(col), this.activeFilters)
1766
+ },
1767
+
1768
+ isColumnFiltered(col) {
1769
+ const def = this.filterDefFor(col)
1770
+ return !!def && isColumnFilterActive(def, this.activeFilters)
1771
+ },
1772
+
1773
+ filterButtonLabel(col) {
1774
+ const column = this.translateLabel(col.label)
1775
+ return this.isColumnFiltered(col)
1776
+ ? t('nextcloud-vue', 'Filter {column}, active', { column })
1777
+ : t('nextcloud-vue', 'Filter {column}', { column })
1778
+ },
1779
+
1780
+ /**
1781
+ * Short text for an active filter: the picked labels, or the range.
1782
+ *
1783
+ * @param {object} col The column.
1784
+ * @return {string}
1785
+ */
1786
+ columnFilterSummary(col) {
1787
+ const def = this.filterDefFor(col)
1788
+ const state = this.columnFilterStateFor(col)
1789
+ if (!def) {
1790
+ return ''
1791
+ }
1792
+ if (def.kind === 'enum') {
1793
+ const labels = Object.fromEntries((def.options || []).map((o) => [o.value, o.label]))
1794
+ return state.values.map((v) => this.translateLabel(labels[v] || v)).join(', ')
1795
+ }
1796
+ if (def.kind === 'reference') {
1797
+ return n('nextcloud-vue', '{count} selected', '{count} selected', state.values.length, { count: state.values.length })
1798
+ }
1799
+ if (def.kind === 'number' || def.kind === 'date') {
1800
+ if (state.from && state.to) {
1801
+ return t('nextcloud-vue', '{from} to {to}', { from: state.from, to: state.to })
1802
+ }
1803
+ return state.from
1804
+ ? t('nextcloud-vue', 'from {value}', { value: state.from })
1805
+ : t('nextcloud-vue', 'up to {value}', { value: state.to })
1806
+ }
1807
+ if (def.kind === 'boolean') {
1808
+ return state.value === 'true' ? t('nextcloud-vue', 'Yes') : t('nextcloud-vue', 'No')
1809
+ }
1810
+ return state.value
1811
+ },
1812
+
1813
+ /**
1814
+ * Open or close a column's filter panel under its button.
1815
+ *
1816
+ * @param {object} col The column.
1817
+ * @param {Event} event The click.
1818
+ * @return {void}
1819
+ */
1820
+ toggleColumnFilter(col, event) {
1821
+ if (this.openFilterKey === col.key) {
1822
+ this.closeColumnFilter()
1823
+ return
1824
+ }
1825
+ const target = event && event.currentTarget
1826
+ this.filterAnchorRect = target && typeof target.getBoundingClientRect === 'function'
1827
+ ? target.getBoundingClientRect()
1828
+ : null
1829
+ this.openFilterKey = col.key
1830
+ },
1831
+
1832
+ /**
1833
+ * Close the open panel and give focus back to its button.
1834
+ *
1835
+ * @return {void}
1836
+ */
1837
+ closeColumnFilter() {
1838
+ const key = this.openFilterKey
1839
+ this.openFilterKey = ''
1840
+ if (!key) {
1841
+ return
1842
+ }
1843
+ this.$nextTick(() => {
1844
+ const ref = this.$refs['filterButton-' + key]
1845
+ const button = Array.isArray(ref) ? ref[0] : ref
1846
+ if (button && typeof button.focus === 'function') {
1847
+ button.focus()
1848
+ }
1849
+ })
1850
+ },
1851
+
1852
+ /**
1853
+ * Apply a panel's state: emit the query parameters it stands for.
1854
+ *
1855
+ * @param {object} col The column.
1856
+ * @param {object} state The panel state.
1857
+ * @return {void}
1858
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-a-header-filter-speaks-the-sidebars-query-language
1859
+ */
1860
+ applyColumnFilter(col, state) {
1861
+ const def = this.filterDefFor(col)
1862
+ if (def) {
1863
+ /**
1864
+ * @event column-filter A header filter was applied or cleared.
1865
+ * @type {{ key: string, params: object }} `params` maps each query parameter the column owns to its values; an empty list clears it.
1866
+ */
1867
+ this.$emit('column-filter', { key: col.key, params: columnFilterParams(def, state) })
1868
+ }
1869
+ this.closeColumnFilter()
1870
+ },
1871
+
1872
+ clearColumnFilter(col) {
1873
+ const def = this.filterDefFor(col)
1874
+ if (def) {
1875
+ this.$emit('column-filter', { key: col.key, params: clearedColumnFilterParams(def) })
1876
+ }
1877
+ },
1878
+
1579
1879
  onHeaderClick(key, event) {
1580
1880
  this.applySort(key, !!(event && event.shiftKey))
1581
1881
  },
@@ -430,3 +430,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
430
430
  ## Not found
431
431
 
432
432
  When the schema-driven fetch answers 404 the page shows a not-found state whose back button is a router link to `notFoundRoute` (default `null`, which means the app root `/`). `notFoundRouteLabel` names that page in the button's "Back to …" label.
433
+
434
+ ### Type eyebrow and breadcrumb
435
+
436
+ `showTypeEyebrow` (default `true`) can drop the type label above the record name. `breadcrumb` (`{ label, route?, params?, href? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb.
@@ -41,6 +41,13 @@
41
41
  {{ skipLinkLabel }}
42
42
  </a>
43
43
 
44
+ <!-- Breadcrumb line (manifest `config.breadcrumb`): the list the record
45
+ belongs to, then the record itself as the current crumb. -->
46
+ <CnBreadcrumbs
47
+ v-if="breadcrumbCrumbs.length > 0 && !objectNotFound"
48
+ class="cn-detail-page__breadcrumbs"
49
+ :crumbs="breadcrumbCrumbs"
50
+ data-testid="cn-detail-page-breadcrumbs" />
44
51
  <!-- Header -->
45
52
  <div v-if="!objectNotFound" class="cn-detail-page__header" data-testid="cn-detail-page-header">
46
53
  <!-- Header (left block) — overridable via #header slot. Default
@@ -98,7 +105,7 @@
98
105
  (ADR-062). Only shown once the object resolves to a
99
106
  display name that differs from the type label. -->
100
107
  <p
101
- v-if="typeEyebrow"
108
+ v-if="typeEyebrow && showTypeEyebrow"
102
109
  class="cn-detail-page__type-eyebrow"
103
110
  data-testid="cn-detail-page-type-eyebrow">
104
111
  {{ typeEyebrow }}
@@ -1025,6 +1032,7 @@ import {
1025
1032
  widgetTitleOf,
1026
1033
  } from '../../utils/widgetDispatch.js'
1027
1034
  import { CnActionButtons } from '../CnActionButtons/index.js'
1035
+ import { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'
1028
1036
  import { CnIcon } from '../CnIcon/index.js'
1029
1037
  import { CnStatusBadge } from '../CnStatusBadge/index.js'
1030
1038
  import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
@@ -1172,6 +1180,7 @@ export default {
1172
1180
  CnDashboardGrid,
1173
1181
  CnLifecycleActions,
1174
1182
  CnActionButtons,
1183
+ CnBreadcrumbs,
1175
1184
  CnStatusBadge,
1176
1185
  CnSummaryAggregates,
1177
1186
  CnRelatedCollections,
@@ -2078,6 +2087,34 @@ export default {
2078
2087
  default: null,
2079
2088
  },
2080
2089
 
2090
+ /**
2091
+ * Whether the type eyebrow (the type label above the record name)
2092
+ * renders once the record resolves. `false` (manifest
2093
+ * `config.showTypeEyebrow: false`) drops it for a header that says
2094
+ * the type in a pill instead.
2095
+ *
2096
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
2097
+ */
2098
+ showTypeEyebrow: {
2099
+ type: Boolean,
2100
+ default: true,
2101
+ },
2102
+
2103
+ /**
2104
+ * A breadcrumb line above the header (manifest `config.breadcrumb`):
2105
+ * `{ label, route?, params?, href? }` names the list the record
2106
+ * belongs to; the record's display name follows as the current
2107
+ * crumb. `label` goes through the host translate function. Null (the
2108
+ * default) draws no trail.
2109
+ *
2110
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
2111
+ * @type {{label: string, route?: string, params?: object, href?: string}|null}
2112
+ */
2113
+ breadcrumb: {
2114
+ type: Object,
2115
+ default: null,
2116
+ },
2117
+
2081
2118
  /**
2082
2119
  * A column of cards beside the body (manifest `config.sideColumn`),
2083
2120
  * for the record's facts: deadline, requester, handler, links. Each
@@ -3201,6 +3238,28 @@ export default {
3201
3238
  return this.objectDisplayName || this.resolvedTitle
3202
3239
  },
3203
3240
 
3241
+ /**
3242
+ * The breadcrumb trail: the declared crumb (a router target from
3243
+ * `route` + `params`, or an `href`), then the record's display name
3244
+ * as the current crumb. Empty without a `breadcrumb` label.
3245
+ *
3246
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
3247
+ * @return {Array<{label: string, to?: object, href?: string}>}
3248
+ */
3249
+ breadcrumbCrumbs() {
3250
+ const crumb = this.breadcrumb
3251
+ if (!crumb || typeof crumb.label !== 'string' || crumb.label === '') {
3252
+ return []
3253
+ }
3254
+ const first = { label: this.effectiveTranslate(crumb.label) }
3255
+ if (crumb.route) {
3256
+ first.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
3257
+ } else if (crumb.href) {
3258
+ first.href = crumb.href
3259
+ }
3260
+ return [first, { label: this.displayTitle }]
3261
+ },
3262
+
3204
3263
  /**
3205
3264
  * Effective translate function: the injected `cnTranslate` (the host
3206
3265
  * app's bound `t()`), identity by default.
@@ -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,