@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.6

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