@conduction/nextcloud-vue 0.1.0-beta.11 → 0.1.0-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/nextcloud-vue.cjs +67614 -0
  2. package/dist/nextcloud-vue.cjs.js +13518 -13617
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.cjs.map +1 -0
  5. package/dist/nextcloud-vue.css +1796 -1800
  6. package/dist/nextcloud-vue.esm.js +13518 -13617
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +3 -2
  9. package/src/components/CnActionsBar/CnActionsBar.vue +254 -254
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -570
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -422
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
  15. package/src/components/CnCard/CnCard.vue +415 -415
  16. package/src/components/CnCardGrid/CnCardGrid.vue +156 -156
  17. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  18. package/src/components/CnChartWidget/CnChartWidget.vue +346 -346
  19. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  20. package/src/components/CnContextMenu/CnContextMenu.vue +142 -142
  21. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -266
  22. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -229
  23. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -397
  24. package/src/components/CnDataTable/CnDataTable.vue +362 -362
  25. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -177
  26. package/src/components/CnDetailCard/CnDetailCard.vue +225 -225
  27. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -256
  28. package/src/components/CnDetailPage/CnDetailPage.vue +432 -432
  29. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -234
  30. package/src/components/CnFilterBar/CnFilterBar.vue +153 -153
  31. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -1047
  32. package/src/components/CnIcon/CnIcon.vue +89 -89
  33. package/src/components/CnIndexPage/CnIndexPage.vue +981 -980
  34. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -536
  35. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -219
  36. package/src/components/CnItemCard/CnItemCard.vue +134 -134
  37. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -312
  38. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -93
  39. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -161
  40. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -327
  41. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -245
  42. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -191
  43. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -494
  44. package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
  45. package/src/components/CnNotesCard/CnNotesCard.vue +416 -416
  46. package/src/components/CnObjectCard/CnObjectCard.vue +294 -294
  47. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -854
  48. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -289
  49. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -369
  50. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -287
  51. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -250
  52. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -255
  53. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -259
  54. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -483
  55. package/src/components/CnPageHeader/CnPageHeader.vue +61 -61
  56. package/src/components/CnPagination/CnPagination.vue +253 -253
  57. package/src/components/CnProgressBar/CnProgressBar.vue +262 -262
  58. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -793
  59. package/src/components/CnRowActions/CnRowActions.vue +95 -95
  60. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
  61. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -788
  62. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
  63. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
  64. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
  65. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  66. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -267
  67. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -437
  68. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -321
  69. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -90
  70. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -545
  71. package/src/components/CnTableWidget/CnTableWidget.vue +333 -333
  72. package/src/components/CnTasksCard/CnTasksCard.vue +374 -374
  73. package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
  74. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -294
  75. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -436
  76. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -313
  77. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
  78. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -248
