@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.
- 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 +271 -32
- 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/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.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 +216 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
- 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/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/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/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/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 +43 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +43 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +449 -0
- 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 +9369 -5193
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +449 -0
- package/l10n/en.json +43 -0
- package/l10n/nl.json +43 -0
- package/package.json +1 -1
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +271 -32
- package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +3 -0
- package/src/components/CnDataTable/CnDataTable.vue +260 -4
- 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/CnObjectListWidget/CnObjectListWidget.vue +39 -1
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- 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/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 +81 -0
- package/src/index.js +2 -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
|
@@ -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>
|
|
@@ -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
|
|