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

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 (153) hide show
  1. package/README.md +226 -226
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +60455 -8755
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +2062 -528
  6. package/dist/nextcloud-vue.esm.js +60411 -8731
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +75 -61
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  10. package/src/components/CnActionsBar/index.js +1 -1
  11. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  12. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  13. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  16. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  17. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  18. package/src/components/CnCardGrid/index.js +1 -1
  19. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  24. package/src/components/CnConfigurationCard/index.js +1 -1
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  32. package/src/components/CnDetailCard/index.js +1 -0
  33. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  34. package/src/components/CnDetailPage/index.js +1 -0
  35. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  36. package/src/components/CnFacetSidebar/index.js +1 -1
  37. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnIcon/CnIcon.vue +89 -89
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  48. package/src/components/CnKpiGrid/index.js +1 -1
  49. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  50. package/src/components/CnMassActionBar/index.js +1 -1
  51. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  52. package/src/components/CnMassCopyDialog/index.js +1 -1
  53. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  54. package/src/components/CnMassDeleteDialog/index.js +1 -1
  55. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  56. package/src/components/CnMassExportDialog/index.js +1 -1
  57. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  58. package/src/components/CnMassImportDialog/index.js +1 -1
  59. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  60. package/src/components/CnNoteCard/index.js +1 -0
  61. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  62. package/src/components/CnNotesCard/index.js +1 -0
  63. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  64. package/src/components/CnObjectCard/index.js +1 -1
  65. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  66. package/src/components/CnObjectSidebar/index.js +1 -0
  67. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  68. package/src/components/CnPageHeader/index.js +1 -1
  69. package/src/components/CnPagination/CnPagination.vue +252 -252
  70. package/src/components/CnPagination/index.js +1 -1
  71. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  72. package/src/components/CnRowActions/index.js +1 -1
  73. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  77. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  78. package/src/components/CnSchemaFormDialog/index.js +1 -0
  79. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  80. package/src/components/CnSettingsCard/index.js +1 -1
  81. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  82. package/src/components/CnSettingsSection/index.js +1 -1
  83. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  84. package/src/components/CnStatsBlock/index.js +1 -1
  85. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  86. package/src/components/CnStatusBadge/index.js +1 -1
  87. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  88. package/src/components/CnTabbedFormDialog/index.js +1 -0
  89. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  90. package/src/components/CnTasksCard/index.js +1 -0
  91. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  92. package/src/components/CnTileWidget/index.js +1 -0
  93. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  94. package/src/components/CnTimelineStages/index.js +1 -0
  95. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  96. package/src/components/CnUserActionMenu/index.js +1 -0
  97. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  98. package/src/components/CnVersionInfoCard/index.js +1 -1
  99. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  100. package/src/components/CnWidgetRenderer/index.js +1 -0
  101. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  102. package/src/components/CnWidgetWrapper/index.js +1 -0
  103. package/src/components/index.js +43 -29
  104. package/src/composables/index.js +4 -3
  105. package/src/composables/useDashboardView.js +240 -0
  106. package/src/composables/useDetailView.js +289 -132
  107. package/src/composables/useListView.js +363 -153
  108. package/src/composables/useSubResource.js +142 -142
  109. package/src/constants/metadata.js +30 -30
  110. package/src/css/CnSchemaFormDialog.css +546 -0
  111. package/src/css/__sample_nextcloud_tokens.css +110 -0
  112. package/src/css/actions-bar.css +48 -48
  113. package/src/css/badge.css +51 -51
  114. package/src/css/card.css +128 -128
  115. package/src/css/dashboard.css +70 -0
  116. package/src/css/detail-page.css +168 -0
  117. package/src/css/detail.css +68 -68
  118. package/src/css/index-page.css +44 -32
  119. package/src/css/index-sidebar.css +193 -187
  120. package/src/css/index.css +16 -12
  121. package/src/css/layout.css +90 -90
  122. package/src/css/page-header.css +33 -33
  123. package/src/css/pagination.css +72 -72
  124. package/src/css/table.css +142 -142
  125. package/src/css/timeline-stages.css +218 -0
  126. package/src/css/utilities.css +46 -46
  127. package/src/index.js +72 -53
  128. package/src/store/createSubResourcePlugin.js +135 -135
  129. package/src/store/index.js +3 -3
  130. package/src/store/plugins/auditTrails.js +17 -17
  131. package/src/store/plugins/files.js +250 -186
  132. package/src/store/plugins/index.js +7 -5
  133. package/src/store/plugins/lifecycle.js +180 -180
  134. package/src/store/plugins/relations.js +68 -68
  135. package/src/store/plugins/search.js +372 -0
  136. package/src/store/plugins/selection.js +104 -0
  137. package/src/store/useObjectStore.js +829 -686
  138. package/src/types/auditTrail.d.ts +32 -32
  139. package/src/types/file.d.ts +23 -23
  140. package/src/types/index.d.ts +35 -35
  141. package/src/types/notification.d.ts +36 -36
  142. package/src/types/object.d.ts +40 -40
  143. package/src/types/organisation.d.ts +41 -41
  144. package/src/types/register.d.ts +25 -25
  145. package/src/types/schema.d.ts +39 -39
  146. package/src/types/shared.d.ts +79 -79
  147. package/src/types/source.d.ts +14 -14
  148. package/src/types/task.d.ts +31 -31
  149. package/src/utils/errors.js +96 -96
  150. package/src/utils/headers.js +68 -50
  151. package/src/utils/id.js +13 -0
  152. package/src/utils/index.js +3 -3
  153. package/src/utils/schema.js +422 -419