@@ -1,362 +1,362 @@
1
- <template>
2
- <div class="cn-table-container" :class="{ 'cn-table-container--scrollable': scrollable }">
3
- <!-- Loading State -->
4
- <div v-if="loading" class="cn-table-loading">
5
- <NcLoadingIcon :size="32" />
6
- <p>{{ loadingText }}</p>
7
- </div>
8
-
9
- <!-- Table -->
10
- <table v-else class="cn-data-table">
11
- <thead>
12
- <tr>
13
- <!-- Checkbox column -->
14
- <th v-if="selectable" class="cn-table-col--checkbox">
15
- <NcCheckboxRadioSwitch
16
- :checked="allSelected"
17
- :indeterminate="someSelected && !allSelected"
18
- @update:checked="toggleSelectAll" />
19
- </th>
20
-
21
- <!-- Data columns -->
22
- <th
23
- v-for="col in effectiveColumns"
24
- :key="col.key"
25
- :class="[
26
- col.sortable ? 'cn-table-header--sortable' : '',
27
- col.class || '',
28
- ]"
29
- :style="col.width ? { width: col.width } : {}"
30
- @click="col.sortable ? onSort(col.key) : null">
31
- {{ col.label }}
32
- <span
33
- v-if="col.sortable && sortKey === col.key"
34
- class="cn-table-sort-indicator">
35
- {{ sortOrder === 'asc' ? '▲' : '▼' }}
36
- </span>
37
- </th>
38
-
39
- <!-- Actions column -->
40
- <th v-if="$scopedSlots['row-actions']" class="cn-table-col--actions">
41
- <slot name="actions-header" />
42
- </th>
43
- </tr>
44
- </thead>
45
-
46
- <tbody>
47
- <!-- Empty state -->
48
- <tr v-if="rows.length === 0" class="cn-table-empty">
49
- <td :colspan="totalColumns">
50
- <slot name="empty">
51
- {{ emptyText }}
52
- </slot>
53
- </td>
54
- </tr>
55
-
56
- <!-- Data rows -->
57
- <tr
58
- v-for="row in rows"
59
- v-else
60
- :key="row[rowKey]"
61
- class="cn-table-row"
62
- :class="[
63
- isSelected(row) ? 'cn-table-row--selected' : '',
64
- rowClass ? rowClass(row) : '',
65
- ]"
66
- @click="$emit('row-click', row)"
67
- @contextmenu.prevent="$emit('row-context-menu', { row, event: $event })">
68
- <!-- Checkbox -->
69
- <td v-if="selectable" class="cn-table-col--checkbox" @click.stop>
70
- <NcCheckboxRadioSwitch
71
- :checked="isSelected(row)"
72
- @update:checked="toggleSelect(row)" />
73
- </td>
74
-
75
- <!-- Data cells -->
76
- <td
77
- v-for="col in effectiveColumns"
78
- :key="col.key"
79
- :class="col.cellClass || ''"
80
- :style="col.width ? { maxWidth: col.width } : {}">
81
- <slot :name="'column-' + col.key" :row="row" :value="getCellValue(row, col.key)">
82
- <!-- Schema-driven: use CnCellRenderer -->
83
- <CnCellRenderer
84
- v-if="isSchemaColumn(col)"
85
- :value="getCellValue(row, col.key)"
86
- :property="getSchemaProperty(col.key)" />
87
- <!-- Manual: plain text -->
88
- <template v-else>
89
- {{ getCellValue(row, col.key) }}
90
- </template>
91
- </slot>
92
- </td>
93
-
94
- <!-- Row actions -->
95
- <td v-if="$scopedSlots['row-actions']" class="cn-table-col--actions" @click.stop>
96
- <slot name="row-actions" :row="row" />
97
- </td>
98
- </tr>
99
- </tbody>
100
- </table>
101
- </div>
102
- </template>
103
-
104
- <script>
105
- import { translate as t } from '@nextcloud/l10n'
106
- import { NcLoadingIcon, NcCheckboxRadioSwitch } from '@nextcloud/vue'
107
- import { CnCellRenderer } from '../CnCellRenderer/index.js'
108
- import { columnsFromSchema } from '../../utils/schema.js'
109
-
110
- /**
111
- * CnDataTable — Generic sortable data table for list views.
112
- *
113
- * Replaces the copy-pasted `<table class="viewTable">` HTML pattern found in
114
- * every list view across OpenRegister, Pipelinq, and Procest. Supports sorting,
115
- * row selection, custom cell rendering via scoped slots, loading states,
116
- * and empty states.
117
- *
118
- * When a `schema` prop is provided, columns are auto-generated from schema
119
- * properties and cells render through CnCellRenderer for type-aware formatting
120
- * (dates, booleans, UUIDs, enums, etc.). Scoped slots still override individual
121
- * columns when needed.
122
- *
123
- * @example Manual columns (backwards compatible)
124
- * <CnDataTable
125
- * :columns="[
126
- * { key: 'name', label: 'Name', sortable: true },
127
- * { key: 'email', label: 'Email' },
128
- * ]"
129
- * :rows="clients"
130
- * @row-click="openClient" />
131
- *
132
- * @example Schema-driven (auto columns)
133
- * <CnDataTable :schema="schema" :rows="objects" />
134
- *
135
- * @example Schema with overrides and custom cell
136
- * <CnDataTable
137
- * :schema="schema"
138
- * :exclude-columns="['description']"
139
- * :column-overrides="{ status: { width: '200px' } }"
140
- * :rows="objects">
141
- * <template #column-status="{ row, value }">
142
- * <QuickStatusDropdown :case-obj="row" />
143
- * </template>
144
- * </CnDataTable>
145
- */
146
- export default {
147
- name: 'CnDataTable',
148
-
149
- components: {
150
- NcLoadingIcon,
151
- NcCheckboxRadioSwitch,
152
- CnCellRenderer,
153
- },
154
-
155
- props: {
156
- /**
157
- * Column definitions (manual mode).
158
- * Not required when `schema` is provided.
159
- * @type {Array<{key: string, label: string, sortable?: boolean, width?: string, class?: string, cellClass?: string}>}
160
- */
161
- columns: {
162
- type: Array,
163
- default: () => [],
164
- },
165
- /**
166
- * Schema object with `properties` field (schema-driven mode).
167
- * When provided, columns are auto-generated from schema properties.
168
- */
169
- schema: {
170
- type: Object,
171
- default: null,
172
- },
173
- /** Per-column overrides when using schema mode: { key: { width, label, sortable, ... } } */
174
- columnOverrides: {
175
- type: Object,
176
- default: () => ({}),
177
- },
178
- /** Column keys to exclude when using schema mode */
179
- excludeColumns: {
180
- type: Array,
181
- default: () => [],
182
- },
183
- /** Column keys to include when using schema mode (whitelist) */
184
- includeColumns: {
185
- type: Array,
186
- default: null,
187
- },
188
- /** Row data array. Each row should have a unique identifier (see rowKey). */
189
- rows: {
190
- type: Array,
191
- default: () => [],
192
- },
193
- /** Whether data is loading (shows loading spinner) */
194
- loading: {
195
- type: Boolean,
196
- default: false,
197
- },
198
- /** Current sort column key */
199
- sortKey: {
200
- type: String,
201
- default: null,
202
- },
203
- /** Current sort order: 'asc', 'desc', or null (no sort) */
204
- sortOrder: {
205
- type: String,
206
- default: 'asc',
207
- validator: (v) => v === null || ['asc', 'desc'].includes(v),
208
- },
209
- /** Whether rows can be selected with checkboxes */
210
- selectable: {
211
- type: Boolean,
212
- default: false,
213
- },
214
- /** Array of currently selected row IDs */
215
- selectedIds: {
216
- type: Array,
217
- default: () => [],
218
- },
219
- /** Property name used as unique row identifier */
220
- rowKey: {
221
- type: String,
222
- default: 'id',
223
- },
224
- /** Text shown when there are no rows */
225
- emptyText: {
226
- type: String,
227
- default: () => t('nextcloud-vue', 'No items found'),
228
- },
229
- /** Function returning CSS class(es) for a row: (row) => string|object */
230
- rowClass: {
231
- type: Function,
232
- default: null,
233
- },
234
- /** Whether to constrain table height and make it scrollable */
235
- scrollable: {
236
- type: Boolean,
237
- default: false,
238
- },
239
- /** Text shown while loading */
240
- loadingText: {
241
- type: String,
242
- default: () => t('nextcloud-vue', 'Loading...'),
243
- },
244
- },
245
-
246
- computed: {
247
- /**
248
- * Effective columns: schema-generated or manually provided.
249
- * Schema columns take precedence when schema is provided and no manual columns given.
250
- */
251
- effectiveColumns() {
252
- if (this.schema && this.columns.length === 0) {
253
- return columnsFromSchema(this.schema, {
254
- exclude: this.excludeColumns,
255
- include: this.includeColumns,
256
- overrides: this.columnOverrides,
257
- })
258
- }
259
- return this.columns
260
- },
261
-
262
- totalColumns() {
263
- let count = this.effectiveColumns.length
264
- if (this.selectable) count++
265
- if (this.$scopedSlots['row-actions']) count++
266
- return count
267
- },
268
-
269
- allSelected() {
270
- return this.rows.length > 0
271
- && this.rows.every((row) => this.selectedIds.includes(row[this.rowKey]))
272
- },
273
-
274
- someSelected() {
275
- return this.rows.some((row) => this.selectedIds.includes(row[this.rowKey]))
276
- },
277
- },
278
-
279
- methods: {
280
- /**
281
- * Get a cell value from a row using dot-notation key.
282
- * @param {object} row The row data
283
- * @param {string} key The column key (supports dot notation: 'address.city')
284
- * @return {*} The cell value
285
- */
286
- getCellValue(row, key) {
287
- if (key.includes('.')) {
288
- return key.split('.').reduce((obj, k) => obj?.[k], row)
289
- }
290
- return row[key]
291
- },
292
-
293
- /**
294
- * Check if a column was generated from schema (has type info).
295
- * @param {object} col Column definition
296
- * @return {boolean}
297
- */
298
- isSchemaColumn(col) {
299
- return !!(this.schema && col.type)
300
- },
301
-
302
- /**
303
- * Get the schema property definition for a column key.
304
- * @param {string} key Column key
305
- * @return {object} Property definition
306
- */
307
- getSchemaProperty(key) {
308
- return this.schema?.properties?.[key] || {}
309
- },
310
-
311
- isSelected(row) {
312
- return this.selectedIds.includes(row[this.rowKey])
313
- },
314
-
315
- /**
316
- * Handle sort column click.
317
- * @param {string} key Column key
318
- */
319
- onSort(key) {
320
- let newKey = key
321
- let order = 'asc'
322
- if (this.sortKey === key) {
323
- if (this.sortOrder === 'asc') {
324
- order = 'desc'
325
- } else {
326
- // desc → disabled: clear sort entirely
327
- newKey = null
328
- order = null
329
- }
330
- }
331
- /**
332
- * @event sort Emitted when a sortable column header is clicked.
333
- * @type {{ key: string|null, order: 'asc'|'desc'|null }}
334
- */
335
- this.$emit('sort', { key: newKey, order })
336
- },
337
-
338
- toggleSelect(row) {
339
- const id = row[this.rowKey]
340
- const newIds = this.isSelected(row)
341
- ? this.selectedIds.filter((i) => i !== id)
342
- : [...this.selectedIds, id]
343
- /** @event select Emitted when row selection changes. Payload: array of selected IDs. */
344
- this.$emit('select', newIds)
345
- },
346
-
347
- toggleSelectAll() {
348
- if (this.allSelected) {
349
- // Remove only current page IDs, preserving cross-page selections
350
- const currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))
351
- this.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))
352
- } else {
353
- // Add current page IDs to existing selections
354
- const merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])
355
- this.$emit('select', [...merged])
356
- }
357
- /** @event select-all Emitted when select-all checkbox is toggled. */
358
- this.$emit('select-all', !this.allSelected)
359
- },
360
- },
361
- }
362
- </script>
1
+ <template>
2
+ <div class="cn-table-container" :class="{ 'cn-table-container--scrollable': scrollable }">
3
+ <!-- Loading State -->
4
+ <div v-if="loading" class="cn-table-loading">
5
+ <NcLoadingIcon :size="32" />
6
+ <p>{{ loadingText }}</p>
7
+ </div>
8
+
9
+ <!-- Table -->
10
+ <table v-else class="cn-data-table">
11
+ <thead>
12
+ <tr>
13
+ <!-- Checkbox column -->
14
+ <th v-if="selectable" class="cn-table-col--checkbox">
15
+ <NcCheckboxRadioSwitch
16
+ :checked="allSelected"
17
+ :indeterminate="someSelected && !allSelected"
18
+ @update:checked="toggleSelectAll" />
19
+ </th>
20
+
21
+ <!-- Data columns -->
22
+ <th
23
+ v-for="col in effectiveColumns"
24
+ :key="col.key"
25
+ :class="[
26
+ col.sortable ? 'cn-table-header--sortable' : '',
27
+ col.class || '',
28
+ ]"
29
+ :style="col.width ? { width: col.width } : {}"
30
+ @click="col.sortable ? onSort(col.key) : null">
31
+ {{ col.label }}
32
+ <span
33
+ v-if="col.sortable && sortKey === col.key"
34
+ class="cn-table-sort-indicator">
35
+ {{ sortOrder === 'asc' ? '▲' : '▼' }}
36
+ </span>
37
+ </th>
38
+
39
+ <!-- Actions column -->
40
+ <th v-if="$scopedSlots['row-actions']" class="cn-table-col--actions">
41
+ <slot name="actions-header" />
42
+ </th>
43
+ </tr>
44
+ </thead>
45
+
46
+ <tbody>
47
+ <!-- Empty state -->
48
+ <tr v-if="rows.length === 0" class="cn-table-empty">
49
+ <td :colspan="totalColumns">
50
+ <slot name="empty">
51
+ {{ emptyText }}
52
+ </slot>
53
+ </td>
54
+ </tr>
55
+
56
+ <!-- Data rows -->
57
+ <tr
58
+ v-for="row in rows"
59
+ v-else
60
+ :key="row[rowKey]"
61
+ class="cn-table-row"
62
+ :class="[
63
+ isSelected(row) ? 'cn-table-row--selected' : '',
64
+ rowClass ? rowClass(row) : '',
65
+ ]"
66
+ @click="$emit('row-click', row)"
67
+ @contextmenu.prevent="$emit('row-context-menu', { row, event: $event })">
68
+ <!-- Checkbox -->
69
+ <td v-if="selectable" class="cn-table-col--checkbox" @click.stop>
70
+ <NcCheckboxRadioSwitch
71
+ :checked="isSelected(row)"
72
+ @update:checked="toggleSelect(row)" />
73
+ </td>
74
+
75
+ <!-- Data cells -->
76
+ <td
77
+ v-for="col in effectiveColumns"
78
+ :key="col.key"
79
+ :class="col.cellClass || ''"
80
+ :style="col.width ? { maxWidth: col.width } : {}">
81
+ <slot :name="'column-' + col.key" :row="row" :value="getCellValue(row, col.key)">
82
+ <!-- Schema-driven: use CnCellRenderer -->
83
+ <CnCellRenderer
84
+ v-if="isSchemaColumn(col)"
85
+ :value="getCellValue(row, col.key)"
86
+ :property="getSchemaProperty(col.key)" />
87
+ <!-- Manual: plain text -->
88
+ <template v-else>
89
+ {{ getCellValue(row, col.key) }}
90
+ </template>
91
+ </slot>
92
+ </td>
93
+
94
+ <!-- Row actions -->
95
+ <td v-if="$scopedSlots['row-actions']" class="cn-table-col--actions" @click.stop>
96
+ <slot name="row-actions" :row="row" />
97
+ </td>
98
+ </tr>
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+ </template>
103
+
104
+ <script>
105
+ import { translate as t } from '@nextcloud/l10n'
106
+ import { NcLoadingIcon, NcCheckboxRadioSwitch } from '@nextcloud/vue'
107
+ import { CnCellRenderer } from '../CnCellRenderer/index.js'
108
+ import { columnsFromSchema } from '../../utils/schema.js'
109
+
110
+ /**
111
+ * CnDataTable — Generic sortable data table for list views.
112
+ *
113
+ * Replaces the copy-pasted `<table class="viewTable">` HTML pattern found in
114
+ * every list view across OpenRegister, Pipelinq, and Procest. Supports sorting,
115
+ * row selection, custom cell rendering via scoped slots, loading states,
116
+ * and empty states.
117
+ *
118
+ * When a `schema` prop is provided, columns are auto-generated from schema
119
+ * properties and cells render through CnCellRenderer for type-aware formatting
120
+ * (dates, booleans, UUIDs, enums, etc.). Scoped slots still override individual
121
+ * columns when needed.
122
+ *
123
+ * @example Manual columns (backwards compatible)
124
+ * <CnDataTable
125
+ * :columns="[
126
+ * { key: 'name', label: 'Name', sortable: true },
127
+ * { key: 'email', label: 'Email' },
128
+ * ]"
129
+ * :rows="clients"
130
+ * @row-click="openClient" />
131
+ *
132
+ * @example Schema-driven (auto columns)
133
+ * <CnDataTable :schema="schema" :rows="objects" />
134
+ *
135
+ * @example Schema with overrides and custom cell
136
+ * <CnDataTable
137
+ * :schema="schema"
138
+ * :exclude-columns="['description']"
139
+ * :column-overrides="{ status: { width: '200px' } }"
140
+ * :rows="objects">
141
+ * <template #column-status="{ row, value }">
142
+ * <QuickStatusDropdown :case-obj="row" />
143
+ * </template>
144
+ * </CnDataTable>
145
+ */
146
+ export default {
147
+ name: 'CnDataTable',
148
+
149
+ components: {
150
+ NcLoadingIcon,
151
+ NcCheckboxRadioSwitch,
152
+ CnCellRenderer,
153
+ },
154
+
155
+ props: {
156
+ /**
157
+ * Column definitions (manual mode).
158
+ * Not required when `schema` is provided.
159
+ * @type {Array<{key: string, label: string, sortable?: boolean, width?: string, class?: string, cellClass?: string}>}
160
+ */
161
+ columns: {
162
+ type: Array,
163
+ default: () => [],
164
+ },
165
+ /**
166
+ * Schema object with `properties` field (schema-driven mode).
167
+ * When provided, columns are auto-generated from schema properties.
168
+ */
169
+ schema: {
170
+ type: Object,
171
+ default: null,
172
+ },
173
+ /** Per-column overrides when using schema mode: { key: { width, label, sortable, ... } } */
174
+ columnOverrides: {
175
+ type: Object,
176
+ default: () => ({}),
177
+ },
178
+ /** Column keys to exclude when using schema mode */
179
+ excludeColumns: {
180
+ type: Array,
181
+ default: () => [],
182
+ },
183
+ /** Column keys to include when using schema mode (whitelist) */
184
+ includeColumns: {
185
+ type: Array,
186
+ default: null,
187
+ },
188
+ /** Row data array. Each row should have a unique identifier (see rowKey). */
189
+ rows: {
190
+ type: Array,
191
+ default: () => [],
192
+ },
193
+ /** Whether data is loading (shows loading spinner) */
194
+ loading: {
195
+ type: Boolean,
196
+ default: false,
197
+ },
198
+ /** Current sort column key */
199
+ sortKey: {
200
+ type: String,
201
+ default: null,
202
+ },
203
+ /** Current sort order: 'asc', 'desc', or null (no sort) */
204
+ sortOrder: {
205
+ type: String,
206
+ default: 'asc',
207
+ validator: (v) => v === null || ['asc', 'desc'].includes(v),
208
+ },
209
+ /** Whether rows can be selected with checkboxes */
210
+ selectable: {
211
+ type: Boolean,
212
+ default: false,
213
+ },
214
+ /** Array of currently selected row IDs */
215
+ selectedIds: {
216
+ type: Array,
217
+ default: () => [],
218
+ },
219
+ /** Property name used as unique row identifier */
220
+ rowKey: {
221
+ type: String,
222
+ default: 'id',
223
+ },
224
+ /** Text shown when there are no rows */
225
+ emptyText: {
226
+ type: String,
227
+ default: () => t('nextcloud-vue', 'No items found'),
228
+ },
229
+ /** Function returning CSS class(es) for a row: (row) => string|object */
230
+ rowClass: {
231
+ type: Function,
232
+ default: null,
233
+ },
234
+ /** Whether to constrain table height and make it scrollable */
235
+ scrollable: {
236
+ type: Boolean,
237
+ default: false,
238
+ },
239
+ /** Text shown while loading */
240
+ loadingText: {
241
+ type: String,
242
+ default: () => t('nextcloud-vue', 'Loading...'),
243
+ },
244
+ },
245
+
246
+ computed: {
247
+ /**
248
+ * Effective columns: schema-generated or manually provided.
249
+ * Schema columns take precedence when schema is provided and no manual columns given.
250
+ */
251
+ effectiveColumns() {
252
+ if (this.schema && this.columns.length === 0) {
253
+ return columnsFromSchema(this.schema, {
254
+ exclude: this.excludeColumns,
255
+ include: this.includeColumns,
256
+ overrides: this.columnOverrides,
257
+ })
258
+ }
259
+ return this.columns
260
+ },
261
+
262
+ totalColumns() {
263
+ let count = this.effectiveColumns.length
264
+ if (this.selectable) count++
265
+ if (this.$scopedSlots['row-actions']) count++
266
+ return count
267
+ },
268
+
269
+ allSelected() {
270
+ return this.rows.length > 0
271
+ && this.rows.every((row) => this.selectedIds.includes(row[this.rowKey]))
272
+ },
273
+
274
+ someSelected() {
275
+ return this.rows.some((row) => this.selectedIds.includes(row[this.rowKey]))
276
+ },
277
+ },
278
+
279
+ methods: {
280
+ /**
281
+ * Get a cell value from a row using dot-notation key.
282
+ * @param {object} row The row data
283
+ * @param {string} key The column key (supports dot notation: 'address.city')
284
+ * @return {*} The cell value
285
+ */
286
+ getCellValue(row, key) {
287
+ if (key.includes('.')) {
288
+ return key.split('.').reduce((obj, k) => obj?.[k], row)
289
+ }
290
+ return row[key]
291
+ },
292
+
293
+ /**
294
+ * Check if a column was generated from schema (has type info).
295
+ * @param {object} col Column definition
296
+ * @return {boolean}
297
+ */
298
+ isSchemaColumn(col) {
299
+ return !!(this.schema && col.type)
300
+ },
301
+
302
+ /**
303
+ * Get the schema property definition for a column key.
304
+ * @param {string} key Column key
305
+ * @return {object} Property definition
306
+ */
307
+ getSchemaProperty(key) {
308
+ return this.schema?.properties?.[key] || {}
309
+ },
310
+
311
+ isSelected(row) {
312
+ return this.selectedIds.includes(row[this.rowKey])
313
+ },
314
+
315
+ /**
316
+ * Handle sort column click.
317
+ * @param {string} key Column key
318
+ */
319
+ onSort(key) {
320
+ let newKey = key
321
+ let order = 'asc'
322
+ if (this.sortKey === key) {
323
+ if (this.sortOrder === 'asc') {
324
+ order = 'desc'
325
+ } else {
326
+ // desc → disabled: clear sort entirely
327
+ newKey = null
328
+ order = null
329
+ }
330
+ }
331
+ /**
332
+ * @event sort Emitted when a sortable column header is clicked.
333
+ * @type {{ key: string|null, order: 'asc'|'desc'|null }}
334
+ */
335
+ this.$emit('sort', { key: newKey, order })
336
+ },
337
+
338
+ toggleSelect(row) {
339
+ const id = row[this.rowKey]
340
+ const newIds = this.isSelected(row)
341
+ ? this.selectedIds.filter((i) => i !== id)
342
+ : [...this.selectedIds, id]
343
+ /** @event select Emitted when row selection changes. Payload: array of selected IDs. */
344
+ this.$emit('select', newIds)
345
+ },
346
+
347
+ toggleSelectAll() {
348
+ if (this.allSelected) {
349
+ // Remove only current page IDs, preserving cross-page selections
350
+ const currentPageIds = new Set(this.rows.map((row) => row[this.rowKey]))
351
+ this.$emit('select', this.selectedIds.filter((id) => !currentPageIds.has(id)))
352
+ } else {
353
+ // Add current page IDs to existing selections
354
+ const merged = new Set([...this.selectedIds, ...this.rows.map((row) => row[this.rowKey])])
355
+ this.$emit('select', [...merged])
356
+ }
357
+ /** @event select-all Emitted when select-all checkbox is toggled. */
358
+ this.$emit('select-all', !this.allSelected)
359
+ },
360
+ },
361
+ }
362
+ </script>