@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.
Files changed (93) hide show
  1. package/dist/nextcloud-vue.cjs.js +10710 -0
  2. package/dist/nextcloud-vue.cjs.js.map +1 -0
  3. package/dist/nextcloud-vue.css +803 -0
  4. package/dist/nextcloud-vue.esm.js +10665 -0
  5. package/dist/nextcloud-vue.esm.js.map +1 -0
  6. package/package.json +63 -0
  7. package/src/components/CnCardGrid/CnCardGrid.vue +152 -0
  8. package/src/components/CnCardGrid/index.js +1 -0
  9. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -0
  10. package/src/components/CnCellRenderer/index.js +1 -0
  11. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -0
  12. package/src/components/CnConfigurationCard/index.js +1 -0
  13. package/src/components/CnDataTable/CnDataTable.vue +354 -0
  14. package/src/components/CnDataTable/index.js +1 -0
  15. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +88 -0
  16. package/src/components/CnDetailViewLayout/index.js +1 -0
  17. package/src/components/CnEmptyState/CnEmptyState.vue +78 -0
  18. package/src/components/CnEmptyState/index.js +1 -0
  19. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +223 -0
  20. package/src/components/CnFacetSidebar/index.js +1 -0
  21. package/src/components/CnFilterBar/CnFilterBar.vue +152 -0
  22. package/src/components/CnFilterBar/index.js +1 -0
  23. package/src/components/CnIndexPage/CnIndexPage.vue +682 -0
  24. package/src/components/CnIndexPage/index.js +1 -0
  25. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -0
  26. package/src/components/CnKpiGrid/index.js +1 -0
  27. package/src/components/CnListViewLayout/CnListViewLayout.vue +80 -0
  28. package/src/components/CnListViewLayout/index.js +1 -0
  29. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -0
  30. package/src/components/CnMassActionBar/index.js +1 -0
  31. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -0
  32. package/src/components/CnMassCopyDialog/index.js +1 -0
  33. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -0
  34. package/src/components/CnMassDeleteDialog/index.js +1 -0
  35. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -0
  36. package/src/components/CnMassExportDialog/index.js +1 -0
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -0
  38. package/src/components/CnMassImportDialog/index.js +1 -0
  39. package/src/components/CnObjectCard/CnObjectCard.vue +292 -0
  40. package/src/components/CnObjectCard/index.js +1 -0
  41. package/src/components/CnPagination/CnPagination.vue +252 -0
  42. package/src/components/CnPagination/index.js +1 -0
  43. package/src/components/CnRowActions/CnRowActions.vue +73 -0
  44. package/src/components/CnRowActions/index.js +1 -0
  45. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -0
  46. package/src/components/CnSettingsCard/index.js +1 -0
  47. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -0
  48. package/src/components/CnSettingsSection/index.js +1 -0
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +366 -0
  50. package/src/components/CnStatsBlock/index.js +1 -0
  51. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -0
  52. package/src/components/CnStatusBadge/index.js +1 -0
  53. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -0
  54. package/src/components/CnVersionInfoCard/index.js +1 -0
  55. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +77 -0
  56. package/src/components/CnViewModeToggle/index.js +1 -0
  57. package/src/components/index.js +25 -0
  58. package/src/composables/index.js +3 -0
  59. package/src/composables/useDetailView.js +132 -0
  60. package/src/composables/useListView.js +153 -0
  61. package/src/composables/useSubResource.js +142 -0
  62. package/src/css/badge.css +51 -0
  63. package/src/css/card.css +128 -0
  64. package/src/css/detail.css +68 -0
  65. package/src/css/index.css +8 -0
  66. package/src/css/layout.css +90 -0
  67. package/src/css/pagination.css +72 -0
  68. package/src/css/table.css +143 -0
  69. package/src/css/utilities.css +46 -0
  70. package/src/index.js +50 -0
  71. package/src/store/createSubResourcePlugin.js +135 -0
  72. package/src/store/index.js +3 -0
  73. package/src/store/plugins/auditTrails.js +17 -0
  74. package/src/store/plugins/files.js +186 -0
  75. package/src/store/plugins/index.js +4 -0
  76. package/src/store/plugins/lifecycle.js +180 -0
  77. package/src/store/plugins/relations.js +68 -0
  78. package/src/store/useObjectStore.js +625 -0
  79. package/src/types/auditTrail.d.ts +32 -0
  80. package/src/types/file.d.ts +23 -0
  81. package/src/types/index.d.ts +35 -0
  82. package/src/types/notification.d.ts +36 -0
  83. package/src/types/object.d.ts +40 -0
  84. package/src/types/organisation.d.ts +41 -0
  85. package/src/types/register.d.ts +25 -0
  86. package/src/types/schema.d.ts +39 -0
  87. package/src/types/shared.d.ts +79 -0
  88. package/src/types/source.d.ts +14 -0
  89. package/src/types/task.d.ts +31 -0
  90. package/src/utils/errors.js +96 -0
  91. package/src/utils/headers.js +44 -0
  92. package/src/utils/index.js +3 -0
  93. package/src/utils/schema.js +287 -0
