@conduction/nextcloud-vue 0.1.0-beta.1
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/nextcloud-vue.cjs.js +10710 -0
- package/dist/nextcloud-vue.cjs.js.map +1 -0
- package/dist/nextcloud-vue.css +803 -0
- package/dist/nextcloud-vue.esm.js +10665 -0
- package/dist/nextcloud-vue.esm.js.map +1 -0
- package/package.json +63 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -0
- package/src/components/CnCardGrid/index.js +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -0
- package/src/components/CnCellRenderer/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -0
- package/src/components/CnConfigurationCard/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +354 -0
- package/src/components/CnDataTable/index.js +1 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +88 -0
- package/src/components/CnDetailViewLayout/index.js +1 -0
- package/src/components/CnEmptyState/CnEmptyState.vue +78 -0
- package/src/components/CnEmptyState/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +223 -0
- package/src/components/CnFacetSidebar/index.js +1 -0
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -0
- package/src/components/CnFilterBar/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +682 -0
- package/src/components/CnIndexPage/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -0
- package/src/components/CnKpiGrid/index.js +1 -0
- package/src/components/CnListViewLayout/CnListViewLayout.vue +80 -0
- package/src/components/CnListViewLayout/index.js +1 -0
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -0
- package/src/components/CnMassActionBar/index.js +1 -0
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -0
- package/src/components/CnMassCopyDialog/index.js +1 -0
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -0
- package/src/components/CnMassDeleteDialog/index.js +1 -0
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -0
- package/src/components/CnMassExportDialog/index.js +1 -0
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -0
- package/src/components/CnMassImportDialog/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -0
- package/src/components/CnObjectCard/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +252 -0
- package/src/components/CnPagination/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +73 -0
- package/src/components/CnRowActions/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -0
- package/src/components/CnSettingsCard/index.js +1 -0
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -0
- package/src/components/CnSettingsSection/index.js +1 -0
- package/src/components/CnStatsBlock/CnStatsBlock.vue +366 -0
- package/src/components/CnStatsBlock/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -0
- package/src/components/CnStatusBadge/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -0
- package/src/components/CnVersionInfoCard/index.js +1 -0
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +77 -0
- package/src/components/CnViewModeToggle/index.js +1 -0
- package/src/components/index.js +25 -0
- package/src/composables/index.js +3 -0
- package/src/composables/useDetailView.js +132 -0
- package/src/composables/useListView.js +153 -0
- package/src/composables/useSubResource.js +142 -0
- package/src/css/badge.css +51 -0
- package/src/css/card.css +128 -0
- package/src/css/detail.css +68 -0
- package/src/css/index.css +8 -0
- package/src/css/layout.css +90 -0
- package/src/css/pagination.css +72 -0
- package/src/css/table.css +143 -0
- package/src/css/utilities.css +46 -0
- package/src/index.js +50 -0
- package/src/store/createSubResourcePlugin.js +135 -0
- package/src/store/index.js +3 -0
- package/src/store/plugins/auditTrails.js +17 -0
- package/src/store/plugins/files.js +186 -0
- package/src/store/plugins/index.js +4 -0
- package/src/store/plugins/lifecycle.js +180 -0
- package/src/store/plugins/relations.js +68 -0
- package/src/store/useObjectStore.js +625 -0
- package/src/types/auditTrail.d.ts +32 -0
- package/src/types/file.d.ts +23 -0
- package/src/types/index.d.ts +35 -0
- package/src/types/notification.d.ts +36 -0
- package/src/types/object.d.ts +40 -0
- package/src/types/organisation.d.ts +41 -0
- package/src/types/register.d.ts +25 -0
- package/src/types/schema.d.ts +39 -0
- package/src/types/shared.d.ts +79 -0
- package/src/types/source.d.ts +14 -0
- package/src/types/task.d.ts +31 -0
- package/src/utils/errors.js +96 -0
- package/src/utils/headers.js +44 -0
- package/src/utils/index.js +3 -0
- package/src/utils/schema.js +287 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="cn-object-card"
|
|
4
|
+
:class="{ 'cn-object-card--selected': selected }"
|
|
5
|
+
@click="$emit('click', object)">
|
|
6
|
+
<!-- Selection checkbox -->
|
|
7
|
+
<div v-if="selectable" class="cn-object-card__checkbox" @click.stop>
|
|
8
|
+
<NcCheckboxRadioSwitch
|
|
9
|
+
:checked="selected"
|
|
10
|
+
@update:checked="$emit('select', object)" />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Card content -->
|
|
14
|
+
<div class="cn-object-card__content">
|
|
15
|
+
<!-- Header: image + title -->
|
|
16
|
+
<div class="cn-object-card__header">
|
|
17
|
+
<img
|
|
18
|
+
v-if="imageUrl"
|
|
19
|
+
:src="imageUrl"
|
|
20
|
+
:alt="title"
|
|
21
|
+
class="cn-object-card__image">
|
|
22
|
+
|
|
23
|
+
<div class="cn-object-card__title-area">
|
|
24
|
+
<h3 class="cn-object-card__title">{{ title }}</h3>
|
|
25
|
+
<p v-if="description" class="cn-object-card__description">
|
|
26
|
+
{{ truncatedDescription }}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Badges slot -->
|
|
32
|
+
<div v-if="$scopedSlots.badges" class="cn-object-card__badges">
|
|
33
|
+
<slot name="badges" :object="object" />
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Metadata: visible properties as label:value pairs -->
|
|
37
|
+
<div v-if="metadataFields.length > 0" class="cn-object-card__metadata">
|
|
38
|
+
<slot name="metadata" :object="object" :fields="metadataFields">
|
|
39
|
+
<div
|
|
40
|
+
v-for="field in metadataFields"
|
|
41
|
+
:key="field.key"
|
|
42
|
+
class="cn-object-card__meta-item">
|
|
43
|
+
<span class="cn-object-card__meta-label">{{ field.label }}</span>
|
|
44
|
+
<CnCellRenderer
|
|
45
|
+
:value="field.value"
|
|
46
|
+
:property="field.property"
|
|
47
|
+
:truncate="60" />
|
|
48
|
+
</div>
|
|
49
|
+
</slot>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Actions slot -->
|
|
54
|
+
<div v-if="$scopedSlots.actions" class="cn-object-card__actions" @click.stop>
|
|
55
|
+
<slot name="actions" :object="object" />
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<script>
|
|
61
|
+
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
|
|
62
|
+
import { CnCellRenderer } from '../CnCellRenderer/index.js'
|
|
63
|
+
import { formatValue } from '../../utils/schema.js'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* CnObjectCard — Schema-configuration-driven card for object display.
|
|
67
|
+
*
|
|
68
|
+
* Uses `schema.configuration` to determine which fields map to the card title,
|
|
69
|
+
* description, and image. Remaining visible properties are shown as metadata.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* <CnObjectCard :object="publication" :schema="pubSchema">
|
|
73
|
+
* <template #actions="{ object }">
|
|
74
|
+
* <NcActions><NcActionButton @click="edit(object)">Edit</NcActionButton></NcActions>
|
|
75
|
+
* </template>
|
|
76
|
+
* </CnObjectCard>
|
|
77
|
+
*/
|
|
78
|
+
export default {
|
|
79
|
+
name: 'CnObjectCard',
|
|
80
|
+
|
|
81
|
+
components: {
|
|
82
|
+
NcCheckboxRadioSwitch,
|
|
83
|
+
CnCellRenderer,
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
props: {
|
|
87
|
+
/** The object data */
|
|
88
|
+
object: {
|
|
89
|
+
type: Object,
|
|
90
|
+
required: true,
|
|
91
|
+
},
|
|
92
|
+
/** Schema definition with properties and configuration */
|
|
93
|
+
schema: {
|
|
94
|
+
type: Object,
|
|
95
|
+
required: true,
|
|
96
|
+
},
|
|
97
|
+
/** Whether this card is selected */
|
|
98
|
+
selected: {
|
|
99
|
+
type: Boolean,
|
|
100
|
+
default: false,
|
|
101
|
+
},
|
|
102
|
+
/** Whether to show selection checkbox */
|
|
103
|
+
selectable: {
|
|
104
|
+
type: Boolean,
|
|
105
|
+
default: false,
|
|
106
|
+
},
|
|
107
|
+
/** Maximum number of metadata fields to show */
|
|
108
|
+
maxMetadata: {
|
|
109
|
+
type: Number,
|
|
110
|
+
default: 4,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
computed: {
|
|
115
|
+
config() {
|
|
116
|
+
return this.schema?.configuration || {}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
title() {
|
|
120
|
+
const field = this.config.objectNameField
|
|
121
|
+
if (field && this.object[field]) {
|
|
122
|
+
return String(this.object[field])
|
|
123
|
+
}
|
|
124
|
+
return this.object.title || this.object.name || this.object.id || '—'
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
description() {
|
|
128
|
+
const field = this.config.objectDescriptionField
|
|
129
|
+
if (field && this.object[field]) {
|
|
130
|
+
return String(this.object[field])
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
truncatedDescription() {
|
|
136
|
+
if (!this.description) return null
|
|
137
|
+
if (this.description.length > 120) {
|
|
138
|
+
return this.description.substring(0, 120) + '...'
|
|
139
|
+
}
|
|
140
|
+
return this.description
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
imageUrl() {
|
|
144
|
+
const field = this.config.objectImageField
|
|
145
|
+
if (field && this.object[field]) {
|
|
146
|
+
return this.object[field]
|
|
147
|
+
}
|
|
148
|
+
return null
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/** Fields excluded from metadata (already shown as title/desc/image) */
|
|
152
|
+
configFields() {
|
|
153
|
+
return [
|
|
154
|
+
this.config.objectNameField,
|
|
155
|
+
this.config.objectDescriptionField,
|
|
156
|
+
this.config.objectSummaryField,
|
|
157
|
+
this.config.objectImageField,
|
|
158
|
+
].filter(Boolean)
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
/** Remaining visible properties for the metadata section */
|
|
162
|
+
metadataFields() {
|
|
163
|
+
if (!this.schema?.properties) return []
|
|
164
|
+
|
|
165
|
+
return Object.entries(this.schema.properties)
|
|
166
|
+
.filter(([key, prop]) => {
|
|
167
|
+
if (this.configFields.includes(key)) return false
|
|
168
|
+
if (prop.visible === false) return false
|
|
169
|
+
if (prop.type === 'object') return false
|
|
170
|
+
if (prop.format === 'markdown') return false
|
|
171
|
+
return true
|
|
172
|
+
})
|
|
173
|
+
.sort(([, a], [, b]) => {
|
|
174
|
+
const orderA = typeof a.order === 'number' ? a.order : Infinity
|
|
175
|
+
const orderB = typeof b.order === 'number' ? b.order : Infinity
|
|
176
|
+
return orderA - orderB
|
|
177
|
+
})
|
|
178
|
+
.slice(0, this.maxMetadata)
|
|
179
|
+
.map(([key, prop]) => ({
|
|
180
|
+
key,
|
|
181
|
+
label: prop.title || key,
|
|
182
|
+
value: this.object[key],
|
|
183
|
+
property: prop,
|
|
184
|
+
}))
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
methods: {
|
|
189
|
+
formatValue,
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
</script>
|
|
193
|
+
|
|
194
|
+
<style scoped>
|
|
195
|
+
.cn-object-card {
|
|
196
|
+
display: flex;
|
|
197
|
+
gap: 12px;
|
|
198
|
+
padding: 16px;
|
|
199
|
+
background: var(--color-main-background);
|
|
200
|
+
border: 1px solid var(--color-border);
|
|
201
|
+
border-radius: var(--border-radius-large, 10px);
|
|
202
|
+
cursor: pointer;
|
|
203
|
+
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.cn-object-card:hover {
|
|
207
|
+
border-color: var(--color-primary-element);
|
|
208
|
+
box-shadow: 0 2px 8px var(--color-box-shadow);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.cn-object-card--selected {
|
|
212
|
+
border-color: var(--color-primary-element);
|
|
213
|
+
background: var(--color-primary-element-light);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.cn-object-card__checkbox {
|
|
217
|
+
flex-shrink: 0;
|
|
218
|
+
padding-top: 2px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cn-object-card__content {
|
|
222
|
+
flex: 1;
|
|
223
|
+
min-width: 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cn-object-card__header {
|
|
227
|
+
display: flex;
|
|
228
|
+
gap: 12px;
|
|
229
|
+
align-items: flex-start;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.cn-object-card__image {
|
|
233
|
+
width: 48px;
|
|
234
|
+
height: 48px;
|
|
235
|
+
border-radius: var(--border-radius);
|
|
236
|
+
object-fit: cover;
|
|
237
|
+
flex-shrink: 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.cn-object-card__title-area {
|
|
241
|
+
flex: 1;
|
|
242
|
+
min-width: 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.cn-object-card__title {
|
|
246
|
+
margin: 0;
|
|
247
|
+
font-size: 16px;
|
|
248
|
+
font-weight: 600;
|
|
249
|
+
line-height: 1.3;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
text-overflow: ellipsis;
|
|
252
|
+
white-space: nowrap;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.cn-object-card__description {
|
|
256
|
+
margin: 4px 0 0;
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
color: var(--color-text-maxcontrast);
|
|
259
|
+
line-height: 1.4;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.cn-object-card__badges {
|
|
263
|
+
margin-top: 8px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.cn-object-card__metadata {
|
|
267
|
+
display: grid;
|
|
268
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
269
|
+
gap: 8px;
|
|
270
|
+
margin-top: 12px;
|
|
271
|
+
padding-top: 12px;
|
|
272
|
+
border-top: 1px solid var(--color-border);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.cn-object-card__meta-item {
|
|
276
|
+
display: flex;
|
|
277
|
+
flex-direction: column;
|
|
278
|
+
gap: 2px;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.cn-object-card__meta-label {
|
|
282
|
+
font-size: 11px;
|
|
283
|
+
font-weight: 500;
|
|
284
|
+
color: var(--color-text-maxcontrast);
|
|
285
|
+
text-transform: uppercase;
|
|
286
|
+
letter-spacing: 0.3px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.cn-object-card__actions {
|
|
290
|
+
flex-shrink: 0;
|
|
291
|
+
}
|
|
292
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnObjectCard } from './CnObjectCard.vue'
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="totalPages > 1 || totalItems > minItemsToShow" class="cn-pagination">
|
|
3
|
+
<!-- Page info -->
|
|
4
|
+
<div class="cn-pagination__info">
|
|
5
|
+
<span class="cn-pagination__page-info">
|
|
6
|
+
{{ pageInfoText }}
|
|
7
|
+
</span>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<!-- Page navigation -->
|
|
11
|
+
<div v-if="totalPages > 1" class="cn-pagination__nav">
|
|
12
|
+
<NcButton
|
|
13
|
+
:disabled="currentPage === 1"
|
|
14
|
+
@click="changePage(1)">
|
|
15
|
+
{{ firstLabel }}
|
|
16
|
+
</NcButton>
|
|
17
|
+
|
|
18
|
+
<NcButton
|
|
19
|
+
:disabled="currentPage === 1"
|
|
20
|
+
@click="changePage(currentPage - 1)">
|
|
21
|
+
{{ previousLabel }}
|
|
22
|
+
</NcButton>
|
|
23
|
+
|
|
24
|
+
<div class="cn-pagination__numbers">
|
|
25
|
+
<template v-for="page in visiblePages">
|
|
26
|
+
<span v-if="page === '...'" :key="'ellipsis-' + page" class="cn-pagination__ellipsis">...</span>
|
|
27
|
+
<NcButton
|
|
28
|
+
v-else
|
|
29
|
+
:key="page"
|
|
30
|
+
:type="page === currentPage ? 'primary' : 'secondary'"
|
|
31
|
+
:disabled="page === currentPage"
|
|
32
|
+
@click="changePage(page)">
|
|
33
|
+
{{ page }}
|
|
34
|
+
</NcButton>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<NcButton
|
|
39
|
+
:disabled="currentPage === totalPages"
|
|
40
|
+
@click="changePage(currentPage + 1)">
|
|
41
|
+
{{ nextLabel }}
|
|
42
|
+
</NcButton>
|
|
43
|
+
|
|
44
|
+
<NcButton
|
|
45
|
+
:disabled="currentPage === totalPages"
|
|
46
|
+
@click="changePage(totalPages)">
|
|
47
|
+
{{ lastLabel }}
|
|
48
|
+
</NcButton>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- Page size selector -->
|
|
52
|
+
<div class="cn-pagination__page-size">
|
|
53
|
+
<label :for="pageSizeId">{{ itemsPerPageLabel }}</label>
|
|
54
|
+
<NcSelect
|
|
55
|
+
:input-id="pageSizeId"
|
|
56
|
+
class="cn-pagination__page-size-select"
|
|
57
|
+
:value="currentPageSizeOption"
|
|
58
|
+
:options="pageSizeOptions"
|
|
59
|
+
:clearable="false"
|
|
60
|
+
:input-label="itemsPerPageLabel"
|
|
61
|
+
@option:selected="changePageSize" />
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<script>
|
|
67
|
+
import { NcButton, NcSelect } from '@nextcloud/vue'
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* CnPagination — Full pagination with page numbers, navigation, and page size selector.
|
|
71
|
+
*
|
|
72
|
+
* Extracted from OpenRegister's PaginationComponent. Zero store dependencies.
|
|
73
|
+
* Supports First/Previous/Next/Last buttons, smart page number display with
|
|
74
|
+
* ellipsis, and configurable page size.
|
|
75
|
+
*
|
|
76
|
+
* NL Design tokens used:
|
|
77
|
+
* - Inherits from cn-pagination CSS class (see css/pagination.css)
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* <CnPagination
|
|
81
|
+
* :current-page="page"
|
|
82
|
+
* :total-pages="totalPages"
|
|
83
|
+
* :total-items="totalItems"
|
|
84
|
+
* :current-page-size="limit"
|
|
85
|
+
* @page-changed="onPageChange"
|
|
86
|
+
* @page-size-changed="onPageSizeChange" />
|
|
87
|
+
*/
|
|
88
|
+
export default {
|
|
89
|
+
name: 'CnPagination',
|
|
90
|
+
|
|
91
|
+
components: {
|
|
92
|
+
NcButton,
|
|
93
|
+
NcSelect,
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
props: {
|
|
97
|
+
/** Current page number (1-based) */
|
|
98
|
+
currentPage: {
|
|
99
|
+
type: Number,
|
|
100
|
+
default: 1,
|
|
101
|
+
},
|
|
102
|
+
/** Total number of pages */
|
|
103
|
+
totalPages: {
|
|
104
|
+
type: Number,
|
|
105
|
+
default: 1,
|
|
106
|
+
},
|
|
107
|
+
/** Total number of items across all pages */
|
|
108
|
+
totalItems: {
|
|
109
|
+
type: Number,
|
|
110
|
+
default: 0,
|
|
111
|
+
},
|
|
112
|
+
/** Current items per page */
|
|
113
|
+
currentPageSize: {
|
|
114
|
+
type: Number,
|
|
115
|
+
default: 20,
|
|
116
|
+
},
|
|
117
|
+
/** Available page size options */
|
|
118
|
+
pageSizeOptions: {
|
|
119
|
+
type: Array,
|
|
120
|
+
default: () => [
|
|
121
|
+
{ value: 10, label: '10' },
|
|
122
|
+
{ value: 20, label: '20' },
|
|
123
|
+
{ value: 50, label: '50' },
|
|
124
|
+
{ value: 100, label: '100' },
|
|
125
|
+
{ value: 250, label: '250' },
|
|
126
|
+
{ value: 500, label: '500' },
|
|
127
|
+
{ value: 1000, label: '1000' },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
/** Minimum items before pagination is shown */
|
|
131
|
+
minItemsToShow: {
|
|
132
|
+
type: Number,
|
|
133
|
+
default: 10,
|
|
134
|
+
},
|
|
135
|
+
/** Label for "First" button */
|
|
136
|
+
firstLabel: {
|
|
137
|
+
type: String,
|
|
138
|
+
default: 'First',
|
|
139
|
+
},
|
|
140
|
+
/** Label for "Previous" button */
|
|
141
|
+
previousLabel: {
|
|
142
|
+
type: String,
|
|
143
|
+
default: 'Previous',
|
|
144
|
+
},
|
|
145
|
+
/** Label for "Next" button */
|
|
146
|
+
nextLabel: {
|
|
147
|
+
type: String,
|
|
148
|
+
default: 'Next',
|
|
149
|
+
},
|
|
150
|
+
/** Label for "Last" button */
|
|
151
|
+
lastLabel: {
|
|
152
|
+
type: String,
|
|
153
|
+
default: 'Last',
|
|
154
|
+
},
|
|
155
|
+
/** Label for "Items per page:" */
|
|
156
|
+
itemsPerPageLabel: {
|
|
157
|
+
type: String,
|
|
158
|
+
default: 'Items per page:',
|
|
159
|
+
},
|
|
160
|
+
/**
|
|
161
|
+
* Page info format string. Use {current} and {total} as placeholders.
|
|
162
|
+
* @example "Page {current} of {total}"
|
|
163
|
+
*/
|
|
164
|
+
pageInfoFormat: {
|
|
165
|
+
type: String,
|
|
166
|
+
default: 'Page {current} of {total}',
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
computed: {
|
|
171
|
+
pageSizeId() {
|
|
172
|
+
return 'cn-page-size-' + this._uid
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
currentPageSizeOption() {
|
|
176
|
+
return this.pageSizeOptions.find(
|
|
177
|
+
(option) => option.value === this.currentPageSize,
|
|
178
|
+
) || this.pageSizeOptions[1]
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
pageInfoText() {
|
|
182
|
+
return this.pageInfoFormat
|
|
183
|
+
.replace('{current}', this.currentPage)
|
|
184
|
+
.replace('{total}', this.totalPages)
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Calculate visible page numbers with ellipsis for large page counts.
|
|
189
|
+
* Shows up to 7 page numbers at a time.
|
|
190
|
+
*/
|
|
191
|
+
visiblePages() {
|
|
192
|
+
const current = this.currentPage
|
|
193
|
+
const total = this.totalPages
|
|
194
|
+
const pages = []
|
|
195
|
+
|
|
196
|
+
if (total <= 7) {
|
|
197
|
+
for (let i = 1; i <= total; i++) {
|
|
198
|
+
pages.push(i)
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
pages.push(1)
|
|
202
|
+
|
|
203
|
+
if (current <= 4) {
|
|
204
|
+
for (let i = 2; i <= 5; i++) {
|
|
205
|
+
pages.push(i)
|
|
206
|
+
}
|
|
207
|
+
pages.push('...')
|
|
208
|
+
pages.push(total)
|
|
209
|
+
} else if (current >= total - 3) {
|
|
210
|
+
pages.push('...')
|
|
211
|
+
for (let i = total - 4; i <= total; i++) {
|
|
212
|
+
pages.push(i)
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
pages.push('...')
|
|
216
|
+
for (let i = current - 1; i <= current + 1; i++) {
|
|
217
|
+
pages.push(i)
|
|
218
|
+
}
|
|
219
|
+
pages.push('...')
|
|
220
|
+
pages.push(total)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return pages
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
methods: {
|
|
229
|
+
/**
|
|
230
|
+
* Navigate to a specific page.
|
|
231
|
+
* @param {number} page Target page number
|
|
232
|
+
*/
|
|
233
|
+
changePage(page) {
|
|
234
|
+
if (page !== this.currentPage && page >= 1 && page <= this.totalPages) {
|
|
235
|
+
/** @event page-changed Emitted when page changes. Payload: new page number. */
|
|
236
|
+
this.$emit('page-changed', page)
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Change the page size.
|
|
242
|
+
* @param {object} option Selected page size option { value, label }
|
|
243
|
+
*/
|
|
244
|
+
changePageSize(option) {
|
|
245
|
+
if (option.value !== this.currentPageSize) {
|
|
246
|
+
/** @event page-size-changed Emitted when page size changes. Payload: new page size. */
|
|
247
|
+
this.$emit('page-size-changed', option.value)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnPagination } from './CnPagination.vue'
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<NcActions :force-menu="actions.length > 3">
|
|
3
|
+
<NcActionButton
|
|
4
|
+
v-for="action in actions"
|
|
5
|
+
:key="action.label"
|
|
6
|
+
:disabled="action.disabled"
|
|
7
|
+
:class="{ 'cn-row-action--destructive': action.destructive }"
|
|
8
|
+
@click="onAction(action)">
|
|
9
|
+
<template v-if="action.icon" #icon>
|
|
10
|
+
<component :is="action.icon" :size="20" />
|
|
11
|
+
</template>
|
|
12
|
+
{{ action.label }}
|
|
13
|
+
</NcActionButton>
|
|
14
|
+
</NcActions>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
import { NcActions, NcActionButton } from '@nextcloud/vue'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* CnRowActions — Action menu wrapper for table rows and cards.
|
|
22
|
+
*
|
|
23
|
+
* Wraps NcActions + NcActionButton for consistent row/card action menus.
|
|
24
|
+
* Actions are defined as an array of objects with label, icon, handler, etc.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <CnRowActions
|
|
28
|
+
* :actions="[
|
|
29
|
+
* { label: 'Edit', icon: PencilIcon, handler: (row) => editRow(row) },
|
|
30
|
+
* { label: 'Delete', icon: TrashIcon, handler: (row) => deleteRow(row), destructive: true },
|
|
31
|
+
* ]"
|
|
32
|
+
* :row="row" />
|
|
33
|
+
*/
|
|
34
|
+
export default {
|
|
35
|
+
name: 'CnRowActions',
|
|
36
|
+
|
|
37
|
+
components: {
|
|
38
|
+
NcActions,
|
|
39
|
+
NcActionButton,
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
props: {
|
|
43
|
+
/**
|
|
44
|
+
* Action definitions.
|
|
45
|
+
* @type {Array<{label: string, icon?: Component, handler: Function, disabled?: boolean, destructive?: boolean}>}
|
|
46
|
+
*/
|
|
47
|
+
actions: {
|
|
48
|
+
type: Array,
|
|
49
|
+
default: () => [],
|
|
50
|
+
},
|
|
51
|
+
/** The row/object data (passed to action handlers) */
|
|
52
|
+
row: {
|
|
53
|
+
type: Object,
|
|
54
|
+
default: null,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
methods: {
|
|
59
|
+
onAction(action) {
|
|
60
|
+
if (action.handler && typeof action.handler === 'function') {
|
|
61
|
+
action.handler(this.row)
|
|
62
|
+
}
|
|
63
|
+
this.$emit('action', { action: action.label, row: this.row })
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<style scoped>
|
|
70
|
+
.cn-row-action--destructive {
|
|
71
|
+
color: var(--color-error) !important;
|
|
72
|
+
}
|
|
73
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as CnRowActions } from './CnRowActions.vue'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="cn-settings-card" :class="{ 'cn-settings-card--collapsible': collapsible }">
|
|
3
|
+
<h4
|
|
4
|
+
v-if="title"
|
|
5
|
+
:class="{ 'cn-settings-card__header--clickable': collapsible }"
|
|
6
|
+
@click="collapsible ? toggleCollapsed() : null">
|
|
7
|
+
<span>{{ icon }} {{ title }}</span>
|
|
8
|
+
<ChevronDown
|
|
9
|
+
v-if="collapsible && !isCollapsed"
|
|
10
|
+
:size="20"
|
|
11
|
+
class="cn-settings-card__chevron" />
|
|
12
|
+
<ChevronUp
|
|
13
|
+
v-if="collapsible && isCollapsed"
|
|
14
|
+
:size="20"
|
|
15
|
+
class="cn-settings-card__chevron" />
|
|
16
|
+
</h4>
|
|
17
|
+
|
|
18
|
+
<transition v-if="collapsible" name="cn-slide-fade">
|
|
19
|
+
<div v-show="!isCollapsed" class="cn-settings-card__content">
|
|
20
|
+
<slot />
|
|
21
|
+
</div>
|
|
22
|
+
</transition>
|
|
23
|
+
|
|
24
|
+
<div v-else>
|
|
25
|
+
<slot />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script>
|
|
31
|
+
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
|
|
32
|
+
import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* CnSettingsCard — Collapsible card for settings and configuration sections.
|
|
36
|
+
*
|
|
37
|
+
* Extracted from OpenRegister's SettingsCard. Provides a titled card with
|
|
38
|
+
* optional collapse/expand animation.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <CnSettingsCard title="Database Settings" icon="🗄️" collapsible>
|
|
42
|
+
* <p>Content here</p>
|
|
43
|
+
* </CnSettingsCard>
|
|
44
|
+
*/
|
|
45
|
+
export default {
|
|
46
|
+
name: 'CnSettingsCard',
|
|
47
|
+
|
|
48
|
+
components: {
|
|
49
|
+
ChevronDown,
|
|
50
|
+
ChevronUp,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
props: {
|
|
54
|
+
/** Card title text */
|
|
55
|
+
title: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: '',
|
|
58
|
+
},
|
|
59
|
+
/** Icon emoji or text displayed before the title */
|
|
60
|
+
icon: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: '',
|
|
63
|
+
},
|
|
64
|
+
/** Whether the card can be collapsed */
|
|
65
|
+
collapsible: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
/** Whether the card starts collapsed (only applies when collapsible) */
|
|
70
|
+
defaultCollapsed: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
data() {
|
|
77
|
+
return {
|
|
78
|
+
isCollapsed: this.defaultCollapsed,
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
methods: {
|
|
83
|
+
toggleCollapsed() {
|
|
84
|
+
if (this.collapsible) {
|
|
85
|
+
this.isCollapsed = !this.isCollapsed
|
|
86
|
+
/** @event toggle Emitted when collapse state changes. Payload: isCollapsed boolean. */
|
|
87
|
+
this.$emit('toggle', this.isCollapsed)
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
</script>
|