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

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