@@ -0,0 +1,682 @@
1
+ <template>
2
+ <div class="cn-index-page">
3
+ <!-- Header -->
4
+ <div class="cn-index-page__header">
5
+ <div class="cn-index-page__title-area">
6
+ <h2 class="cn-index-page__title">{{ title }}</h2>
7
+ <span v-if="pagination && pagination.total > 0" class="cn-index-page__count">
8
+ {{ countText }}
9
+ </span>
10
+ </div>
11
+ <div class="cn-index-page__header-actions">
12
+ <!-- Mass actions dropdown (shows when items selected) -->
13
+ <CnMassActionBar
14
+ v-if="selectable"
15
+ :selected-ids="selectedIds"
16
+ :count="selectedIds.length"
17
+ :show-import="showMassImport"
18
+ :show-export="showMassExport"
19
+ :show-copy="showMassCopy"
20
+ :show-delete="showMassDelete"
21
+ @mass-import="showImportDialog = true"
22
+ @mass-export="showExportDialog = true"
23
+ @mass-copy="showCopyDialog = true"
24
+ @mass-delete="showDeleteDialog = true">
25
+ <template #actions="{ count: selCount, selectedIds: selIds }">
26
+ <slot name="mass-actions" :count="selCount" :selected-ids="selIds" />
27
+ </template>
28
+ </CnMassActionBar>
29
+
30
+ <CnViewModeToggle
31
+ v-if="showViewToggle"
32
+ :value="currentViewMode"
33
+ @input="onViewModeChange" />
34
+ <slot name="header-actions" />
35
+ </div>
36
+ </div>
37
+
38
+ <!-- Mass delete dialog -->
39
+ <CnMassDeleteDialog
40
+ v-if="showDeleteDialog"
41
+ ref="deleteDialog"
42
+ :items="selectedObjects"
43
+ :name-field="massActionNameField"
44
+ @confirm="onMassDeleteConfirm"
45
+ @close="showDeleteDialog = false" />
46
+
47
+ <!-- Mass copy dialog -->
48
+ <CnMassCopyDialog
49
+ v-if="showCopyDialog"
50
+ ref="copyDialog"
51
+ :items="selectedObjects"
52
+ :name-field="massActionNameField"
53
+ @confirm="onMassCopyConfirm"
54
+ @close="showCopyDialog = false" />
55
+
56
+ <!-- Mass export dialog -->
57
+ <CnMassExportDialog
58
+ v-if="showExportDialog"
59
+ ref="exportDialog"
60
+ :formats="exportFormats"
61
+ @confirm="onMassExportConfirm"
62
+ @close="showExportDialog = false" />
63
+
64
+ <!-- Mass import dialog -->
65
+ <CnMassImportDialog
66
+ v-if="showImportDialog"
67
+ ref="importDialog"
68
+ :options="importOptions"
69
+ @confirm="onMassImportConfirm"
70
+ @close="showImportDialog = false">
71
+ <template v-if="$scopedSlots['import-fields']" #fields="{ file }">
72
+ <slot name="import-fields" :file="file" />
73
+ </template>
74
+ </CnMassImportDialog>
75
+
76
+ <!-- Body: sidebar + main content -->
77
+ <div class="cn-index-page__body" :class="{ 'cn-index-page__body--with-sidebar': showSidebar }">
78
+ <!-- Facet sidebar -->
79
+ <aside v-if="showSidebar" class="cn-index-page__sidebar">
80
+ <slot name="sidebar">
81
+ <CnFacetSidebar
82
+ v-if="schema"
83
+ :schema="schema"
84
+ :facet-data="facetData"
85
+ :active-filters="activeFilters"
86
+ :loading="facetLoading"
87
+ @filter-change="$emit('filter-change', $event)"
88
+ @clear-all="$emit('clear-filters')" />
89
+ </slot>
90
+ </aside>
91
+
92
+ <!-- Main content area -->
93
+ <div class="cn-index-page__main">
94
+ <!-- Search bar -->
95
+ <div v-if="showSearch" class="cn-index-page__search">
96
+ <CnFilterBar
97
+ :search-value="searchValue"
98
+ :search-placeholder="searchPlaceholder"
99
+ :filters="inlineFilters"
100
+ :show-clear-all="false"
101
+ @search="$emit('search', $event)"
102
+ @filter-change="$emit('filter-change', $event)" />
103
+ </div>
104
+
105
+ <!-- Loading state -->
106
+ <div v-if="loading" class="cn-index-page__loading">
107
+ <NcLoadingIcon :size="32" />
108
+ </div>
109
+
110
+ <!-- Empty state -->
111
+ <div v-else-if="objects.length === 0" class="cn-index-page__empty">
112
+ <slot name="empty">
113
+ <NcEmptyContent :name="emptyText">
114
+ <template #icon>
115
+ <DatabaseSearch :size="64" />
116
+ </template>
117
+ </NcEmptyContent>
118
+ </slot>
119
+ </div>
120
+
121
+ <!-- Table view -->
122
+ <CnDataTable
123
+ v-else-if="currentViewMode === 'table'"
124
+ :schema="schema"
125
+ :columns="columns"
126
+ :rows="objects"
127
+ :sort-key="sortKey"
128
+ :sort-order="sortOrder"
129
+ :selectable="selectable"
130
+ :selected-ids="selectedIds"
131
+ :row-key="rowKey"
132
+ :empty-text="emptyText"
133
+ :exclude-columns="excludeColumns"
134
+ :include-columns="includeColumns"
135
+ :column-overrides="columnOverrides"
136
+ :row-class="rowClass"
137
+ @sort="$emit('sort', $event)"
138
+ @select="$emit('select', $event)"
139
+ @row-click="$emit('row-click', $event)">
140
+ <!-- Pass through column slots -->
141
+ <template
142
+ v-for="col in slotColumns"
143
+ #[`column-${col}`]="{ row, value }">
144
+ <slot :name="'column-' + col" :row="row" :value="value" />
145
+ </template>
146
+
147
+ <!-- Row actions -->
148
+ <template v-if="hasRowActions" #row-actions="{ row }">
149
+ <slot name="row-actions" :row="row">
150
+ <CnRowActions
151
+ v-if="actions.length > 0"
152
+ :actions="actions"
153
+ :row="row"
154
+ @action="$emit('action', $event)" />
155
+ </slot>
156
+ </template>
157
+ </CnDataTable>
158
+
159
+ <!-- Card view -->
160
+ <CnCardGrid
161
+ v-else
162
+ :objects="objects"
163
+ :schema="schema"
164
+ :selectable="selectable"
165
+ :selected-ids="selectedIds"
166
+ :row-key="rowKey"
167
+ :empty-text="emptyText"
168
+ @click="$emit('row-click', $event)"
169
+ @select="$emit('select', $event)">
170
+ <template v-if="$scopedSlots.card" #card="{ object, selected }">
171
+ <slot name="card" :object="object" :selected="selected" />
172
+ </template>
173
+ <template v-if="hasRowActions" #card-actions="{ object }">
174
+ <slot name="row-actions" :row="object">
175
+ <CnRowActions
176
+ v-if="actions.length > 0"
177
+ :actions="actions"
178
+ :row="object"
179
+ @action="$emit('action', $event)" />
180
+ </slot>
181
+ </template>
182
+ </CnCardGrid>
183
+
184
+ <!-- Pagination -->
185
+ <CnPagination
186
+ v-if="pagination && pagination.pages > 1"
187
+ :current-page="pagination.page || 1"
188
+ :total-pages="pagination.pages || 1"
189
+ :total-items="pagination.total || 0"
190
+ :current-page-size="pagination.limit || 20"
191
+ class="cn-index-page__pagination"
192
+ @page-changed="$emit('page-changed', $event)"
193
+ @page-size-changed="$emit('page-size-changed', $event)" />
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </template>
198
+
199
+ <script>
200
+ import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
201
+ import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
202
+ import { CnDataTable } from '../CnDataTable/index.js'
203
+ import { CnCardGrid } from '../CnCardGrid/index.js'
204
+ import { CnPagination } from '../CnPagination/index.js'
205
+ import { CnFilterBar } from '../CnFilterBar/index.js'
206
+ import { CnFacetSidebar } from '../CnFacetSidebar/index.js'
207
+ import { CnViewModeToggle } from '../CnViewModeToggle/index.js'
208
+ import { CnRowActions } from '../CnRowActions/index.js'
209
+ import { CnMassActionBar } from '../CnMassActionBar/index.js'
210
+ import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
211
+ import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
212
+ import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
213
+ import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
214
+
215
+ /**
216
+ * CnIndexPage — Top-level schema-driven index page component.
217
+ *
218
+ * Assembles all sub-components (table, cards, pagination, search, faceted
219
+ * sidebar, view mode toggle) into a single zero-config page. Takes a schema
220
+ * and objects array, then auto-generates everything.
221
+ *
222
+ * @example Minimal usage
223
+ * <CnIndexPage
224
+ * title="Publications"
225
+ * :schema="schema"
226
+ * :objects="publications"
227
+ * :pagination="pagination"
228
+ * :loading="loading"
229
+ * :search-value="search"
230
+ * @search="onSearch"
231
+ * @row-click="openPublication"
232
+ * @page-changed="onPage" />
233
+ *
234
+ * @example Full usage with sidebar, actions, mass actions
235
+ * <CnIndexPage
236
+ * ref="indexPage"
237
+ * title="Cases"
238
+ * :schema="caseSchema"
239
+ * :objects="cases"
240
+ * :pagination="pagination"
241
+ * :loading="loading"
242
+ * :search-value="search"
243
+ * :selected-ids="selectedIds"
244
+ * :facet-data="facetData"
245
+ * :active-filters="filters"
246
+ * :actions="[{ label: 'Edit', handler: editCase }]"
247
+ * @search="onSearch"
248
+ * @select="selectedIds = $event"
249
+ * @row-click="openCase"
250
+ * @mass-delete="onMassDelete"
251
+ * @mass-copy="onMassCopy">
252
+ * <template #header-actions>
253
+ * <NcButton type="primary" @click="createCase">New case</NcButton>
254
+ * </template>
255
+ * <template #mass-actions="{ count, selectedIds }">
256
+ * <NcButton @click="exportSelected(selectedIds)">Export {{ count }}</NcButton>
257
+ * </template>
258
+ * </CnIndexPage>
259
+ *
260
+ * // In methods:
261
+ * async onMassDelete(ids) {
262
+ * try {
263
+ * await store.massDelete(ids)
264
+ * this.$refs.indexPage.setDeleteResult({ success: true })
265
+ * } catch (e) {
266
+ * this.$refs.indexPage.setDeleteResult({ error: e.message })
267
+ * }
268
+ * }
269
+ * async onMassCopy({ ids, getName }) {
270
+ * try {
271
+ * for (const obj of this.selectedObjects) {
272
+ * await store.copyObject(obj.id, { title: getName(obj) })
273
+ * }
274
+ * this.$refs.indexPage.setCopyResult({ success: true })
275
+ * } catch (e) {
276
+ * this.$refs.indexPage.setCopyResult({ error: e.message })
277
+ * }
278
+ * }
279
+ */
280
+ export default {
281
+ name: 'CnIndexPage',
282
+
283
+ components: {
284
+ NcLoadingIcon,
285
+ NcEmptyContent,
286
+ DatabaseSearch,
287
+ CnDataTable,
288
+ CnCardGrid,
289
+ CnPagination,
290
+ CnFilterBar,
291
+ CnFacetSidebar,
292
+ CnViewModeToggle,
293
+ CnRowActions,
294
+ CnMassActionBar,
295
+ CnMassDeleteDialog,
296
+ CnMassCopyDialog,
297
+ CnMassExportDialog,
298
+ CnMassImportDialog,
299
+ },
300
+
301
+ props: {
302
+ /** Page title */
303
+ title: {
304
+ type: String,
305
+ required: true,
306
+ },
307
+ /** Schema definition */
308
+ schema: {
309
+ type: Object,
310
+ default: null,
311
+ },
312
+ /** Manual column definitions (used instead of schema when provided) */
313
+ columns: {
314
+ type: Array,
315
+ default: () => [],
316
+ },
317
+ /** Object/row data array */
318
+ objects: {
319
+ type: Array,
320
+ default: () => [],
321
+ },
322
+ /** Pagination state: { page, pages, total, limit } */
323
+ pagination: {
324
+ type: Object,
325
+ default: null,
326
+ },
327
+ /** Whether data is loading */
328
+ loading: {
329
+ type: Boolean,
330
+ default: false,
331
+ },
332
+ /** Current search term */
333
+ searchValue: {
334
+ type: String,
335
+ default: '',
336
+ },
337
+ /** Search input placeholder */
338
+ searchPlaceholder: {
339
+ type: String,
340
+ default: 'Search...',
341
+ },
342
+ /** Inline filter definitions (shown in the search bar) */
343
+ inlineFilters: {
344
+ type: Array,
345
+ default: () => [],
346
+ },
347
+ /** Facet data from API: { fieldName: { values: [{value, count}] } } */
348
+ facetData: {
349
+ type: Object,
350
+ default: null,
351
+ },
352
+ /** Current active facet filters: { fieldName: [values] } */
353
+ activeFilters: {
354
+ type: Object,
355
+ default: () => ({}),
356
+ },
357
+ /** Whether facet data is loading */
358
+ facetLoading: {
359
+ type: Boolean,
360
+ default: false,
361
+ },
362
+ /** Whether rows/cards can be selected */
363
+ selectable: {
364
+ type: Boolean,
365
+ default: true,
366
+ },
367
+ /** Currently selected IDs */
368
+ selectedIds: {
369
+ type: Array,
370
+ default: () => [],
371
+ },
372
+ /** View mode: 'table' or 'cards' */
373
+ viewMode: {
374
+ type: String,
375
+ default: 'table',
376
+ validator: (v) => ['table', 'cards'].includes(v),
377
+ },
378
+ /** Current sort key */
379
+ sortKey: {
380
+ type: String,
381
+ default: null,
382
+ },
383
+ /** Current sort order */
384
+ sortOrder: {
385
+ type: String,
386
+ default: 'asc',
387
+ },
388
+ /** Unique row identifier property */
389
+ rowKey: {
390
+ type: String,
391
+ default: 'id',
392
+ },
393
+ /** Columns to exclude in schema mode */
394
+ excludeColumns: {
395
+ type: Array,
396
+ default: () => [],
397
+ },
398
+ /** Columns to include in schema mode (whitelist) */
399
+ includeColumns: {
400
+ type: Array,
401
+ default: null,
402
+ },
403
+ /** Per-column overrides in schema mode */
404
+ columnOverrides: {
405
+ type: Object,
406
+ default: () => ({}),
407
+ },
408
+ /** Row action definitions */
409
+ actions: {
410
+ type: Array,
411
+ default: () => [],
412
+ },
413
+ /** Text shown when no items found */
414
+ emptyText: {
415
+ type: String,
416
+ default: 'No items found',
417
+ },
418
+ /** Whether to show the view mode toggle */
419
+ showViewToggle: {
420
+ type: Boolean,
421
+ default: true,
422
+ },
423
+ /** Whether to show the search bar */
424
+ showSearch: {
425
+ type: Boolean,
426
+ default: true,
427
+ },
428
+ /** Function returning CSS class(es) for a row */
429
+ rowClass: {
430
+ type: Function,
431
+ default: null,
432
+ },
433
+ /** Whether to show the built-in mass Import action */
434
+ showMassImport: {
435
+ type: Boolean,
436
+ default: true,
437
+ },
438
+ /** Whether to show the built-in mass Export action */
439
+ showMassExport: {
440
+ type: Boolean,
441
+ default: true,
442
+ },
443
+ /** Whether to show the built-in mass Copy button */
444
+ showMassCopy: {
445
+ type: Boolean,
446
+ default: true,
447
+ },
448
+ /** Whether to show the built-in mass Delete button */
449
+ showMassDelete: {
450
+ type: Boolean,
451
+ default: true,
452
+ },
453
+ /** Property name used to display item names in mass action dialogs */
454
+ massActionNameField: {
455
+ type: String,
456
+ default: 'title',
457
+ },
458
+ /** Available export formats for the export dialog */
459
+ exportFormats: {
460
+ type: Array,
461
+ default: () => [
462
+ { id: 'excel', label: 'Excel (.xlsx)' },
463
+ { id: 'csv', label: 'CSV (.csv)' },
464
+ ],
465
+ },
466
+ /** Import option definitions for the import dialog */
467
+ importOptions: {
468
+ type: Array,
469
+ default: () => [],
470
+ },
471
+ },
472
+
473
+ data() {
474
+ return {
475
+ currentViewMode: this.viewMode,
476
+ showDeleteDialog: false,
477
+ showCopyDialog: false,
478
+ showExportDialog: false,
479
+ showImportDialog: false,
480
+ }
481
+ },
482
+
483
+ computed: {
484
+ countText() {
485
+ if (!this.pagination) return ''
486
+ return `Showing ${this.objects.length} of ${this.pagination.total}`
487
+ },
488
+
489
+ showSidebar() {
490
+ return this.$scopedSlots.sidebar || this.facetData !== null
491
+ },
492
+
493
+ hasRowActions() {
494
+ return this.$scopedSlots['row-actions'] || this.actions.length > 0
495
+ },
496
+
497
+ /** Whether all visible items are selected */
498
+ allSelected() {
499
+ if (this.objects.length === 0 || this.selectedIds.length === 0) return false
500
+ return this.objects.every((o) => this.selectedIds.includes(o[this.rowKey]))
501
+ },
502
+
503
+ /** Full objects for the selected IDs (used by mass action dialogs) */
504
+ selectedObjects() {
505
+ return this.objects.filter((o) => this.selectedIds.includes(o[this.rowKey]))
506
+ },
507
+
508
+ /** Column slot names that the parent has provided (for pass-through) */
509
+ slotColumns() {
510
+ return Object.keys(this.$scopedSlots)
511
+ .filter((name) => name.startsWith('column-'))
512
+ .map((name) => name.replace('column-', ''))
513
+ },
514
+ },
515
+
516
+ watch: {
517
+ viewMode(val) {
518
+ this.currentViewMode = val
519
+ },
520
+ },
521
+
522
+ methods: {
523
+ onViewModeChange(mode) {
524
+ this.currentViewMode = mode
525
+ this.$emit('view-mode-change', mode)
526
+ },
527
+
528
+ /**
529
+ * Handle mass delete confirm. Emits 'mass-delete' with the IDs.
530
+ * Parent should call `this.$refs.indexPage.setDeleteResult(...)` when done.
531
+ * @param {Array} ids Array of item IDs to delete
532
+ */
533
+ onMassDeleteConfirm(ids) {
534
+ this.$emit('mass-delete', ids)
535
+ },
536
+
537
+ /**
538
+ * Handle mass copy confirm. Emits 'mass-copy' with the payload.
539
+ * Parent should call `this.$refs.indexPage.setCopyResult(...)` when done.
540
+ * @param {{ ids: Array, getName: Function }} payload
541
+ */
542
+ onMassCopyConfirm(payload) {
543
+ this.$emit('mass-copy', payload)
544
+ },
545
+
546
+ /**
547
+ * Set the result of a mass delete operation. Call from parent after API call.
548
+ * @param {{ success?: boolean, error?: string }} resultData
549
+ * @public
550
+ */
551
+ setDeleteResult(resultData) {
552
+ if (this.$refs.deleteDialog) {
553
+ this.$refs.deleteDialog.setResult(resultData)
554
+ }
555
+ },
556
+
557
+ /**
558
+ * Set the result of a mass copy operation. Call from parent after API call.
559
+ * @param {{ success?: boolean, error?: string }} resultData
560
+ * @public
561
+ */
562
+ setCopyResult(resultData) {
563
+ if (this.$refs.copyDialog) {
564
+ this.$refs.copyDialog.setResult(resultData)
565
+ }
566
+ },
567
+
568
+ /**
569
+ * Handle mass export confirm.
570
+ * @param {{ format: string }} payload
571
+ */
572
+ onMassExportConfirm(payload) {
573
+ this.$emit('mass-export', payload)
574
+ },
575
+
576
+ /**
577
+ * Handle mass import confirm.
578
+ * @param {{ file: File, options: object }} payload
579
+ */
580
+ onMassImportConfirm(payload) {
581
+ this.$emit('mass-import', payload)
582
+ },
583
+
584
+ /**
585
+ * Set the result of a mass export operation.
586
+ * @param {{ success?: boolean, error?: string }} resultData
587
+ * @public
588
+ */
589
+ setExportResult(resultData) {
590
+ if (this.$refs.exportDialog) {
591
+ this.$refs.exportDialog.setResult(resultData)
592
+ }
593
+ },
594
+
595
+ /**
596
+ * Set the result of a mass import operation.
597
+ * @param {{ success?: boolean, error?: string, summary?: object }} resultData
598
+ * @public
599
+ */
600
+ setImportResult(resultData) {
601
+ if (this.$refs.importDialog) {
602
+ this.$refs.importDialog.setResult(resultData)
603
+ }
604
+ },
605
+ },
606
+ }
607
+ </script>
608
+
609
+ <style scoped>
610
+ .cn-index-page {
611
+ padding: 20px;
612
+ }
613
+
614
+ .cn-index-page__header {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: center;
618
+ margin-bottom: 16px;
619
+ flex-wrap: wrap;
620
+ gap: 12px;
621
+ }
622
+
623
+ .cn-index-page__title-area {
624
+ display: flex;
625
+ align-items: baseline;
626
+ gap: 8px;
627
+ }
628
+
629
+ .cn-index-page__title {
630
+ margin: 0;
631
+ font-size: 22px;
632
+ font-weight: 700;
633
+ }
634
+
635
+ .cn-index-page__count {
636
+ font-size: 14px;
637
+ color: var(--color-text-maxcontrast);
638
+ }
639
+
640
+ .cn-index-page__header-actions {
641
+ display: flex;
642
+ align-items: center;
643
+ gap: 8px;
644
+ }
645
+
646
+ .cn-index-page__body {
647
+ display: flex;
648
+ gap: 0;
649
+ }
650
+
651
+ .cn-index-page__body--with-sidebar {
652
+ gap: 0;
653
+ }
654
+
655
+ .cn-index-page__sidebar {
656
+ flex-shrink: 0;
657
+ }
658
+
659
+ .cn-index-page__main {
660
+ flex: 1;
661
+ min-width: 0;
662
+ }
663
+
664
+ .cn-index-page__search {
665
+ margin-bottom: 16px;
666
+ }
667
+
668
+ .cn-index-page__loading {
669
+ display: flex;
670
+ justify-content: center;
671
+ padding: 60px;
672
+ }
673
+
674
+ .cn-index-page__empty {
675
+ padding: 40px 20px;
676
+ text-align: center;
677
+ }
678
+
679
+ .cn-index-page__pagination {
680
+ margin-top: 16px;
681
+ }
682
+ </style>
@@ -0,0 +1 @@
1
+ export { default as CnIndexPage } from './CnIndexPage.vue'