@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
|
@@ -282,9 +282,9 @@
|
|
|
282
282
|
<NcEmptyContent
|
|
283
283
|
:name="installAppLabel(missingRequiredApp(item))"
|
|
284
284
|
:description="t('nextcloud-vue', 'This widget shows data from another app that isn\'t installed yet.')"
|
|
285
|
-
class="cn-dashboard-page__requires-app">
|
|
285
|
+
class="cn-dashboard-page__requires-app cn-requires-app">
|
|
286
286
|
<template #icon>
|
|
287
|
-
<Download :size="
|
|
287
|
+
<Download :size="24" />
|
|
288
288
|
</template>
|
|
289
289
|
<template #action>
|
|
290
290
|
<NcButton
|
|
@@ -2752,8 +2752,12 @@ export default {
|
|
|
2752
2752
|
* which for an attention card is its `title` when it has no `text`.
|
|
2753
2753
|
*
|
|
2754
2754
|
* @param {object} def Widget definition.
|
|
2755
|
+
* `layout` is returned so the page can tell an attention card from a
|
|
2756
|
+
* plain banner: only the former keeps its cell when its check fails.
|
|
2757
|
+
*
|
|
2755
2758
|
* @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-an-attention-card-with-a-title-keeps-its-cell
|
|
2756
|
-
* @
|
|
2759
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2760
|
+
* @return {{ text: string, visibleWhen: (object|null), layout: string }}
|
|
2757
2761
|
*/
|
|
2758
2762
|
widgetDisplayConfig(def) {
|
|
2759
2763
|
const content = (def && typeof def.content === 'object' && def.content) || {}
|
|
@@ -2767,6 +2771,7 @@ export default {
|
|
|
2767
2771
|
return {
|
|
2768
2772
|
text: content.text || props.text || title || '',
|
|
2769
2773
|
visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
|
|
2774
|
+
layout,
|
|
2770
2775
|
}
|
|
2771
2776
|
},
|
|
2772
2777
|
|
|
@@ -2776,15 +2781,22 @@ export default {
|
|
|
2776
2781
|
* text at all — and must therefore surrender its grid cell in live
|
|
2777
2782
|
* mode instead of leaving an empty card behind.
|
|
2778
2783
|
*
|
|
2784
|
+
* One exception: an attention card whose `visibleWhen` request FAILED
|
|
2785
|
+
* keeps its cell, so the card can say it could not check. A collapsed
|
|
2786
|
+
* cell reads as "nothing needs attention", which a failed count does
|
|
2787
|
+
* not know. A count that was read and does not meet the condition
|
|
2788
|
+
* still collapses.
|
|
2789
|
+
*
|
|
2779
2790
|
* @param {object} item Layout item.
|
|
2780
2791
|
* @return {boolean} true when the cell must collapse.
|
|
2792
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2781
2793
|
*/
|
|
2782
2794
|
isCollapsedWidget(item) {
|
|
2783
2795
|
const def = this.getWidgetDef(item.widgetId)
|
|
2784
2796
|
if (!def || !def.type) {
|
|
2785
2797
|
return false
|
|
2786
2798
|
}
|
|
2787
|
-
const { text, visibleWhen } = this.widgetDisplayConfig(def)
|
|
2799
|
+
const { text, visibleWhen, layout } = this.widgetDisplayConfig(def)
|
|
2788
2800
|
if (this.isBannerDef(def) && text === '') {
|
|
2789
2801
|
return true
|
|
2790
2802
|
}
|
|
@@ -2792,6 +2804,9 @@ export default {
|
|
|
2792
2804
|
return false
|
|
2793
2805
|
}
|
|
2794
2806
|
const outcome = this.widgetConditionOutcome[item.widgetId]
|
|
2807
|
+
if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
|
|
2808
|
+
return false
|
|
2809
|
+
}
|
|
2795
2810
|
return !outcome || outcome.met !== true
|
|
2796
2811
|
},
|
|
2797
2812
|
|
|
@@ -2802,13 +2817,17 @@ export default {
|
|
|
2802
2817
|
* Runs on created() and again whenever `widgets` changes (the
|
|
2803
2818
|
* in-app editor mutates defs in place). Fail-safe like the banner
|
|
2804
2819
|
* itself: any fetch/shape error counts as "not met", so a broken
|
|
2805
|
-
* predicate collapses the cell rather than breaking the page.
|
|
2820
|
+
* predicate collapses the cell rather than breaking the page. The
|
|
2821
|
+
* outcome of a failed read carries `failed: true` and the `reason`,
|
|
2822
|
+
* which `isCollapsedWidget` and CnBannerWidget use to show an
|
|
2823
|
+
* attention card's "Could not check" line instead of nothing.
|
|
2806
2824
|
*
|
|
2807
2825
|
* Flips `widgetConditionsSettled` when the initial round is done —
|
|
2808
2826
|
* synchronously when there is nothing to evaluate, so a dashboard
|
|
2809
2827
|
* without conditional widgets never waits.
|
|
2810
2828
|
*
|
|
2811
2829
|
* @return {Promise<void>}
|
|
2830
|
+
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2812
2831
|
*/
|
|
2813
2832
|
async evaluateWidgetConditions() {
|
|
2814
2833
|
const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
|
|
@@ -2830,12 +2849,17 @@ export default {
|
|
|
2830
2849
|
}
|
|
2831
2850
|
await Promise.all(conditional.map(async (def) => {
|
|
2832
2851
|
const cond = this.widgetDisplayConfig(def).visibleWhen
|
|
2833
|
-
let outcome
|
|
2852
|
+
let outcome
|
|
2834
2853
|
try {
|
|
2835
2854
|
const value = await readVisibleWhenValue(cond)
|
|
2836
2855
|
outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value }
|
|
2837
|
-
} catch {
|
|
2838
|
-
//
|
|
2856
|
+
} catch (error) {
|
|
2857
|
+
// Fail-safe: not met. But the failure is kept, and said
|
|
2858
|
+
// out loud: a failed count and "nothing to report" look
|
|
2859
|
+
// the same on screen.
|
|
2860
|
+
outcome = { met: false, value: null, failed: true, reason: typeof error?.message === 'string' ? error.message : String(error ?? '') }
|
|
2861
|
+
// eslint-disable-next-line no-console
|
|
2862
|
+
console.warn(`[CnDashboardPage] visibleWhen of widget "${def.id}" could not be evaluated:`, error?.message || error)
|
|
2839
2863
|
}
|
|
2840
2864
|
// A newer run owns the map now — a stale verdict (possibly for
|
|
2841
2865
|
// a def that no longer exists) must not overwrite its writes.
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: EUPL-1.2 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->
|
|
3
|
+
<template>
|
|
4
|
+
<Teleport to="body" :disabled="!teleport">
|
|
5
|
+
<div
|
|
6
|
+
ref="panel"
|
|
7
|
+
class="cn-column-filter"
|
|
8
|
+
role="dialog"
|
|
9
|
+
:aria-labelledby="headingId"
|
|
10
|
+
:style="panelStyle"
|
|
11
|
+
data-testid="cn-column-filter"
|
|
12
|
+
@keydown.esc.stop.prevent="close"
|
|
13
|
+
@keydown.enter="onEnter">
|
|
14
|
+
<p :id="headingId" class="cn-column-filter__heading">
|
|
15
|
+
{{ headingText }}
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<!-- enum and reference: a checkbox list of real inputs -->
|
|
19
|
+
<template v-if="def.kind === 'enum' || def.kind === 'reference'">
|
|
20
|
+
<label v-if="def.kind === 'reference'" class="cn-column-filter__field">
|
|
21
|
+
<span class="cn-column-filter__label">{{ searchLabel }}</span>
|
|
22
|
+
<input
|
|
23
|
+
v-model="query"
|
|
24
|
+
class="cn-column-filter__input"
|
|
25
|
+
type="search"
|
|
26
|
+
data-testid="cn-column-filter-search"
|
|
27
|
+
@input="onSearchInput">
|
|
28
|
+
</label>
|
|
29
|
+
<p v-if="def.kind === 'reference' && referenceLoading" class="cn-column-filter__hint" role="status">
|
|
30
|
+
{{ loadingLabel }}
|
|
31
|
+
</p>
|
|
32
|
+
<p v-else-if="def.kind === 'reference' && referenceError" class="cn-column-filter__hint" role="status">
|
|
33
|
+
{{ referenceError }}
|
|
34
|
+
</p>
|
|
35
|
+
<fieldset class="cn-column-filter__options">
|
|
36
|
+
<legend class="cn-column-filter__sr-only">
|
|
37
|
+
{{ headingText }}
|
|
38
|
+
</legend>
|
|
39
|
+
<label
|
|
40
|
+
v-for="option in listOptions"
|
|
41
|
+
:key="option.value"
|
|
42
|
+
class="cn-column-filter__option">
|
|
43
|
+
<input
|
|
44
|
+
type="checkbox"
|
|
45
|
+
:value="option.value"
|
|
46
|
+
:checked="draft.values.includes(option.value)"
|
|
47
|
+
data-testid="cn-column-filter-option"
|
|
48
|
+
@change="toggleValue(option.value)">
|
|
49
|
+
<span>{{ option.label }}</span>
|
|
50
|
+
</label>
|
|
51
|
+
<p v-if="listOptions.length === 0 && !referenceLoading" class="cn-column-filter__hint">
|
|
52
|
+
{{ noOptionsLabel }}
|
|
53
|
+
</p>
|
|
54
|
+
</fieldset>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<!-- boolean: yes, no, any -->
|
|
58
|
+
<fieldset v-else-if="def.kind === 'boolean'" class="cn-column-filter__options">
|
|
59
|
+
<legend class="cn-column-filter__sr-only">
|
|
60
|
+
{{ headingText }}
|
|
61
|
+
</legend>
|
|
62
|
+
<label v-for="option in booleanOptions" :key="option.value" class="cn-column-filter__option">
|
|
63
|
+
<input
|
|
64
|
+
type="radio"
|
|
65
|
+
:name="headingId + '-bool'"
|
|
66
|
+
:value="option.value"
|
|
67
|
+
:checked="draft.value === option.value"
|
|
68
|
+
data-testid="cn-column-filter-option"
|
|
69
|
+
@change="draft.value = option.value">
|
|
70
|
+
<span>{{ option.label }}</span>
|
|
71
|
+
</label>
|
|
72
|
+
</fieldset>
|
|
73
|
+
|
|
74
|
+
<!-- number and date: from and to -->
|
|
75
|
+
<div v-else-if="def.kind === 'number' || def.kind === 'date'" class="cn-column-filter__range">
|
|
76
|
+
<label class="cn-column-filter__field">
|
|
77
|
+
<span class="cn-column-filter__label">{{ fromLabel }}</span>
|
|
78
|
+
<input
|
|
79
|
+
v-model="draft.from"
|
|
80
|
+
class="cn-column-filter__input"
|
|
81
|
+
:type="def.kind === 'date' ? 'date' : 'number'"
|
|
82
|
+
data-testid="cn-column-filter-from">
|
|
83
|
+
</label>
|
|
84
|
+
<label class="cn-column-filter__field">
|
|
85
|
+
<span class="cn-column-filter__label">{{ toLabel }}</span>
|
|
86
|
+
<input
|
|
87
|
+
v-model="draft.to"
|
|
88
|
+
class="cn-column-filter__input"
|
|
89
|
+
:type="def.kind === 'date' ? 'date' : 'number'"
|
|
90
|
+
data-testid="cn-column-filter-to">
|
|
91
|
+
</label>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- string: equals -->
|
|
95
|
+
<label v-else class="cn-column-filter__field">
|
|
96
|
+
<span class="cn-column-filter__label">{{ equalsLabel }}</span>
|
|
97
|
+
<input
|
|
98
|
+
v-model="draft.value"
|
|
99
|
+
class="cn-column-filter__input"
|
|
100
|
+
type="text"
|
|
101
|
+
data-testid="cn-column-filter-text">
|
|
102
|
+
</label>
|
|
103
|
+
|
|
104
|
+
<div class="cn-column-filter__actions">
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
class="cn-column-filter__button"
|
|
108
|
+
data-testid="cn-column-filter-clear"
|
|
109
|
+
@click="clear">
|
|
110
|
+
{{ clearLabel }}
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
class="cn-column-filter__button cn-column-filter__button--primary"
|
|
115
|
+
data-testid="cn-column-filter-apply"
|
|
116
|
+
@click="apply">
|
|
117
|
+
{{ applyLabel }}
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</Teleport>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<script>
|
|
125
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
126
|
+
|
|
127
|
+
let popoverSeq = 0
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* CnColumnFilterPopover — the small panel a table header's filter button opens.
|
|
131
|
+
*
|
|
132
|
+
* Renders the control that fits the column: a checkbox list for an enum, yes,
|
|
133
|
+
* no or any for a boolean, from and to for a number or a date, a searchable
|
|
134
|
+
* list of referenced objects for a reference, and an equals box for text.
|
|
135
|
+
* Real inputs throughout, so every control works from the keyboard. Escape
|
|
136
|
+
* closes it and the host returns focus to the filter button.
|
|
137
|
+
*
|
|
138
|
+
* Internal to CnDataTable. It emits a state object; CnDataTable turns that
|
|
139
|
+
* into query parameters with `columnFilterParams`.
|
|
140
|
+
*
|
|
141
|
+
* @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
|
|
142
|
+
*/
|
|
143
|
+
export default {
|
|
144
|
+
name: 'CnColumnFilterPopover',
|
|
145
|
+
|
|
146
|
+
props: {
|
|
147
|
+
/** The filter definition from `columnFilterDef`. */
|
|
148
|
+
def: {
|
|
149
|
+
type: Object,
|
|
150
|
+
required: true,
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/** The column's current state from `columnFilterState`. */
|
|
154
|
+
state: {
|
|
155
|
+
type: Object,
|
|
156
|
+
default: () => ({}),
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/** The translated column label. */
|
|
160
|
+
label: {
|
|
161
|
+
type: String,
|
|
162
|
+
default: '',
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/** The filter button's bounding rect, to place the panel under it. */
|
|
166
|
+
anchorRect: {
|
|
167
|
+
type: Object,
|
|
168
|
+
default: null,
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/** Register slug a reference column's objects live in, when the column names none. */
|
|
172
|
+
register: {
|
|
173
|
+
type: String,
|
|
174
|
+
default: '',
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/** Render into `<body>`, so a scrolling table cannot clip the panel. */
|
|
178
|
+
teleport: {
|
|
179
|
+
type: Boolean,
|
|
180
|
+
default: true,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
emits: ['apply', 'close'],
|
|
185
|
+
|
|
186
|
+
data() {
|
|
187
|
+
popoverSeq += 1
|
|
188
|
+
const s = this.state || {}
|
|
189
|
+
return {
|
|
190
|
+
headingId: 'cn-column-filter-' + popoverSeq,
|
|
191
|
+
draft: {
|
|
192
|
+
values: Array.isArray(s.values) ? [...s.values] : [],
|
|
193
|
+
value: s.value !== undefined && s.value !== null ? String(s.value) : '',
|
|
194
|
+
from: s.from || '',
|
|
195
|
+
to: s.to || '',
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
query: '',
|
|
199
|
+
referenceOptions: [],
|
|
200
|
+
referenceLabels: {},
|
|
201
|
+
referenceLoading: false,
|
|
202
|
+
referenceError: '',
|
|
203
|
+
searchTimer: null,
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
computed: {
|
|
208
|
+
headingText() {
|
|
209
|
+
return t('nextcloud-vue', 'Filter {column}', { column: this.label })
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The options a list filter shows. For a reference, the search results
|
|
214
|
+
* plus any picked object that is not among them, so a selection never
|
|
215
|
+
* disappears from view.
|
|
216
|
+
*
|
|
217
|
+
* @return {Array<{value: string, label: string}>}
|
|
218
|
+
*/
|
|
219
|
+
listOptions() {
|
|
220
|
+
if (this.def.kind !== 'reference') {
|
|
221
|
+
return this.def.options || []
|
|
222
|
+
}
|
|
223
|
+
const shown = new Set(this.referenceOptions.map((o) => o.value))
|
|
224
|
+
const picked = this.draft.values
|
|
225
|
+
.filter((v) => !shown.has(v))
|
|
226
|
+
.map((v) => ({ value: v, label: this.referenceLabels[v] || v }))
|
|
227
|
+
return [...picked, ...this.referenceOptions]
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
booleanOptions() {
|
|
231
|
+
return [
|
|
232
|
+
{ value: '', label: t('nextcloud-vue', 'Any') },
|
|
233
|
+
{ value: 'true', label: t('nextcloud-vue', 'Yes') },
|
|
234
|
+
{ value: 'false', label: t('nextcloud-vue', 'No') },
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
panelStyle() {
|
|
239
|
+
if (!this.teleport || !this.anchorRect) {
|
|
240
|
+
return {}
|
|
241
|
+
}
|
|
242
|
+
const width = 280
|
|
243
|
+
const viewport = typeof window !== 'undefined' ? window.innerWidth : 1024
|
|
244
|
+
const left = Math.max(8, Math.min(this.anchorRect.left, viewport - width - 8))
|
|
245
|
+
return { position: 'fixed', top: `${this.anchorRect.bottom + 4}px`, left: `${left}px`, width: `${width}px` }
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
searchLabel() {
|
|
249
|
+
return t('nextcloud-vue', 'Search')
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
loadingLabel() {
|
|
253
|
+
return t('nextcloud-vue', 'Loading…')
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
noOptionsLabel() {
|
|
257
|
+
return t('nextcloud-vue', 'Nothing to choose from here.')
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
fromLabel() {
|
|
261
|
+
return t('nextcloud-vue', 'From')
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
toLabel() {
|
|
265
|
+
return t('nextcloud-vue', 'To')
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
equalsLabel() {
|
|
269
|
+
return t('nextcloud-vue', 'Equals')
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
clearLabel() {
|
|
273
|
+
return t('nextcloud-vue', 'Clear')
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
applyLabel() {
|
|
277
|
+
return t('nextcloud-vue', 'Apply')
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
mounted() {
|
|
282
|
+
document.addEventListener('mousedown', this.onOutsideMouseDown, true)
|
|
283
|
+
if (this.def.kind === 'reference') {
|
|
284
|
+
this.loadReferenceOptions('')
|
|
285
|
+
}
|
|
286
|
+
this.$nextTick(() => {
|
|
287
|
+
const first = this.$refs.panel && this.$refs.panel.querySelector('input, button')
|
|
288
|
+
if (first && typeof first.focus === 'function') {
|
|
289
|
+
first.focus()
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
beforeUnmount() {
|
|
295
|
+
document.removeEventListener('mousedown', this.onOutsideMouseDown, true)
|
|
296
|
+
clearTimeout(this.searchTimer)
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
methods: {
|
|
300
|
+
toggleValue(value) {
|
|
301
|
+
this.draft.values = this.draft.values.includes(value)
|
|
302
|
+
? this.draft.values.filter((v) => v !== value)
|
|
303
|
+
: [...this.draft.values, value]
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* The state this panel applies, in the shape `columnFilterParams` reads.
|
|
308
|
+
*
|
|
309
|
+
* @return {object}
|
|
310
|
+
*/
|
|
311
|
+
draftState() {
|
|
312
|
+
if (this.def.kind === 'enum' || this.def.kind === 'reference') {
|
|
313
|
+
return { values: [...this.draft.values] }
|
|
314
|
+
}
|
|
315
|
+
if (this.def.kind === 'number' || this.def.kind === 'date') {
|
|
316
|
+
return { from: this.draft.from, to: this.draft.to }
|
|
317
|
+
}
|
|
318
|
+
return { value: this.draft.value }
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
apply() {
|
|
322
|
+
/**
|
|
323
|
+
* @event apply The person applied the filter.
|
|
324
|
+
* @type {object} The column filter state.
|
|
325
|
+
*/
|
|
326
|
+
this.$emit('apply', this.draftState())
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
clear() {
|
|
330
|
+
this.$emit('apply', this.def.kind === 'enum' || this.def.kind === 'reference'
|
|
331
|
+
? { values: [] }
|
|
332
|
+
: (this.def.kind === 'number' || this.def.kind === 'date') ? { from: '', to: '' } : { value: '' })
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
close() {
|
|
336
|
+
/**
|
|
337
|
+
* @event close The panel should close without applying.
|
|
338
|
+
*/
|
|
339
|
+
this.$emit('close')
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
onEnter(event) {
|
|
343
|
+
// Enter in a text, number or date box applies, like a form submit.
|
|
344
|
+
// On a checkbox or a button it keeps its own meaning.
|
|
345
|
+
const target = event && event.target
|
|
346
|
+
if (target && target.tagName === 'INPUT' && ['text', 'number', 'date', 'search'].includes(target.type)) {
|
|
347
|
+
event.preventDefault()
|
|
348
|
+
this.apply()
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
onOutsideMouseDown(event) {
|
|
353
|
+
const panel = this.$refs.panel
|
|
354
|
+
if (panel && event && event.target && !panel.contains(event.target)) {
|
|
355
|
+
this.close()
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
onSearchInput() {
|
|
360
|
+
clearTimeout(this.searchTimer)
|
|
361
|
+
this.searchTimer = setTimeout(() => this.loadReferenceOptions(this.query), 250)
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Search the referenced objects. The value is the object's uuid,
|
|
366
|
+
* because that is what the server filters a reference on.
|
|
367
|
+
*
|
|
368
|
+
* @param {string} search The search term.
|
|
369
|
+
* @return {Promise<void>}
|
|
370
|
+
*/
|
|
371
|
+
async loadReferenceOptions(search) {
|
|
372
|
+
const ref = this.def.reference || {}
|
|
373
|
+
const register = ref.register || this.register
|
|
374
|
+
if (!register || !ref.schema) {
|
|
375
|
+
this.referenceError = t('nextcloud-vue', 'This column cannot list its objects.')
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
this.referenceLoading = true
|
|
379
|
+
this.referenceError = ''
|
|
380
|
+
try {
|
|
381
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
382
|
+
import('@nextcloud/axios'),
|
|
383
|
+
import('@nextcloud/router'),
|
|
384
|
+
])
|
|
385
|
+
const url = generateUrl('/apps/openregister/api/objects/{register}/{schema}', {
|
|
386
|
+
register,
|
|
387
|
+
schema: String(ref.schema),
|
|
388
|
+
})
|
|
389
|
+
const params = { _limit: 50 }
|
|
390
|
+
if (search) {
|
|
391
|
+
params._search = search
|
|
392
|
+
}
|
|
393
|
+
const response = await axios.get(url, { params })
|
|
394
|
+
const rows = (response && response.data && (response.data.results || response.data)) || []
|
|
395
|
+
const list = Array.isArray(rows) ? rows : []
|
|
396
|
+
const options = list
|
|
397
|
+
.map((row) => {
|
|
398
|
+
const self = (row && row['@self']) || {}
|
|
399
|
+
const value = String(self.id || self.uuid || row.id || row.uuid || '')
|
|
400
|
+
const label = String((ref.labelField && row[ref.labelField]) || row.name || row.title || self.name || value)
|
|
401
|
+
return { value, label }
|
|
402
|
+
})
|
|
403
|
+
.filter((o) => o.value !== '')
|
|
404
|
+
this.referenceOptions = options
|
|
405
|
+
this.referenceLabels = { ...this.referenceLabels, ...Object.fromEntries(options.map((o) => [o.value, o.label])) }
|
|
406
|
+
} catch {
|
|
407
|
+
this.referenceOptions = []
|
|
408
|
+
this.referenceError = t('nextcloud-vue', 'The list could not load.')
|
|
409
|
+
} finally {
|
|
410
|
+
this.referenceLoading = false
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
</script>
|
|
416
|
+
|
|
417
|
+
<style scoped>
|
|
418
|
+
.cn-column-filter {
|
|
419
|
+
z-index: 10000;
|
|
420
|
+
display: flex;
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
gap: 8px;
|
|
423
|
+
box-sizing: border-box;
|
|
424
|
+
max-height: 60vh;
|
|
425
|
+
overflow-y: auto;
|
|
426
|
+
padding: 12px;
|
|
427
|
+
background: var(--color-main-background);
|
|
428
|
+
color: var(--color-main-text);
|
|
429
|
+
border: 1px solid var(--color-border-dark);
|
|
430
|
+
border-radius: var(--border-radius-large, 8px);
|
|
431
|
+
box-shadow: 0 2px 8px var(--color-box-shadow);
|
|
432
|
+
font-weight: normal;
|
|
433
|
+
text-align: start;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.cn-column-filter__heading {
|
|
437
|
+
margin: 0;
|
|
438
|
+
font-weight: bold;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.cn-column-filter__options {
|
|
442
|
+
display: flex;
|
|
443
|
+
flex-direction: column;
|
|
444
|
+
gap: 4px;
|
|
445
|
+
margin: 0;
|
|
446
|
+
padding: 0;
|
|
447
|
+
border: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.cn-column-filter__option {
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
gap: 8px;
|
|
454
|
+
min-height: 32px;
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.cn-column-filter__range {
|
|
459
|
+
display: flex;
|
|
460
|
+
gap: 8px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.cn-column-filter__field {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex: 1 1 0;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
gap: 4px;
|
|
468
|
+
min-width: 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.cn-column-filter__label {
|
|
472
|
+
font-size: var(--font-size-small, 13px);
|
|
473
|
+
color: var(--color-text-maxcontrast);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.cn-column-filter__input {
|
|
477
|
+
width: 100%;
|
|
478
|
+
box-sizing: border-box;
|
|
479
|
+
margin: 0;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.cn-column-filter__hint {
|
|
483
|
+
margin: 0;
|
|
484
|
+
color: var(--color-text-maxcontrast);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
.cn-column-filter__actions {
|
|
488
|
+
display: flex;
|
|
489
|
+
justify-content: flex-end;
|
|
490
|
+
gap: 8px;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.cn-column-filter__button {
|
|
494
|
+
min-height: 34px;
|
|
495
|
+
padding: 0 12px;
|
|
496
|
+
border: 1px solid var(--color-border-dark);
|
|
497
|
+
border-radius: var(--border-radius-element, 8px);
|
|
498
|
+
background: var(--color-main-background);
|
|
499
|
+
color: var(--color-main-text);
|
|
500
|
+
cursor: pointer;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.cn-column-filter__button--primary {
|
|
504
|
+
border-color: var(--color-primary-element);
|
|
505
|
+
background: var(--color-primary-element);
|
|
506
|
+
color: var(--color-primary-element-text);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.cn-column-filter__button:focus-visible,
|
|
510
|
+
.cn-column-filter__input:focus-visible,
|
|
511
|
+
.cn-column-filter__option input:focus-visible {
|
|
512
|
+
outline: 2px solid var(--color-primary-element);
|
|
513
|
+
outline-offset: 2px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.cn-column-filter__sr-only {
|
|
517
|
+
position: absolute;
|
|
518
|
+
width: 1px;
|
|
519
|
+
height: 1px;
|
|
520
|
+
overflow: hidden;
|
|
521
|
+
clip-path: inset(50%);
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
}
|
|
524
|
+
</style>
|
|
@@ -186,7 +186,7 @@ export default {
|
|
|
186
186
|
| `includeColumns` | Array | `null` | Column keys to include (whitelist) in schema mode |
|
|
187
187
|
| `sortKey` | String | `null` | Currently active sort column key (controlled) |
|
|
188
188
|
| `sortOrder` | String | `'asc'` | Current sort direction: `'asc'` or `'desc'` |
|
|
189
|
-
| `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty |
|
|
189
|
+
| `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered |
|
|
190
190
|
| `selectedIds` | Array | `[]` | Array of selected row IDs (controlled) |
|
|
191
191
|
| `rowClass` | Function | `null` | `(row) => string` — CSS class(es) applied to each `<tr>` |
|
|
192
192
|
| `rowClickToView` | Boolean | `false` | Emit `row-click` on a row-body click even while `selectable` (selection then via the checkbox column only) |
|
|
@@ -201,6 +201,9 @@ export default {
|
|
|
201
201
|
| `hideHeader` | Boolean | `false` | Hide the column-header row (`<thead>`) — for compact dashboard list widgets that want a plain bordered-row list without column labels |
|
|
202
202
|
| `fixedLayout` | Boolean | `false` | Switch to `table-layout: fixed` so each column's `width` is authoritative rather than a hint the browser may override from cell content. Long unbreakable values (a PHP FQCN, a UUID) then wrap inside their cell instead of widening the column or painting past it, and no single unsized column soaks up all remaining width. Size every column when using it — percentages summing to 100 are easiest to reason about |
|
|
203
203
|
| `fillHeight` | Boolean | `false` | Fill the parent's height (a flex-column card / widget content area) so an optional `#footer` is pushed to the bottom instead of floating under a short list; the footer stays pinned via its sticky rule when the list overflows. No-op outside a height-constrained parent — opt-in |
|
|
204
|
+
| `filterable` | Boolean | `false` | Show a filter button in every header whose column can filter: enum, boolean, text, number, date and reference columns backed by a schema property. A column opts out with `filterable: false`. Applying a filter emits `column-filter`; the host owns the state and passes it back as `activeFilters`. `CnIndexPage` turns it on by default. |
|
|
205
|
+
| `activeFilters` | Object | `{}` | The active filter map, `{ paramKey: values[] }`: the same map the facet sidebar writes, so a header filter and a sidebar filter on one field show the same state. |
|
|
206
|
+
| `filterRegister` | String | `''` | Register slug a reference column's filter searches in when the column names none. |
|
|
204
207
|
|
|
205
208
|
## Slots
|
|
206
209
|
|