@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.
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
- package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
- package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +65 -0
- package/dist/esm/composables/useDependencyCheck.js.map +1 -0
- package/dist/esm/composables/useWalkthrough.js +218 -6
- package/dist/esm/composables/useWalkthrough.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +48 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +48 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +557 -9
- package/dist/esm/store/useObjectStore.js +8 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +311 -0
- package/dist/esm/utils/columnFilters.js.map +1 -0
- package/dist/esm/utils/groupAutocomplete.js +123 -0
- package/dist/esm/utils/groupAutocomplete.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/pickerOptions.js +198 -0
- package/dist/esm/utils/pickerOptions.js.map +1 -0
- package/dist/esm/utils/schema.js +83 -7
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/timelineEvents.js +174 -0
- package/dist/esm/utils/timelineEvents.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/walkthroughSignals.js +88 -0
- package/dist/esm/utils/walkthroughSignals.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +9779 -5236
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +557 -9
- package/l10n/en.json +48 -0
- package/l10n/nl.json +48 -0
- package/package.json +3 -2
- package/scripts/manifest-schema-hash.js +41 -0
- package/scripts/record-manifest-schema-hash.js +52 -0
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +394 -27
- package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
- package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
- package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +4 -1
- package/src/components/CnDataTable/CnDataTable.vue +302 -21
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
- package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
- package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
- package/src/components/CnIndexPage/selfModeActions.js +3 -3
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
- package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnStorePage/CnStorePage.vue +7 -0
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
- package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
- package/src/components/CnTimelineWidget/index.js +4 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +2 -0
- package/src/composables/useDependencyCheck.js +61 -0
- package/src/composables/useWalkthrough.js +217 -5
- package/src/css/dashboard.css +49 -0
- package/src/css/detail-page.css +19 -0
- package/src/css/table.css +82 -2
- package/src/index.js +2 -0
- package/src/public/index.js +5 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -1
- package/src/store/useObjectStore.js +8 -0
- package/src/utils/columnFilters.js +324 -0
- package/src/utils/groupAutocomplete.js +119 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/pickerOptions.js +194 -0
- package/src/utils/schema.js +83 -7
- package/src/utils/timelineEvents.js +171 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- 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 &&
|
|
138
|
+
v-if="col.sortable && sortBadgeFor(col.key) !== null"
|
|
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'
|
|
@@ -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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1527
|
-
*
|
|
1528
|
-
*
|
|
1529
|
-
*
|
|
1530
|
-
|
|
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="
|
|
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,
|