@@ -1,816 +1,874 @@
1
- <template>
2
- <div class="cn-index-page">
3
- <!-- Header (hidden by default — shown in sidebar instead) -->
4
- <CnPageHeader
5
- v-if="showTitle"
6
- :title="title"
7
- :description="description"
8
- :icon="resolvedIcon" />
9
-
10
- <!-- Actions bar -->
11
- <CnActionsBar
12
- :pagination="pagination"
13
- :object-count="objects.length"
14
- :selectable="selectable"
15
- :selected-ids="internalSelectedIds"
16
- :add-label="resolvedAddLabel"
17
- :add-icon="resolvedIcon"
18
- :inline-action-count="inlineActionCount"
19
- :show-mass-import="showMassImport"
20
- :show-mass-export="showMassExport"
21
- :show-mass-copy="showMassCopy"
22
- :show-mass-delete="showMassDelete"
23
- :view-mode="currentViewMode"
24
- :show-view-toggle="showViewToggle"
25
- @add="onAddClick"
26
- @refresh="$emit('refresh')"
27
- @show-import="showImportDialog = true"
28
- @show-export="showExportDialog = true"
29
- @show-copy="showMassCopyDialog = true"
30
- @show-delete="showMassDeleteDialog = true"
31
- @view-mode-change="onViewModeChange">
32
- <template v-if="$scopedSlots['mass-actions']" #mass-actions="{ count, selectedIds: ids }">
33
- <slot name="mass-actions" :count="count" :selected-ids="ids" />
34
- </template>
35
- <template v-if="$scopedSlots['action-items']" #action-items>
36
- <slot name="action-items" />
37
- </template>
38
- <template v-if="$scopedSlots['header-actions']" #header-actions>
39
- <slot name="header-actions" />
40
- </template>
41
- </CnActionsBar>
42
-
43
- <!-- Mass delete dialog -->
44
- <CnMassDeleteDialog
45
- v-if="showMassDeleteDialog"
46
- ref="massDeleteDialog"
47
- :items="selectedObjects"
48
- :name-field="massActionNameField"
49
- @confirm="onMassDeleteConfirm"
50
- @close="showMassDeleteDialog = false" />
51
-
52
- <!-- Mass copy dialog -->
53
- <CnMassCopyDialog
54
- v-if="showMassCopyDialog"
55
- ref="massCopyDialog"
56
- :items="selectedObjects"
57
- :name-field="massActionNameField"
58
- @confirm="onMassCopyConfirm"
59
- @close="showMassCopyDialog = false" />
60
-
61
- <!-- Mass export dialog -->
62
- <CnMassExportDialog
63
- v-if="showExportDialog"
64
- ref="exportDialog"
65
- :formats="exportFormats"
66
- @confirm="onMassExportConfirm"
67
- @close="showExportDialog = false" />
68
-
69
- <!-- Mass import dialog -->
70
- <CnMassImportDialog
71
- v-if="showImportDialog"
72
- ref="importDialog"
73
- :options="importOptions"
74
- @confirm="onMassImportConfirm"
75
- @close="showImportDialog = false">
76
- <template v-if="$scopedSlots['import-fields']" #fields="{ file }">
77
- <slot name="import-fields" :file="file" />
78
- </template>
79
- </CnMassImportDialog>
80
-
81
- <!-- Single delete dialog (overridable via slot) -->
82
- <slot
83
- name="delete-dialog"
84
- :item="actionTargetItem"
85
- :close="closeSingleDelete">
86
- <CnDeleteDialog
87
- v-if="showSingleDeleteDialog && actionTargetItem"
88
- ref="singleDeleteDialog"
89
- :item="actionTargetItem"
90
- :name-field="massActionNameField"
91
- @confirm="onSingleDeleteConfirm"
92
- @close="closeSingleDelete" />
93
- </slot>
94
-
95
- <!-- Single copy dialog (overridable via slot) -->
96
- <slot
97
- name="copy-dialog"
98
- :item="actionTargetItem"
99
- :close="closeSingleCopy">
100
- <CnCopyDialog
101
- v-if="showSingleCopyDialog && actionTargetItem"
102
- ref="singleCopyDialog"
103
- :item="actionTargetItem"
104
- :name-field="massActionNameField"
105
- @confirm="onSingleCopyConfirm"
106
- @close="closeSingleCopy" />
107
- </slot>
108
-
109
- <!-- Form dialog for create/edit (overridable via slot) -->
110
- <slot
111
- name="form-dialog"
112
- :item="editItem"
113
- :schema="schema"
114
- :close="closeFormDialog">
115
- <CnFormDialog
116
- v-if="showFormDialogVisible"
117
- ref="formDialog"
118
- :schema="schema"
119
- :item="editItem"
120
- :exclude-fields="excludeFields"
121
- :include-fields="includeFields"
122
- :field-overrides="fieldOverrides"
123
- :name-field="massActionNameField"
124
- @confirm="onFormConfirm"
125
- @close="closeFormDialog">
126
- <template v-if="$scopedSlots['form-fields']" #form="scope">
127
- <slot name="form-fields" v-bind="scope" />
128
- </template>
129
- </CnFormDialog>
130
- </slot>
131
-
132
- <!-- Body -->
133
- <div class="cn-index-page__body">
134
- <div class="cn-index-page__main">
135
- <!-- Loading state -->
136
- <div v-if="loading" class="cn-index-page__loading">
137
- <NcLoadingIcon :size="32" />
138
- </div>
139
-
140
- <!-- Empty state -->
141
- <div v-else-if="objects.length === 0" class="cn-index-page__empty">
142
- <slot name="empty">
143
- <NcEmptyContent :name="emptyText">
144
- <template #icon>
145
- <CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
146
- <DatabaseSearch v-else :size="64" />
147
- </template>
148
- </NcEmptyContent>
149
- </slot>
150
- </div>
151
-
152
- <!-- Table view -->
153
- <CnDataTable
154
- v-else-if="currentViewMode === 'table'"
155
- :schema="schema"
156
- :columns="columns"
157
- :rows="objects"
158
- :sort-key="sortKey"
159
- :sort-order="sortOrder"
160
- :selectable="selectable"
161
- :selected-ids="internalSelectedIds"
162
- :row-key="rowKey"
163
- :empty-text="emptyText"
164
- :exclude-columns="excludeColumns"
165
- :include-columns="includeColumns"
166
- :column-overrides="columnOverrides"
167
- :row-class="rowClass"
168
- @sort="$emit('sort', $event)"
169
- @select="onSelect"
170
- @row-click="$emit('row-click', $event)">
171
- <!-- Pass through column slots -->
172
- <template
173
- v-for="col in slotColumns"
174
- #[`column-${col}`]="{ row, value }">
175
- <slot :name="'column-' + col" :row="row" :value="value" />
176
- </template>
177
-
178
- <!-- Row actions -->
179
- <template v-if="hasRowActions" #row-actions="{ row }">
180
- <slot name="row-actions" :row="row">
181
- <CnRowActions
182
- :actions="mergedActions"
183
- :row="row"
184
- @action="$emit('action', $event)" />
185
- </slot>
186
- </template>
187
- </CnDataTable>
188
-
189
- <!-- Card view -->
190
- <CnCardGrid
191
- v-else
192
- :objects="objects"
193
- :schema="schema"
194
- :selectable="selectable"
195
- :selected-ids="internalSelectedIds"
196
- :row-key="rowKey"
197
- :empty-text="emptyText"
198
- @click="$emit('row-click', $event)"
199
- @select="onSelect">
200
- <template v-if="$scopedSlots.card" #card="{ object, selected }">
201
- <slot name="card" :object="object" :selected="selected" />
202
- </template>
203
- <template v-if="hasRowActions" #card-actions="{ object }">
204
- <slot name="row-actions" :row="object">
205
- <CnRowActions
206
- :actions="mergedActions"
207
- :row="object"
208
- @action="$emit('action', $event)" />
209
- </slot>
210
- </template>
211
- </CnCardGrid>
212
-
213
- <!-- Pagination -->
214
- <CnPagination
215
- v-if="pagination && pagination.pages > 1"
216
- :current-page="pagination.page || 1"
217
- :total-pages="pagination.pages || 1"
218
- :total-items="pagination.total || 0"
219
- :current-page-size="pagination.limit || 20"
220
- class="cn-index-page__pagination"
221
- @page-changed="$emit('page-changed', $event)"
222
- @page-size-changed="$emit('page-size-changed', $event)" />
223
- </div>
224
- </div>
225
- </div>
226
- </template>
227
-
228
- <script>
229
- import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
230
- import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
231
- import Eye from 'vue-material-design-icons/Eye.vue'
232
- import Pencil from 'vue-material-design-icons/Pencil.vue'
233
- import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
234
- import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
235
- import { CnPageHeader } from '../CnPageHeader/index.js'
236
- import { CnActionsBar } from '../CnActionsBar/index.js'
237
- import { CnIcon, ICON_MAP } from '../CnIcon/index.js'
238
- import { CnDataTable } from '../CnDataTable/index.js'
239
- import { CnCardGrid } from '../CnCardGrid/index.js'
240
- import { CnPagination } from '../CnPagination/index.js'
241
- import { CnRowActions } from '../CnRowActions/index.js'
242
- import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
243
- import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
244
- import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
245
- import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
246
- import { CnDeleteDialog } from '../CnDeleteDialog/index.js'
247
- import { CnCopyDialog } from '../CnCopyDialog/index.js'
248
- import { CnFormDialog } from '../CnFormDialog/index.js'
249
-
250
- /**
251
- * CnIndexPage — Top-level schema-driven index page component.
252
- *
253
- * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,
254
- * pagination, mass actions, single-object dialogs) into a single
255
- * zero-config page.
256
- *
257
- * Dialogs are overridable via named slots:
258
- * - `#form-dialog` Replace the create/edit dialog entirely
259
- * - `#delete-dialog` Replace the single-item delete dialog
260
- * - `#copy-dialog` Replace the single-item copy dialog
261
- * - `#form-fields` Replace only the form content inside the built-in form dialog
262
- *
263
- * @example Minimal usage (auto-generated dialogs from schema)
264
- * <CnIndexPage
265
- * title="Clients"
266
- * :schema="schema"
267
- * :objects="clients"
268
- * :pagination="pagination"
269
- * :loading="loading"
270
- * @create="onCreate"
271
- * @edit="onEdit"
272
- * @delete="onDelete"
273
- * @refresh="fetchClients"
274
- * @row-click="openClient"
275
- * @page-changed="onPage" />
276
- *
277
- * @example With custom form dialog
278
- * <CnIndexPage ...>
279
- * <template #form-dialog="{ item, schema, close }">
280
- * <MyCustomFormDialog :item="item" @close="close" />
281
- * </template>
282
- * </CnIndexPage>
283
- *
284
- * @event {void} add — Add button clicked (backward compat, only if listener attached)
285
- * @event {object} create — Form dialog create confirmed. Payload: formData object
286
- * @event {object} edit — Form dialog edit confirmed. Payload: formData object (includes id)
287
- * @event {string} delete — Single delete confirmed. Payload: item ID
288
- * @event {{ id: string, newName: string }} copy — Single copy confirmed
289
- * @event {string[]} mass-delete — Mass delete confirmed. Payload: array of IDs
290
- * @event {object} mass-copy — Mass copy confirmed. Payload: { ids, pattern }
291
- * @event {object} mass-export — Mass export confirmed. Payload: { ids, format }
292
- * @event {object} mass-import — Mass import confirmed. Payload: import data
293
- * @event {void} refresh — Refresh button clicked
294
- * @event {object} row-click — Table row or card clicked. Payload: row object
295
- * @event {{ key: string, order: string }} sort — Column sort changed
296
- * @event {number} page-changed — Pagination page changed
297
- * @event {number} page-size-changed Pagination page size changed
298
- * @event {string[]} select — Selection changed. Payload: array of selected IDs
299
- * @event {object} action — Row action triggered. Payload: { action, row }
300
- *
301
- * @slot mass-actions — Extra mass action buttons (shown when items are selected)
302
- * @slot action-items — Extra action bar buttons
303
- * @slot header-actions — Extra buttons in the page header
304
- * @slot delete-dialogReplace the single-item delete dialog. Scope: `{ item, close }`
305
- * @slot copy-dialogReplace the single-item copy dialog. Scope: `{ item, close }`
306
- * @slot form-dialogReplace the create/edit form dialog. Scope: `{ item, schema, close }`
307
- * @slot form-fieldsReplace form content inside the built-in CnFormDialog. Scope: `{ fields, formData, errors, updateField }`
308
- * @slot import-fields Extra fields in the import dialog
309
- * @slot emptyCustom empty state content
310
- * @slot cardCustom card template for card view. Scope: `{ row }`
311
- * @slot row-actionsCustom row actions. Scope: `{ row }`
312
- * @slot column-{key} — Custom cell renderer for a specific column. Scope: `{ row, value }`
313
- */
314
- export default {
315
- name: 'CnIndexPage',
316
-
317
- components: {
318
- NcLoadingIcon,
319
- NcEmptyContent,
320
- DatabaseSearch,
321
- CnPageHeader,
322
- CnActionsBar,
323
- CnIcon,
324
- CnDataTable,
325
- CnCardGrid,
326
- CnPagination,
327
- CnRowActions,
328
- CnMassDeleteDialog,
329
- CnMassCopyDialog,
330
- CnMassExportDialog,
331
- CnMassImportDialog,
332
- CnDeleteDialog,
333
- CnCopyDialog,
334
- CnFormDialog,
335
- },
336
-
337
- props: {
338
- /** Page title */
339
- title: {
340
- type: String,
341
- required: true,
342
- },
343
- /** Optional description shown below the title */
344
- description: {
345
- type: String,
346
- default: '',
347
- },
348
- /**
349
- * Whether to show the page header (icon, title, description) inline.
350
- * When false (default), the title is shown in the sidebar header instead.
351
- */
352
- showTitle: {
353
- type: Boolean,
354
- default: false,
355
- },
356
- /** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
357
- icon: {
358
- type: String,
359
- default: '',
360
- },
361
- /** Schema definition */
362
- schema: {
363
- type: Object,
364
- default: null,
365
- },
366
- /** Manual column definitions (used instead of schema when provided) */
367
- columns: {
368
- type: Array,
369
- default: () => [],
370
- },
371
- /** Object/row data array */
372
- objects: {
373
- type: Array,
374
- default: () => [],
375
- },
376
- /** Pagination state: { page, pages, total, limit } */
377
- pagination: {
378
- type: Object,
379
- default: null,
380
- },
381
- /** Whether data is loading */
382
- loading: {
383
- type: Boolean,
384
- default: false,
385
- },
386
- /** Whether rows/cards can be selected */
387
- selectable: {
388
- type: Boolean,
389
- default: true,
390
- },
391
- /** Currently selected IDs */
392
- selectedIds: {
393
- type: Array,
394
- default: () => [],
395
- },
396
- /** View mode: 'table' or 'cards' */
397
- viewMode: {
398
- type: String,
399
- default: 'table',
400
- validator: (v) => ['table', 'cards'].includes(v),
401
- },
402
- /** Current sort key */
403
- sortKey: {
404
- type: String,
405
- default: null,
406
- },
407
- /** Current sort order */
408
- sortOrder: {
409
- type: String,
410
- default: 'asc',
411
- },
412
- /** Unique row identifier property */
413
- rowKey: {
414
- type: String,
415
- default: 'id',
416
- },
417
- /** Columns to exclude in schema mode */
418
- excludeColumns: {
419
- type: Array,
420
- default: () => [],
421
- },
422
- /** Columns to include in schema mode (whitelist) */
423
- includeColumns: {
424
- type: Array,
425
- default: null,
426
- },
427
- /** Per-column overrides in schema mode */
428
- columnOverrides: {
429
- type: Object,
430
- default: () => ({}),
431
- },
432
- /** Row action definitions (app-provided, merged with built-in actions) */
433
- actions: {
434
- type: Array,
435
- default: () => [],
436
- },
437
- /** Text shown when no items found */
438
- emptyText: {
439
- type: String,
440
- default: 'No items found',
441
- },
442
- /** Function returning CSS class(es) for a row */
443
- rowClass: {
444
- type: Function,
445
- default: null,
446
- },
447
- /** Override label for the Add button. Defaults to "Add {schema.title}" */
448
- addLabel: {
449
- type: String,
450
- default: '',
451
- },
452
- /** How many action buttons to show inline (rest go in overflow dropdown) */
453
- inlineActionCount: {
454
- type: Number,
455
- default: 2,
456
- },
457
- /** Whether to show the built-in mass Import action */
458
- showMassImport: {
459
- type: Boolean,
460
- default: true,
461
- },
462
- /** Whether to show the built-in mass Export action */
463
- showMassExport: {
464
- type: Boolean,
465
- default: true,
466
- },
467
- /** Whether to show the built-in mass Copy button */
468
- showMassCopy: {
469
- type: Boolean,
470
- default: true,
471
- },
472
- /** Whether to show the built-in mass Delete button */
473
- showMassDelete: {
474
- type: Boolean,
475
- default: true,
476
- },
477
- /** Property name used to display item names in dialogs */
478
- massActionNameField: {
479
- type: String,
480
- default: 'title',
481
- },
482
- /** Available export formats for the export dialog */
483
- exportFormats: {
484
- type: Array,
485
- default: () => [
486
- { id: 'excel', label: 'Excel (.xlsx)' },
487
- { id: 'csv', label: 'CSV (.csv)' },
488
- ],
489
- },
490
- /** Import option definitions for the import dialog */
491
- importOptions: {
492
- type: Array,
493
- default: () => [],
494
- },
495
- /** Whether to show the built-in form dialog for Add/Edit */
496
- showFormDialog: {
497
- type: Boolean,
498
- default: true,
499
- },
500
- /** Whether to add an Edit action to row actions */
501
- showEditAction: {
502
- type: Boolean,
503
- default: true,
504
- },
505
- /** Whether to add a Copy action to row actions */
506
- showCopyAction: {
507
- type: Boolean,
508
- default: true,
509
- },
510
- /** Whether to add a Delete action to row actions */
511
- showDeleteAction: {
512
- type: Boolean,
513
- default: true,
514
- },
515
- /** Field keys to exclude from the form dialog */
516
- excludeFields: {
517
- type: Array,
518
- default: () => [],
519
- },
520
- /** Field keys to include in the form dialog (whitelist mode) */
521
- includeFields: {
522
- type: Array,
523
- default: null,
524
- },
525
- /** Per-field overrides passed to CnFormDialog */
526
- fieldOverrides: {
527
- type: Object,
528
- default: () => ({}),
529
- },
530
- /** Whether to show the Cards/Table view toggle in the actions bar */
531
- showViewToggle: {
532
- type: Boolean,
533
- default: true,
534
- },
535
- },
536
-
537
- data() {
538
- return {
539
- currentViewMode: this.viewMode,
540
- internalSelectedIds: [...this.selectedIds],
541
- // Mass action dialogs
542
- showMassDeleteDialog: false,
543
- showMassCopyDialog: false,
544
- showExportDialog: false,
545
- showImportDialog: false,
546
- // Single-object dialogs
547
- showSingleDeleteDialog: false,
548
- showSingleCopyDialog: false,
549
- showFormDialogVisible: false,
550
- // Dialog targets
551
- actionTargetItem: null,
552
- editItem: null,
553
- }
554
- },
555
-
556
- computed: {
557
- /** Resolved icon — explicit prop overrides schema.icon */
558
- resolvedIcon() {
559
- if (this.icon) return this.icon
560
- return this.schema?.icon || ''
561
- },
562
-
563
- /** Resolved schema icon component for View action */
564
- schemaIconComponent() {
565
- if (this.resolvedIcon && ICON_MAP[this.resolvedIcon]) {
566
- return ICON_MAP[this.resolvedIcon]
567
- }
568
- return Eye
569
- },
570
-
571
- /** Built-in row actions based on show*Action props */
572
- defaultActions() {
573
- const builtIn = []
574
- if (this.$listeners && this.$listeners['row-click']) {
575
- builtIn.push({
576
- label: 'View',
577
- icon: this.schemaIconComponent,
578
- handler: (row) => {
579
- this.$emit('row-click', row)
580
- },
581
- })
582
- }
583
- if (this.showEditAction) {
584
- builtIn.push({
585
- label: 'Edit',
586
- icon: Pencil,
587
- handler: (row) => {
588
- this.editItem = row
589
- this.showFormDialogVisible = true
590
- },
591
- })
592
- }
593
- if (this.showCopyAction) {
594
- builtIn.push({
595
- label: 'Copy',
596
- icon: ContentCopy,
597
- handler: (row) => {
598
- this.actionTargetItem = row
599
- this.showSingleCopyDialog = true
600
- },
601
- })
602
- }
603
- if (this.showDeleteAction) {
604
- builtIn.push({
605
- label: 'Delete',
606
- icon: TrashCanOutline,
607
- destructive: true,
608
- handler: (row) => {
609
- this.actionTargetItem = row
610
- this.showSingleDeleteDialog = true
611
- },
612
- })
613
- }
614
- return builtIn
615
- },
616
-
617
- /** Merged actions: app-provided first, then built-in defaults */
618
- mergedActions() {
619
- return [...this.actions, ...this.defaultActions]
620
- },
621
-
622
- hasRowActions() {
623
- return this.$scopedSlots['row-actions'] || this.mergedActions.length > 0
624
- },
625
-
626
- /** Whether all visible items are selected */
627
- allSelected() {
628
- if (this.objects.length === 0 || this.internalSelectedIds.length === 0) return false
629
- return this.objects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))
630
- },
631
-
632
- /** Full objects for the selected IDs (used by mass action dialogs) */
633
- selectedObjects() {
634
- return this.objects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))
635
- },
636
-
637
- /** Column slot names that the parent has provided (for pass-through) */
638
- slotColumns() {
639
- return Object.keys(this.$scopedSlots)
640
- .filter((name) => name.startsWith('column-'))
641
- .map((name) => name.replace('column-', ''))
642
- },
643
-
644
- /** Add button label — derived from schema.title if not explicitly set */
645
- resolvedAddLabel() {
646
- if (this.addLabel) return this.addLabel
647
- return 'Add ' + (this.schema?.title || 'Item')
648
- },
649
- },
650
-
651
- watch: {
652
- viewMode(val) {
653
- this.currentViewMode = val
654
- },
655
- selectedIds(val) {
656
- this.internalSelectedIds = [...val]
657
- },
658
- },
659
-
660
- methods: {
661
- /**
662
- * Handle the Add button click. If the consumer listens to @add,
663
- * emit the event (backward compatible). Otherwise open the form dialog.
664
- */
665
- onAddClick() {
666
- if (this.$listeners && this.$listeners.add) {
667
- this.$emit('add')
668
- } else if (this.showFormDialog) {
669
- this.editItem = null
670
- this.showFormDialogVisible = true
671
- }
672
- },
673
-
674
- /**
675
- * Handle view mode toggle.
676
- * @param {string} mode 'table' or 'cards'
677
- */
678
- onViewModeChange(mode) {
679
- this.currentViewMode = mode
680
- this.$emit('view-mode-change', mode)
681
- },
682
-
683
- /**
684
- * Handle selection changes from CnDataTable/CnCardGrid.
685
- * Updates internal state and re-emits for parent.
686
- * @param {Array} ids Array of selected row IDs
687
- */
688
- onSelect(ids) {
689
- this.internalSelectedIds = ids
690
- this.$emit('select', ids)
691
- },
692
-
693
- // --- Mass action handlers ---
694
-
695
- onMassDeleteConfirm(ids) {
696
- this.$emit('mass-delete', ids)
697
- },
698
-
699
- onMassCopyConfirm(payload) {
700
- this.$emit('mass-copy', payload)
701
- },
702
-
703
- onMassExportConfirm(payload) {
704
- this.$emit('mass-export', payload)
705
- },
706
-
707
- onMassImportConfirm(payload) {
708
- this.$emit('mass-import', payload)
709
- },
710
-
711
- /** @public Forward result to mass delete dialog */
712
- setMassDeleteResult(resultData) {
713
- if (this.$refs.massDeleteDialog) {
714
- this.$refs.massDeleteDialog.setResult(resultData)
715
- }
716
- },
717
-
718
- /** @public Forward result to mass copy dialog */
719
- setMassCopyResult(resultData) {
720
- if (this.$refs.massCopyDialog) {
721
- this.$refs.massCopyDialog.setResult(resultData)
722
- }
723
- },
724
-
725
- /** @public Forward result to export dialog */
726
- setExportResult(resultData) {
727
- if (this.$refs.exportDialog) {
728
- this.$refs.exportDialog.setResult(resultData)
729
- }
730
- },
731
-
732
- /** @public Forward result to import dialog */
733
- setImportResult(resultData) {
734
- if (this.$refs.importDialog) {
735
- this.$refs.importDialog.setResult(resultData)
736
- }
737
- },
738
-
739
- // --- Backward-compatible aliases ---
740
- /** @public @deprecated Use setMassDeleteResult instead */
741
- setDeleteResult(resultData) {
742
- this.setMassDeleteResult(resultData)
743
- },
744
- /** @public @deprecated Use setMassCopyResult instead */
745
- setCopyResult(resultData) {
746
- this.setMassCopyResult(resultData)
747
- },
748
-
749
- // --- Single-object dialog handlers ---
750
-
751
- onSingleDeleteConfirm(id) {
752
- this.$emit('delete', id)
753
- },
754
-
755
- onSingleCopyConfirm(payload) {
756
- this.$emit('copy', payload)
757
- },
758
-
759
- onFormConfirm(formData) {
760
- if (this.editItem) {
761
- this.$emit('edit', formData)
762
- } else {
763
- this.$emit('create', formData)
764
- }
765
- },
766
-
767
- closeSingleDelete() {
768
- this.showSingleDeleteDialog = false
769
- this.actionTargetItem = null
770
- },
771
-
772
- closeSingleCopy() {
773
- this.showSingleCopyDialog = false
774
- this.actionTargetItem = null
775
- },
776
-
777
- closeFormDialog() {
778
- this.showFormDialogVisible = false
779
- this.editItem = null
780
- },
781
-
782
- /** @public Forward result to single delete dialog */
783
- setSingleDeleteResult(resultData) {
784
- if (this.$refs.singleDeleteDialog) {
785
- this.$refs.singleDeleteDialog.setResult(resultData)
786
- }
787
- },
788
-
789
- /** @public Forward result to single copy dialog */
790
- setSingleCopyResult(resultData) {
791
- if (this.$refs.singleCopyDialog) {
792
- this.$refs.singleCopyDialog.setResult(resultData)
793
- }
794
- },
795
-
796
- /** @public Forward result to form dialog */
797
- setFormResult(resultData) {
798
- if (this.$refs.formDialog) {
799
- this.$refs.formDialog.setResult(resultData)
800
- }
801
- },
802
-
803
- /**
804
- * Programmatically open the form dialog.
805
- * @param {object|null} item Pass null for create mode, or an object for edit mode
806
- * @public
807
- */
808
- openFormDialog(item = null) {
809
- this.editItem = item
810
- this.showFormDialogVisible = true
811
- },
812
- },
813
- }
814
- </script>
815
-
816
- <!-- Styles in css/index-page.css -->
1
+ <template>
2
+ <div class="cn-index-page">
3
+ <!-- Header (hidden by default — shown in sidebar instead) -->
4
+ <CnPageHeader
5
+ v-if="showTitle"
6
+ :title="title"
7
+ :description="description"
8
+ :icon="resolvedIcon" />
9
+
10
+ <!-- Optional content below header, above actions bar -->
11
+ <div v-if="$scopedSlots['below-header']" class="cn-index-page__below-header">
12
+ <slot name="below-header" />
13
+ </div>
14
+
15
+ <!-- Actions bar -->
16
+ <CnActionsBar
17
+ :pagination="pagination"
18
+ :object-count="objects.length"
19
+ :selectable="selectable"
20
+ :selected-ids="internalSelectedIds"
21
+ :add-label="resolvedAddLabel"
22
+ :add-icon="resolvedIcon"
23
+ :inline-action-count="inlineActionCount"
24
+ :show-mass-import="showMassImport"
25
+ :show-mass-export="showMassExport"
26
+ :show-mass-copy="showMassCopy"
27
+ :show-mass-delete="showMassDelete"
28
+ :view-mode="currentViewMode"
29
+ :show-view-toggle="showViewToggle"
30
+ :refreshing="refreshing"
31
+ @add="onAddClick"
32
+ @refresh="$emit('refresh')"
33
+ @show-import="showImportDialog = true"
34
+ @show-export="showExportDialog = true"
35
+ @show-copy="showMassCopyDialog = true"
36
+ @show-delete="showMassDeleteDialog = true"
37
+ @view-mode-change="onViewModeChange">
38
+ <template v-if="$scopedSlots['mass-actions']" #mass-actions="{ count, selectedIds: ids }">
39
+ <slot name="mass-actions" :count="count" :selected-ids="ids" />
40
+ </template>
41
+ <template v-if="$scopedSlots['action-items']" #action-items>
42
+ <slot name="action-items" />
43
+ </template>
44
+ <template v-if="$scopedSlots['header-actions']" #header-actions>
45
+ <slot name="header-actions" />
46
+ </template>
47
+ </CnActionsBar>
48
+
49
+ <!-- Mass delete dialog -->
50
+ <CnMassDeleteDialog
51
+ v-if="showMassDeleteDialog"
52
+ ref="massDeleteDialog"
53
+ :items="selectedObjects"
54
+ :name-field="massActionNameField"
55
+ @confirm="onMassDeleteConfirm"
56
+ @close="showMassDeleteDialog = false" />
57
+
58
+ <!-- Mass copy dialog -->
59
+ <CnMassCopyDialog
60
+ v-if="showMassCopyDialog"
61
+ ref="massCopyDialog"
62
+ :items="selectedObjects"
63
+ :name-field="massActionNameField"
64
+ @confirm="onMassCopyConfirm"
65
+ @close="showMassCopyDialog = false" />
66
+
67
+ <!-- Mass export dialog -->
68
+ <CnMassExportDialog
69
+ v-if="showExportDialog"
70
+ ref="exportDialog"
71
+ :formats="exportFormats"
72
+ @confirm="onMassExportConfirm"
73
+ @close="showExportDialog = false" />
74
+
75
+ <!-- Mass import dialog -->
76
+ <CnMassImportDialog
77
+ v-if="showImportDialog"
78
+ ref="importDialog"
79
+ :options="importOptions"
80
+ @confirm="onMassImportConfirm"
81
+ @close="showImportDialog = false">
82
+ <template v-if="$scopedSlots['import-fields']" #fields="{ file }">
83
+ <slot name="import-fields" :file="file" />
84
+ </template>
85
+ </CnMassImportDialog>
86
+
87
+ <!-- Single delete dialog (overridable via slot) -->
88
+ <slot
89
+ name="delete-dialog"
90
+ :item="actionTargetItem"
91
+ :close="closeSingleDelete">
92
+ <CnDeleteDialog
93
+ v-if="showSingleDeleteDialog && actionTargetItem"
94
+ ref="singleDeleteDialog"
95
+ :item="actionTargetItem"
96
+ :name-field="massActionNameField"
97
+ @confirm="onSingleDeleteConfirm"
98
+ @close="closeSingleDelete" />
99
+ </slot>
100
+
101
+ <!-- Single copy dialog (overridable via slot) -->
102
+ <slot
103
+ name="copy-dialog"
104
+ :item="actionTargetItem"
105
+ :close="closeSingleCopy">
106
+ <CnCopyDialog
107
+ v-if="showSingleCopyDialog && actionTargetItem"
108
+ ref="singleCopyDialog"
109
+ :item="actionTargetItem"
110
+ :name-field="massActionNameField"
111
+ @confirm="onSingleCopyConfirm"
112
+ @close="closeSingleCopy" />
113
+ </slot>
114
+
115
+ <!-- Form dialog for create/edit (overridable via slot) -->
116
+ <slot
117
+ name="form-dialog"
118
+ :item="editItem"
119
+ :schema="schema"
120
+ :close="closeFormDialog">
121
+ <CnFormDialog
122
+ v-if="showFormDialogVisible && !useAdvancedFormDialog"
123
+ ref="formDialog"
124
+ :schema="schema"
125
+ :item="editItem"
126
+ :exclude-fields="excludeFields"
127
+ :include-fields="includeFields"
128
+ :field-overrides="fieldOverrides"
129
+ :name-field="massActionNameField"
130
+ @confirm="onFormConfirm"
131
+ @close="closeFormDialog">
132
+ <template v-if="$scopedSlots['form-fields']" #form="scope">
133
+ <slot name="form-fields" v-bind="scope" />
134
+ </template>
135
+ </CnFormDialog>
136
+ <CnAdvancedFormDialog
137
+ v-if="showFormDialogVisible && useAdvancedFormDialog"
138
+ ref="formDialog"
139
+ :schema="schema"
140
+ :item="editItem"
141
+ :exclude-fields="excludeFields"
142
+ :include-fields="includeFields"
143
+ :field-overrides="fieldOverrides"
144
+ :name-field="massActionNameField"
145
+ @confirm="onFormConfirm"
146
+ @close="closeFormDialog" />
147
+ </slot>
148
+
149
+ <!-- Body -->
150
+ <div class="cn-index-page__body">
151
+ <div class="cn-index-page__main">
152
+ <!-- Loading state -->
153
+ <div v-if="loading" class="cn-index-page__loading">
154
+ <NcLoadingIcon :size="32" />
155
+ </div>
156
+
157
+ <!-- Empty state -->
158
+ <div v-else-if="objects.length === 0" class="cn-index-page__empty">
159
+ <slot name="empty">
160
+ <NcEmptyContent :name="emptyText">
161
+ <template #icon>
162
+ <CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
163
+ <DatabaseSearch v-else :size="64" />
164
+ </template>
165
+ </NcEmptyContent>
166
+ </slot>
167
+ </div>
168
+
169
+ <!-- Table view -->
170
+ <CnDataTable
171
+ v-else-if="currentViewMode === 'table'"
172
+ :schema="schema"
173
+ :columns="columns"
174
+ :rows="objects"
175
+ :sort-key="sortKey"
176
+ :sort-order="sortOrder"
177
+ :selectable="selectable"
178
+ :selected-ids="internalSelectedIds"
179
+ :row-key="rowKey"
180
+ :empty-text="emptyText"
181
+ :exclude-columns="excludeColumns"
182
+ :include-columns="includeColumns"
183
+ :column-overrides="columnOverrides"
184
+ :row-class="rowClass"
185
+ @sort="$emit('sort', $event)"
186
+ @select="onSelect"
187
+ @row-click="$emit('row-click', $event)">
188
+ <!-- Pass through column slots -->
189
+ <template
190
+ v-for="col in slotColumns"
191
+ #[`column-${col}`]="{ row, value }">
192
+ <slot :name="'column-' + col" :row="row" :value="value" />
193
+ </template>
194
+
195
+ <!-- Row actions -->
196
+ <template v-if="hasRowActions" #row-actions="{ row }">
197
+ <slot name="row-actions" :row="row">
198
+ <CnRowActions
199
+ :actions="mergedActions"
200
+ :row="row"
201
+ @action="$emit('action', $event)" />
202
+ </slot>
203
+ </template>
204
+ </CnDataTable>
205
+
206
+ <!-- Card view -->
207
+ <CnCardGrid
208
+ v-else
209
+ :objects="objects"
210
+ :schema="schema"
211
+ :selectable="selectable"
212
+ :selected-ids="internalSelectedIds"
213
+ :row-key="rowKey"
214
+ :empty-text="emptyText"
215
+ @click="$emit('row-click', $event)"
216
+ @select="onSelect">
217
+ <template v-if="$scopedSlots.card" #card="{ object, selected }">
218
+ <slot name="card" :object="object" :selected="selected" />
219
+ </template>
220
+ <template v-if="hasRowActions" #card-actions="{ object }">
221
+ <slot name="row-actions" :row="object">
222
+ <CnRowActions
223
+ :actions="mergedActions"
224
+ :row="object"
225
+ @action="$emit('action', $event)" />
226
+ </slot>
227
+ </template>
228
+ </CnCardGrid>
229
+
230
+ <!-- Pagination -->
231
+ <CnPagination
232
+ v-if="pagination && pagination.pages > 1"
233
+ :current-page="pagination.page || 1"
234
+ :total-pages="pagination.pages || 1"
235
+ :total-items="pagination.total || 0"
236
+ :current-page-size="pagination.limit || 20"
237
+ class="cn-index-page__pagination"
238
+ @page-changed="$emit('page-changed', $event)"
239
+ @page-size-changed="$emit('page-size-changed', $event)" />
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </template>
244
+
245
+ <script>
246
+ import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
247
+ import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
248
+ import Eye from 'vue-material-design-icons/Eye.vue'
249
+ import Pencil from 'vue-material-design-icons/Pencil.vue'
250
+ import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
251
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
252
+ import { CnPageHeader } from '../CnPageHeader/index.js'
253
+ import { CnActionsBar } from '../CnActionsBar/index.js'
254
+ import { CnIcon, ICON_MAP } from '../CnIcon/index.js'
255
+ import { CnDataTable } from '../CnDataTable/index.js'
256
+ import { CnCardGrid } from '../CnCardGrid/index.js'
257
+ import { CnPagination } from '../CnPagination/index.js'
258
+ import { CnRowActions } from '../CnRowActions/index.js'
259
+ import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
260
+ import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
261
+ import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
262
+ import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
263
+ import { CnDeleteDialog } from '../CnDeleteDialog/index.js'
264
+ import { CnCopyDialog } from '../CnCopyDialog/index.js'
265
+ import { CnFormDialog } from '../CnFormDialog/index.js'
266
+ import { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'
267
+
268
+ /**
269
+ * CnIndexPage — Top-level schema-driven index page component.
270
+ *
271
+ * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,
272
+ * pagination, mass actions, single-object dialogs) into a single
273
+ * zero-config page.
274
+ *
275
+ * Dialogs are overridable via named slots:
276
+ * - `#form-dialog` — Replace the create/edit dialog entirely
277
+ * - `#delete-dialog` Replace the single-item delete dialog
278
+ * - `#copy-dialog` — Replace the single-item copy dialog
279
+ * - `#form-fields` Replace only the form content inside the built-in form dialog (CnFormDialog only)
280
+ *
281
+ * Use the `useAdvancedFormDialog` prop to use CnAdvancedFormDialog for create/edit (properties table, JSON tab, optional metadata).
282
+ *
283
+ * @example Minimal usage (auto-generated dialogs from schema)
284
+ * <CnIndexPage
285
+ * title="Clients"
286
+ * :schema="schema"
287
+ * :objects="clients"
288
+ * :pagination="pagination"
289
+ * :loading="loading"
290
+ * @create="onCreate"
291
+ * @edit="onEdit"
292
+ * @delete="onDelete"
293
+ * @refresh="fetchClients"
294
+ * @row-click="openClient"
295
+ * @page-changed="onPage" />
296
+ *
297
+ * @example With custom form dialog
298
+ * <CnIndexPage ...>
299
+ * <template #form-dialog="{ item, schema, close }">
300
+ * <MyCustomFormDialog :item="item" @close="close" />
301
+ * </template>
302
+ * </CnIndexPage>
303
+ *
304
+ * @event {void} add Add button clicked (backward compat, only if listener attached)
305
+ * @event {object} create Form dialog create confirmed. Payload: formData object
306
+ * @event {object} edit Form dialog edit confirmed. Payload: formData object (includes id)
307
+ * @event {string} delete Single delete confirmed. Payload: item ID
308
+ * @event {{ id: string, newName: string }} copy — Single copy confirmed
309
+ * @event {string[]} mass-delete Mass delete confirmed. Payload: array of IDs
310
+ * @event {object} mass-copy Mass copy confirmed. Payload: { ids, pattern }
311
+ * @event {object} mass-exportMass export confirmed. Payload: { ids, format }
312
+ * @event {object} mass-import Mass import confirmed. Payload: import data
313
+ * @event {void} refresh — Refresh button clicked
314
+ * @event {object} row-click — Table row or card clicked. Payload: row object
315
+ * @event {{ key: string, order: string }} sort — Column sort changed
316
+ * @event {number} page-changed — Pagination page changed
317
+ * @event {number} page-size-changed — Pagination page size changed
318
+ * @event {string[]} select — Selection changed. Payload: array of selected IDs
319
+ * @event {object} action — Row action triggered. Payload: { action, row }
320
+ *
321
+ * @slot mass-actions — Extra mass action buttons (shown when items are selected)
322
+ * @slot action-items — Extra action bar buttons
323
+ * @slot header-actions — Extra buttons in the page header
324
+ * @slot delete-dialog — Replace the single-item delete dialog. Scope: `{ item, close }`
325
+ * @slot copy-dialog — Replace the single-item copy dialog. Scope: `{ item, close }`
326
+ * @slot form-dialog — Replace the create/edit form dialog. Scope: `{ item, schema, close }`
327
+ * @slot form-fields — Replace form content inside the built-in CnFormDialog. Scope: `{ fields, formData, errors, updateField }`
328
+ * @slot import-fields — Extra fields in the import dialog
329
+ * @slot empty — Custom empty state content
330
+ * @slot card — Custom card template for card view. Scope: `{ row }`
331
+ * @slot row-actions — Custom row actions. Scope: `{ row }`
332
+ * @slot column-{key} — Custom cell renderer for a specific column. Scope: `{ row, value }`
333
+ */
334
+ export default {
335
+ name: 'CnIndexPage',
336
+
337
+ components: {
338
+ NcLoadingIcon,
339
+ NcEmptyContent,
340
+ DatabaseSearch,
341
+ CnPageHeader,
342
+ CnActionsBar,
343
+ CnIcon,
344
+ CnDataTable,
345
+ CnCardGrid,
346
+ CnPagination,
347
+ CnRowActions,
348
+ CnMassDeleteDialog,
349
+ CnMassCopyDialog,
350
+ CnMassExportDialog,
351
+ CnMassImportDialog,
352
+ CnDeleteDialog,
353
+ CnCopyDialog,
354
+ CnFormDialog,
355
+ CnAdvancedFormDialog,
356
+ },
357
+
358
+ props: {
359
+ /** Page title */
360
+ title: {
361
+ type: String,
362
+ required: true,
363
+ },
364
+ /** Optional description shown below the title */
365
+ description: {
366
+ type: String,
367
+ default: '',
368
+ },
369
+ /**
370
+ * Whether to show the page header (icon, title, description) inline.
371
+ * When false (default), the title is shown in the sidebar header instead.
372
+ */
373
+ showTitle: {
374
+ type: Boolean,
375
+ default: false,
376
+ },
377
+ /** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
378
+ icon: {
379
+ type: String,
380
+ default: '',
381
+ },
382
+ /** Schema definition */
383
+ schema: {
384
+ type: Object,
385
+ default: null,
386
+ },
387
+ /** Manual column definitions (used instead of schema when provided) */
388
+ columns: {
389
+ type: Array,
390
+ default: () => [],
391
+ },
392
+ /** Object/row data array */
393
+ objects: {
394
+ type: Array,
395
+ default: () => [],
396
+ },
397
+ /** Pagination state: { page, pages, total, limit } */
398
+ pagination: {
399
+ type: Object,
400
+ default: null,
401
+ },
402
+ /** Whether data is loading */
403
+ loading: {
404
+ type: Boolean,
405
+ default: false,
406
+ },
407
+ /** Whether rows/cards can be selected */
408
+ selectable: {
409
+ type: Boolean,
410
+ default: true,
411
+ },
412
+ /** Currently selected IDs */
413
+ selectedIds: {
414
+ type: Array,
415
+ default: () => [],
416
+ },
417
+ /** View mode: 'table' or 'cards' */
418
+ viewMode: {
419
+ type: String,
420
+ default: 'table',
421
+ validator: (v) => ['table', 'cards'].includes(v),
422
+ },
423
+ /** Current sort key */
424
+ sortKey: {
425
+ type: String,
426
+ default: null,
427
+ },
428
+ /** Current sort order */
429
+ sortOrder: {
430
+ type: String,
431
+ default: 'asc',
432
+ },
433
+ /** Unique row identifier property */
434
+ rowKey: {
435
+ type: String,
436
+ default: 'id',
437
+ },
438
+ /** Columns to exclude in schema mode */
439
+ excludeColumns: {
440
+ type: Array,
441
+ default: () => [],
442
+ },
443
+ /** Columns to include in schema mode (whitelist) */
444
+ includeColumns: {
445
+ type: Array,
446
+ default: null,
447
+ },
448
+ /** Per-column overrides in schema mode */
449
+ columnOverrides: {
450
+ type: Object,
451
+ default: () => ({}),
452
+ },
453
+ /** Row action definitions (app-provided, merged with built-in actions) */
454
+ actions: {
455
+ type: Array,
456
+ default: () => [],
457
+ },
458
+ /** Text shown when no items found */
459
+ emptyText: {
460
+ type: String,
461
+ default: 'No items found',
462
+ },
463
+ /** Function returning CSS class(es) for a row */
464
+ rowClass: {
465
+ type: Function,
466
+ default: null,
467
+ },
468
+ /** Override label for the Add button. Defaults to "Add {schema.title}" */
469
+ addLabel: {
470
+ type: String,
471
+ default: '',
472
+ },
473
+ /** How many action buttons to show inline (rest go in overflow dropdown) */
474
+ inlineActionCount: {
475
+ type: Number,
476
+ default: 2,
477
+ },
478
+ /** Whether to show the built-in mass Import action */
479
+ showMassImport: {
480
+ type: Boolean,
481
+ default: true,
482
+ },
483
+ /** Whether to show the built-in mass Export action */
484
+ showMassExport: {
485
+ type: Boolean,
486
+ default: true,
487
+ },
488
+ /** Whether to show the built-in mass Copy button */
489
+ showMassCopy: {
490
+ type: Boolean,
491
+ default: true,
492
+ },
493
+ /** Whether to show the built-in mass Delete button */
494
+ showMassDelete: {
495
+ type: Boolean,
496
+ default: true,
497
+ },
498
+ /** Property name used to display item names in dialogs */
499
+ massActionNameField: {
500
+ type: String,
501
+ default: 'title',
502
+ },
503
+ /** Available export formats for the export dialog */
504
+ exportFormats: {
505
+ type: Array,
506
+ default: () => [
507
+ { id: 'excel', label: 'Excel (.xlsx)' },
508
+ { id: 'csv', label: 'CSV (.csv)' },
509
+ ],
510
+ },
511
+ /** Import option definitions for the import dialog */
512
+ importOptions: {
513
+ type: Array,
514
+ default: () => [],
515
+ },
516
+ /** Whether to show the built-in form dialog for Add/Edit */
517
+ showFormDialog: {
518
+ type: Boolean,
519
+ default: true,
520
+ },
521
+ /** Use CnAdvancedFormDialog (properties table, JSON tab, optional metadata) instead of CnFormDialog for Add/Edit */
522
+ useAdvancedFormDialog: {
523
+ type: Boolean,
524
+ default: false,
525
+ },
526
+ /** Whether to add an Edit action to row actions */
527
+ showEditAction: {
528
+ type: Boolean,
529
+ default: true,
530
+ },
531
+ /** Whether to add a Copy action to row actions */
532
+ showCopyAction: {
533
+ type: Boolean,
534
+ default: true,
535
+ },
536
+ /** Whether to add a Delete action to row actions */
537
+ showDeleteAction: {
538
+ type: Boolean,
539
+ default: true,
540
+ },
541
+ /** Field keys to exclude from the form dialog */
542
+ excludeFields: {
543
+ type: Array,
544
+ default: () => [],
545
+ },
546
+ /** Field keys to include in the form dialog (whitelist mode) */
547
+ includeFields: {
548
+ type: Array,
549
+ default: null,
550
+ },
551
+ /** Per-field overrides passed to CnFormDialog */
552
+ fieldOverrides: {
553
+ type: Object,
554
+ default: () => ({}),
555
+ },
556
+ /** Whether to show the Cards/Table view toggle in the actions bar */
557
+ showViewToggle: {
558
+ type: Boolean,
559
+ default: true,
560
+ },
561
+ /** Whether the refresh action is currently in progress */
562
+ refreshing: {
563
+ type: Boolean,
564
+ default: false,
565
+ },
566
+ /**
567
+ * Store instance for automatic save integration. When provided alongside
568
+ * objectType, the form dialog saves directly to the store instead of
569
+ * emitting create/edit events. The object type must already be registered
570
+ * in the store via registerObjectType() before passing the store here.
571
+ */
572
+ store: { type: Object, default: null },
573
+ /**
574
+ * Object type slug for store integration (e.g. `${registerId}-${schemaId}`).
575
+ * Required when store is set — a console warning is emitted if missing.
576
+ */
577
+ objectType: { type: String, default: '' },
578
+ },
579
+
580
+ data() {
581
+ return {
582
+ currentViewMode: this.viewMode,
583
+ internalSelectedIds: [...this.selectedIds],
584
+ // Mass action dialogs
585
+ showMassDeleteDialog: false,
586
+ showMassCopyDialog: false,
587
+ showExportDialog: false,
588
+ showImportDialog: false,
589
+ // Single-object dialogs
590
+ showSingleDeleteDialog: false,
591
+ showSingleCopyDialog: false,
592
+ showFormDialogVisible: false,
593
+ // Dialog targets
594
+ actionTargetItem: null,
595
+ editItem: null,
596
+ }
597
+ },
598
+
599
+ computed: {
600
+ /** Resolved icon — explicit prop overrides schema.icon */
601
+ resolvedIcon() {
602
+ if (this.icon) return this.icon
603
+ return this.schema?.icon || ''
604
+ },
605
+
606
+ /** Resolved schema icon component for View action */
607
+ schemaIconComponent() {
608
+ if (this.resolvedIcon && ICON_MAP[this.resolvedIcon]) {
609
+ return ICON_MAP[this.resolvedIcon]
610
+ }
611
+ return Eye
612
+ },
613
+
614
+ /** Built-in row actions based on show*Action props */
615
+ defaultActions() {
616
+ const builtIn = []
617
+ if (this.$listeners && this.$listeners['row-click']) {
618
+ builtIn.push({
619
+ label: 'View',
620
+ icon: this.schemaIconComponent,
621
+ handler: (row) => {
622
+ this.$emit('row-click', row)
623
+ },
624
+ })
625
+ }
626
+ if (this.showEditAction) {
627
+ builtIn.push({
628
+ label: 'Edit',
629
+ icon: Pencil,
630
+ handler: (row) => {
631
+ this.editItem = row
632
+ this.showFormDialogVisible = true
633
+ },
634
+ })
635
+ }
636
+ if (this.showCopyAction) {
637
+ builtIn.push({
638
+ label: 'Copy',
639
+ icon: ContentCopy,
640
+ handler: (row) => {
641
+ this.actionTargetItem = row
642
+ this.showSingleCopyDialog = true
643
+ },
644
+ })
645
+ }
646
+ if (this.showDeleteAction) {
647
+ builtIn.push({
648
+ label: 'Delete',
649
+ icon: TrashCanOutline,
650
+ destructive: true,
651
+ handler: (row) => {
652
+ this.actionTargetItem = row
653
+ this.showSingleDeleteDialog = true
654
+ },
655
+ })
656
+ }
657
+ return builtIn
658
+ },
659
+
660
+ /** Merged actions: app-provided first, then built-in defaults */
661
+ mergedActions() {
662
+ return [...this.actions, ...this.defaultActions]
663
+ },
664
+
665
+ hasRowActions() {
666
+ return this.$scopedSlots['row-actions'] || this.mergedActions.length > 0
667
+ },
668
+
669
+ /** Whether all visible items are selected */
670
+ allSelected() {
671
+ if (this.objects.length === 0 || this.internalSelectedIds.length === 0) return false
672
+ return this.objects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))
673
+ },
674
+
675
+ /** Full objects for the selected IDs (used by mass action dialogs) */
676
+ selectedObjects() {
677
+ return this.objects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))
678
+ },
679
+
680
+ /** Column slot names that the parent has provided (for pass-through) */
681
+ slotColumns() {
682
+ return Object.keys(this.$scopedSlots)
683
+ .filter((name) => name.startsWith('column-'))
684
+ .map((name) => name.replace('column-', ''))
685
+ },
686
+
687
+ /** Add button label — derived from schema.title if not explicitly set */
688
+ resolvedAddLabel() {
689
+ if (this.addLabel) return this.addLabel
690
+ return 'Add ' + (this.schema?.title || 'Item')
691
+ },
692
+ },
693
+
694
+ watch: {
695
+ viewMode(val) {
696
+ this.currentViewMode = val
697
+ },
698
+ selectedIds(val) {
699
+ this.internalSelectedIds = [...val]
700
+ },
701
+ },
702
+
703
+ methods: {
704
+ /**
705
+ * Handle the Add button click. If the consumer listens to @add,
706
+ * emit the event (backward compatible). Otherwise open the form dialog.
707
+ */
708
+ onAddClick() {
709
+ if (this.$listeners && this.$listeners.add) {
710
+ this.$emit('add')
711
+ } else if (this.showFormDialog) {
712
+ this.editItem = null
713
+ this.showFormDialogVisible = true
714
+ }
715
+ },
716
+
717
+ /**
718
+ * Handle view mode toggle.
719
+ * @param {string} mode 'table' or 'cards'
720
+ */
721
+ onViewModeChange(mode) {
722
+ this.currentViewMode = mode
723
+ this.$emit('view-mode-change', mode)
724
+ },
725
+
726
+ /**
727
+ * Handle selection changes from CnDataTable/CnCardGrid.
728
+ * Updates internal state and re-emits for parent.
729
+ * @param {Array} ids Array of selected row IDs
730
+ */
731
+ onSelect(ids) {
732
+ this.internalSelectedIds = ids
733
+ this.$emit('select', ids)
734
+ },
735
+
736
+ // --- Mass action handlers ---
737
+
738
+ onMassDeleteConfirm(ids) {
739
+ this.$emit('mass-delete', ids)
740
+ },
741
+
742
+ onMassCopyConfirm(payload) {
743
+ this.$emit('mass-copy', payload)
744
+ },
745
+
746
+ onMassExportConfirm(payload) {
747
+ this.$emit('mass-export', payload)
748
+ },
749
+
750
+ onMassImportConfirm(payload) {
751
+ this.$emit('mass-import', payload)
752
+ },
753
+
754
+ /** @public Forward result to mass delete dialog */
755
+ setMassDeleteResult(resultData) {
756
+ if (this.$refs.massDeleteDialog) {
757
+ this.$refs.massDeleteDialog.setResult(resultData)
758
+ }
759
+ },
760
+
761
+ /** @public Forward result to mass copy dialog */
762
+ setMassCopyResult(resultData) {
763
+ if (this.$refs.massCopyDialog) {
764
+ this.$refs.massCopyDialog.setResult(resultData)
765
+ }
766
+ },
767
+
768
+ /** @public Forward result to export dialog */
769
+ setExportResult(resultData) {
770
+ if (this.$refs.exportDialog) {
771
+ this.$refs.exportDialog.setResult(resultData)
772
+ }
773
+ },
774
+
775
+ /** @public Forward result to import dialog */
776
+ setImportResult(resultData) {
777
+ if (this.$refs.importDialog) {
778
+ this.$refs.importDialog.setResult(resultData)
779
+ }
780
+ },
781
+
782
+ // --- Backward-compatible aliases ---
783
+ /** @public @deprecated Use setMassDeleteResult instead */
784
+ setDeleteResult(resultData) {
785
+ this.setMassDeleteResult(resultData)
786
+ },
787
+ /** @public @deprecated Use setMassCopyResult instead */
788
+ setCopyResult(resultData) {
789
+ this.setMassCopyResult(resultData)
790
+ },
791
+
792
+ // --- Single-object dialog handlers ---
793
+
794
+ onSingleDeleteConfirm(id) {
795
+ this.$emit('delete', id)
796
+ },
797
+
798
+ onSingleCopyConfirm(payload) {
799
+ this.$emit('copy', payload)
800
+ },
801
+
802
+ async onFormConfirm(formData) {
803
+ if (this.store) {
804
+ if (!this.objectType) {
805
+ console.warn('[CnIndexPage] store prop is set but objectType is missing. Cannot save to store.')
806
+ return
807
+ }
808
+ const saved = await this.store.saveObject(this.objectType, formData)
809
+ if (saved) {
810
+ this.setFormResult({ success: true })
811
+ this.$emit(this.editItem ? 'edit' : 'create', saved)
812
+ } else {
813
+ const err = this.store.getError?.(this.objectType)
814
+ this.setFormResult({ error: (err && err.message) || 'Save failed' })
815
+ }
816
+ return
817
+ }
818
+ if (this.editItem) {
819
+ this.$emit('edit', formData)
820
+ } else {
821
+ this.$emit('create', formData)
822
+ }
823
+ },
824
+
825
+ closeSingleDelete() {
826
+ this.showSingleDeleteDialog = false
827
+ this.actionTargetItem = null
828
+ },
829
+
830
+ closeSingleCopy() {
831
+ this.showSingleCopyDialog = false
832
+ this.actionTargetItem = null
833
+ },
834
+
835
+ closeFormDialog() {
836
+ this.showFormDialogVisible = false
837
+ this.editItem = null
838
+ },
839
+
840
+ /** @public Forward result to single delete dialog */
841
+ setSingleDeleteResult(resultData) {
842
+ if (this.$refs.singleDeleteDialog) {
843
+ this.$refs.singleDeleteDialog.setResult(resultData)
844
+ }
845
+ },
846
+
847
+ /** @public Forward result to single copy dialog */
848
+ setSingleCopyResult(resultData) {
849
+ if (this.$refs.singleCopyDialog) {
850
+ this.$refs.singleCopyDialog.setResult(resultData)
851
+ }
852
+ },
853
+
854
+ /** @public Forward result to form dialog */
855
+ setFormResult(resultData) {
856
+ if (this.$refs.formDialog) {
857
+ this.$refs.formDialog.setResult(resultData)
858
+ }
859
+ },
860
+
861
+ /**
862
+ * Programmatically open the form dialog.
863
+ * @param {object|null} item Pass null for create mode, or an object for edit mode
864
+ * @public
865
+ */
866
+ openFormDialog(item = null) {
867
+ this.editItem = item
868
+ this.showFormDialogVisible = true
869
+ },
870
+ },
871
+ }
872
+ </script>
873
+
874
+ <!-- Styles in css/index-page.css -->