@conduction/nextcloud-vue 2.62.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 (154) 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 +271 -32
  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/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  21. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  22. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  23. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  24. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  25. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  34. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  36. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  50. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  52. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  53. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  54. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  56. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  57. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  58. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  65. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  66. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  67. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  68. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  69. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  70. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  71. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  72. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  73. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  74. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  79. package/dist/esm/composables/useDependencyCheck.js +65 -0
  80. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  81. package/dist/esm/composables/useWalkthrough.js +218 -6
  82. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  83. package/dist/esm/index.js +2 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +43 -0
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +43 -0
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +449 -0
  90. package/dist/esm/store/useObjectStore.js +8 -0
  91. package/dist/esm/store/useObjectStore.js.map +1 -1
  92. package/dist/esm/utils/columnFilters.js +311 -0
  93. package/dist/esm/utils/columnFilters.js.map +1 -0
  94. package/dist/esm/utils/groupAutocomplete.js +123 -0
  95. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  96. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  97. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  98. package/dist/esm/utils/pickerOptions.js +198 -0
  99. package/dist/esm/utils/pickerOptions.js.map +1 -0
  100. package/dist/esm/utils/schema.js +83 -7
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/timelineEvents.js +174 -0
  103. package/dist/esm/utils/timelineEvents.js.map +1 -0
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/walkthroughSignals.js +88 -0
  107. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  108. package/dist/nextcloud-vue.cjs.js +9369 -5193
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +449 -0
  111. package/l10n/en.json +43 -0
  112. package/l10n/nl.json +43 -0
  113. package/package.json +1 -1
  114. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  115. package/src/components/CnAdminActionCard/index.js +4 -0
  116. package/src/components/CnAppNav/CnAppNav.vue +271 -32
  117. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  118. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  119. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
  120. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  121. package/src/components/CnDataTable/CnDataTable.md +3 -0
  122. package/src/components/CnDataTable/CnDataTable.vue +260 -4
  123. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  124. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  125. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  126. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  127. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  129. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  130. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  131. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  132. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  133. package/src/components/CnTimelineWidget/index.js +4 -0
  134. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  135. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  136. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  137. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  138. package/src/components/index.js +2 -0
  139. package/src/composables/useDependencyCheck.js +61 -0
  140. package/src/composables/useWalkthrough.js +217 -5
  141. package/src/css/dashboard.css +49 -0
  142. package/src/css/detail-page.css +19 -0
  143. package/src/css/table.css +81 -0
  144. package/src/index.js +2 -0
  145. package/src/schemas/app-manifest-v2.schema.json +23 -1
  146. package/src/store/useObjectStore.js +8 -0
  147. package/src/utils/columnFilters.js +324 -0
  148. package/src/utils/groupAutocomplete.js +119 -0
  149. package/src/utils/libraryWidgetKeys.js +2 -0
  150. package/src/utils/pickerOptions.js +194 -0
  151. package/src/utils/schema.js +83 -7
  152. package/src/utils/timelineEvents.js +171 -0
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
  154. 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 -->
@@ -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'
@@ -357,8 +409,11 @@ export default {
357
409
  NcLoadingIcon,
358
410
  NcCheckboxRadioSwitch,
359
411
  CnCellRenderer,
412
+ CnColumnFilterPopover,
360
413
  CnIcon,
361
414
  CnLockIndicator,
415
+ FilterIcon,
416
+ FilterOutline,
362
417
  },
363
418
 
364
419
  inject: {
@@ -727,9 +782,39 @@ export default {
727
782
  type: Boolean,
728
783
  default: false,
729
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
+ },
730
815
  },
731
816
 
732
- 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'],
733
818
 
734
819
  setup() {
735
820
  // Tell a deliberate row click apart from a text-selection drag.
@@ -738,6 +823,10 @@ export default {
738
823
 
739
824
  data() {
740
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,
741
830
  /**
742
831
  * Resolved aggregate-column values, keyed by `String(row[rowKey])`
743
832
  * then by column key. Populated by `loadAggregates()` for columns
@@ -855,8 +944,12 @@ export default {
855
944
  overrides: this.columnOverrides,
856
945
  })
857
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)
858
951
  if (!(cols || []).some((c) => typeof c === 'string')) {
859
- return cols || []
952
+ return (cols || []).map(withSort)
860
953
  }
861
954
  const schemaCols = this.schema
862
955
  ? columnsFromSchema(this.schema, { overrides: this.columnOverrides })
@@ -864,7 +957,7 @@ export default {
864
957
  const byKey = new Map(schemaCols.map((c) => [c.key, c]))
865
958
  return (cols || []).map((c) => {
866
959
  if (typeof c !== 'string') {
867
- return c
960
+ return withSort(c)
868
961
  }
869
962
  return byKey.get(c) || { key: c, label: c, sortable: true }
870
963
  })
@@ -905,6 +998,40 @@ export default {
905
998
  return links
906
999
  },
907
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
+
908
1035
  effectiveSortKeys() {
909
1036
  if (this.sortKeys && this.sortKeys.length > 0) {
910
1037
  return this.sortKeys
@@ -1576,6 +1703,135 @@ export default {
1576
1703
  * @param {string} key Column key.
1577
1704
  * @param {MouseEvent} [event] The originating click event.
1578
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
+
1579
1835
  onHeaderClick(key, event) {
1580
1836
  this.applySort(key, !!(event && event.shiftKey))
1581
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,