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

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 (197) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs +67614 -0
  4. package/dist/nextcloud-vue.cjs.js +76311 -5905
  5. package/dist/nextcloud-vue.cjs.js.map +1 -1
  6. package/dist/nextcloud-vue.cjs.map +1 -0
  7. package/dist/nextcloud-vue.css +3279 -203
  8. package/dist/nextcloud-vue.esm.js +76240 -5882
  9. package/dist/nextcloud-vue.esm.js.map +1 -1
  10. package/package.json +89 -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 +569 -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 +23 -20
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/index.js +1 -1
  24. package/src/components/CnChartWidget/CnChartWidget.vue +318 -0
  25. package/src/components/CnChartWidget/index.js +1 -0
  26. package/src/components/CnConfigurationCard/index.js +1 -1
  27. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  28. package/src/components/CnContextMenu/index.js +1 -0
  29. package/src/components/CnCopyDialog/CnCopyDialog.vue +257 -0
  30. package/src/components/CnCopyDialog/index.js +1 -0
  31. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  32. package/src/components/CnDashboardGrid/index.js +1 -0
  33. package/src/components/CnDashboardPage/CnDashboardPage.vue +396 -0
  34. package/src/components/CnDashboardPage/index.js +1 -0
  35. package/src/components/CnDataTable/CnDataTable.vue +24 -16
  36. package/src/components/CnDataTable/index.js +1 -1
  37. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  38. package/src/components/CnDeleteDialog/index.js +1 -0
  39. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  40. package/src/components/CnDetailCard/index.js +1 -0
  41. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  42. package/src/components/CnDetailGrid/index.js +1 -0
  43. package/src/components/CnDetailPage/CnDetailPage.vue +431 -0
  44. package/src/components/CnDetailPage/index.js +1 -0
  45. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +12 -2
  46. package/src/components/CnFacetSidebar/index.js +1 -1
  47. package/src/components/CnFilterBar/index.js +1 -1
  48. package/src/components/CnFormDialog/CnFormDialog.vue +934 -0
  49. package/src/components/CnFormDialog/index.js +1 -0
  50. package/src/components/CnIcon/CnIcon.vue +89 -0
  51. package/src/components/CnIcon/index.js +1 -0
  52. package/src/components/CnIndexPage/CnIndexPage.vue +589 -291
  53. package/src/components/CnIndexPage/index.js +1 -1
  54. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +535 -0
  55. package/src/components/CnIndexSidebar/index.js +1 -0
  56. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  57. package/src/components/CnInfoWidget/index.js +1 -0
  58. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  59. package/src/components/CnItemCard/index.js +1 -0
  60. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  61. package/src/components/CnJsonViewer/index.js +1 -0
  62. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  63. package/src/components/CnKpiGrid/index.js +1 -1
  64. package/src/components/CnMassActionBar/CnMassActionBar.vue +6 -5
  65. package/src/components/CnMassActionBar/index.js +1 -1
  66. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +16 -9
  67. package/src/components/CnMassCopyDialog/index.js +1 -1
  68. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +16 -9
  69. package/src/components/CnMassDeleteDialog/index.js +1 -1
  70. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +8 -7
  71. package/src/components/CnMassExportDialog/index.js +1 -1
  72. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +20 -17
  73. package/src/components/CnMassImportDialog/index.js +1 -1
  74. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  75. package/src/components/CnNoteCard/index.js +1 -0
  76. package/src/components/CnNotesCard/CnNotesCard.vue +415 -0
  77. package/src/components/CnNotesCard/index.js +1 -0
  78. package/src/components/CnObjectCard/CnObjectCard.vue +3 -1
  79. package/src/components/CnObjectCard/index.js +1 -1
  80. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +853 -0
  81. package/src/components/CnObjectDataWidget/index.js +1 -0
  82. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +288 -0
  83. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  84. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  85. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  86. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  87. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +254 -0
  88. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  89. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  90. package/src/components/CnObjectSidebar/index.js +6 -0
  91. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  92. package/src/components/CnPageHeader/index.js +1 -0
  93. package/src/components/CnPagination/CnPagination.vue +7 -6
  94. package/src/components/CnPagination/index.js +1 -1
  95. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  96. package/src/components/CnProgressBar/index.js +1 -0
  97. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  98. package/src/components/CnRegisterMapping/index.js +1 -0
  99. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  100. package/src/components/CnRowActions/index.js +1 -1
  101. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  102. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  103. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  104. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  106. package/src/components/CnSchemaFormDialog/index.js +1 -0
  107. package/src/components/CnSettingsCard/index.js +1 -1
  108. package/src/components/CnSettingsSection/index.js +1 -1
  109. package/src/components/CnStatsBlock/CnStatsBlock.vue +89 -19
  110. package/src/components/CnStatsBlock/index.js +1 -1
  111. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  112. package/src/components/CnStatsPanel/index.js +1 -0
  113. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  114. package/src/components/CnStatusBadge/index.js +1 -1
  115. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +544 -0
  116. package/src/components/CnTabbedFormDialog/index.js +1 -0
  117. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  118. package/src/components/CnTableWidget/index.js +1 -0
  119. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  120. package/src/components/CnTasksCard/index.js +1 -0
  121. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  122. package/src/components/CnTileWidget/index.js +1 -0
  123. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  124. package/src/components/CnTimelineStages/index.js +1 -0
  125. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  126. package/src/components/CnUserActionMenu/index.js +1 -0
  127. package/src/components/CnVersionInfoCard/index.js +1 -1
  128. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  129. package/src/components/CnWidgetRenderer/index.js +1 -0
  130. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +246 -0
  131. package/src/components/CnWidgetWrapper/index.js +1 -0
  132. package/src/components/index.js +57 -25
  133. package/src/composables/index.js +5 -3
  134. package/src/composables/useContextMenu.js +126 -0
  135. package/src/composables/useDashboardView.js +286 -0
  136. package/src/composables/useDetailView.js +290 -132
  137. package/src/composables/useListView.js +364 -153
  138. package/src/composables/useSubResource.js +142 -142
  139. package/src/constants/metadata.js +30 -0
  140. package/src/css/CnSchemaFormDialog.css +546 -0
  141. package/src/css/__sample_nextcloud_tokens.css +110 -0
  142. package/src/css/actions-bar.css +54 -0
  143. package/src/css/badge.css +83 -51
  144. package/src/css/card.css +129 -128
  145. package/src/css/context-menu.css +20 -0
  146. package/src/css/dashboard.css +70 -0
  147. package/src/css/detail-page.css +235 -0
  148. package/src/css/detail.css +68 -68
  149. package/src/css/index-page.css +44 -0
  150. package/src/css/index-sidebar.css +193 -0
  151. package/src/css/index.css +17 -8
  152. package/src/css/layout.css +90 -90
  153. package/src/css/page-header.css +35 -0
  154. package/src/css/pagination.css +72 -72
  155. package/src/css/table.css +142 -143
  156. package/src/css/timeline-stages.css +220 -0
  157. package/src/css/utilities.css +46 -46
  158. package/src/index.js +91 -50
  159. package/src/mixins/gridLayout.js +118 -0
  160. package/src/store/createCrudStore.js +360 -0
  161. package/src/store/createSubResourcePlugin.js +125 -135
  162. package/src/store/index.js +4 -3
  163. package/src/store/plugins/auditTrails.js +357 -17
  164. package/src/store/plugins/files.js +250 -186
  165. package/src/store/plugins/index.js +7 -4
  166. package/src/store/plugins/lifecycle.js +180 -180
  167. package/src/store/plugins/registerMapping.js +195 -0
  168. package/src/store/plugins/relations.js +68 -68
  169. package/src/store/plugins/search.js +385 -0
  170. package/src/store/plugins/selection.js +104 -0
  171. package/src/store/useObjectStore.js +823 -625
  172. package/src/types/auditTrail.d.ts +32 -32
  173. package/src/types/file.d.ts +23 -23
  174. package/src/types/index.d.ts +35 -35
  175. package/src/types/notification.d.ts +36 -36
  176. package/src/types/object.d.ts +40 -40
  177. package/src/types/organisation.d.ts +41 -41
  178. package/src/types/register.d.ts +25 -25
  179. package/src/types/schema.d.ts +39 -39
  180. package/src/types/shared.d.ts +79 -79
  181. package/src/types/source.d.ts +14 -14
  182. package/src/types/task.d.ts +31 -31
  183. package/src/utils/errors.js +96 -96
  184. package/src/utils/getTheme.js +9 -0
  185. package/src/utils/headers.js +80 -44
  186. package/src/utils/id.js +13 -0
  187. package/src/utils/index.js +4 -3
  188. package/src/utils/schema.js +422 -287
  189. package/src/utils/widgetVisibility.js +162 -0
  190. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  191. package/src/components/CnDetailViewLayout/index.js +0 -1
  192. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  193. package/src/components/CnEmptyState/index.js +0 -1
  194. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  195. package/src/components/CnListViewLayout/index.js +0 -1
  196. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  197. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -1,57 +1,73 @@
1
1
  <template>
2
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>
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" />
36
13
  </div>
37
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
+
38
52
  <!-- Mass delete dialog -->
39
53
  <CnMassDeleteDialog
40
- v-if="showDeleteDialog"
41
- ref="deleteDialog"
54
+ v-if="showMassDeleteDialog"
55
+ ref="massDeleteDialog"
42
56
  :items="selectedObjects"
43
57
  :name-field="massActionNameField"
58
+ :name-formatter="nameFormatter"
44
59
  @confirm="onMassDeleteConfirm"
45
- @close="showDeleteDialog = false" />
60
+ @close="showMassDeleteDialog = false" />
46
61
 
47
62
  <!-- Mass copy dialog -->
48
63
  <CnMassCopyDialog
49
- v-if="showCopyDialog"
50
- ref="copyDialog"
64
+ v-if="showMassCopyDialog"
65
+ ref="massCopyDialog"
51
66
  :items="selectedObjects"
52
67
  :name-field="massActionNameField"
68
+ :name-formatter="nameFormatter"
53
69
  @confirm="onMassCopyConfirm"
54
- @close="showCopyDialog = false" />
70
+ @close="showMassCopyDialog = false" />
55
71
 
56
72
  <!-- Mass export dialog -->
57
73
  <CnMassExportDialog
@@ -73,35 +89,73 @@
73
89
  </template>
74
90
  </CnMassImportDialog>
75
91
 
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 -->
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">
93
158
  <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
159
  <!-- Loading state -->
106
160
  <div v-if="loading" class="cn-index-page__loading">
107
161
  <NcLoadingIcon :size="32" />
@@ -112,7 +166,8 @@
112
166
  <slot name="empty">
113
167
  <NcEmptyContent :name="emptyText">
114
168
  <template #icon>
115
- <DatabaseSearch :size="64" />
169
+ <CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
170
+ <DatabaseSearch v-else :size="64" />
116
171
  </template>
117
172
  </NcEmptyContent>
118
173
  </slot>
@@ -127,7 +182,7 @@
127
182
  :sort-key="sortKey"
128
183
  :sort-order="sortOrder"
129
184
  :selectable="selectable"
130
- :selected-ids="selectedIds"
185
+ :selected-ids="internalSelectedIds"
131
186
  :row-key="rowKey"
132
187
  :empty-text="emptyText"
133
188
  :exclude-columns="excludeColumns"
@@ -135,8 +190,9 @@
135
190
  :column-overrides="columnOverrides"
136
191
  :row-class="rowClass"
137
192
  @sort="$emit('sort', $event)"
138
- @select="$emit('select', $event)"
139
- @row-click="$emit('row-click', $event)">
193
+ @select="onSelect"
194
+ @row-click="onRowClick"
195
+ @row-context-menu="onRowContextMenu">
140
196
  <!-- Pass through column slots -->
141
197
  <template
142
198
  v-for="col in slotColumns"
@@ -148,8 +204,7 @@
148
204
  <template v-if="hasRowActions" #row-actions="{ row }">
149
205
  <slot name="row-actions" :row="row">
150
206
  <CnRowActions
151
- v-if="actions.length > 0"
152
- :actions="actions"
207
+ :actions="mergedActions"
153
208
  :row="row"
154
209
  @action="$emit('action', $event)" />
155
210
  </slot>
@@ -162,25 +217,32 @@
162
217
  :objects="objects"
163
218
  :schema="schema"
164
219
  :selectable="selectable"
165
- :selected-ids="selectedIds"
220
+ :selected-ids="internalSelectedIds"
166
221
  :row-key="rowKey"
167
222
  :empty-text="emptyText"
168
- @click="$emit('row-click', $event)"
169
- @select="$emit('select', $event)">
223
+ @click="onRowClick"
224
+ @select="onSelect">
170
225
  <template v-if="$scopedSlots.card" #card="{ object, selected }">
171
226
  <slot name="card" :object="object" :selected="selected" />
172
227
  </template>
173
228
  <template v-if="hasRowActions" #card-actions="{ object }">
174
229
  <slot name="row-actions" :row="object">
175
230
  <CnRowActions
176
- v-if="actions.length > 0"
177
- :actions="actions"
231
+ :actions="mergedActions"
178
232
  :row="object"
179
233
  @action="$emit('action', $event)" />
180
234
  </slot>
181
235
  </template>
182
236
  </CnCardGrid>
183
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
+
184
246
  <!-- Pagination -->
185
247
  <CnPagination
186
248
  v-if="pagination && pagination.pages > 1"
@@ -199,83 +261,93 @@
199
261
  <script>
200
262
  import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
201
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'
202
271
  import { CnDataTable } from '../CnDataTable/index.js'
203
272
  import { CnCardGrid } from '../CnCardGrid/index.js'
204
273
  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
274
  import { CnRowActions } from '../CnRowActions/index.js'
209
- import { CnMassActionBar } from '../CnMassActionBar/index.js'
210
275
  import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
211
276
  import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
212
277
  import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
213
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'
214
285
 
215
286
  /**
216
287
  * CnIndexPage — Top-level schema-driven index page component.
217
288
  *
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.
289
+ * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,
290
+ * pagination, mass actions, single-object dialogs) into a single
291
+ * zero-config page.
221
292
  *
222
- * @example Minimal usage
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)
223
302
  * <CnIndexPage
224
- * title="Publications"
303
+ * title="Clients"
225
304
  * :schema="schema"
226
- * :objects="publications"
305
+ * :objects="clients"
227
306
  * :pagination="pagination"
228
307
  * :loading="loading"
229
- * :search-value="search"
230
- * @search="onSearch"
231
- * @row-click="openPublication"
308
+ * @create="onCreate"
309
+ * @edit="onEdit"
310
+ * @delete="onDelete"
311
+ * @refresh="fetchClients"
312
+ * @row-click="openClient"
232
313
  * @page-changed="onPage" />
233
314
  *
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>
315
+ * @example With custom form dialog
316
+ * <CnIndexPage ...>
317
+ * <template #form-dialog="{ item, schema, close }">
318
+ * <MyCustomFormDialog :item="item" @close="close" />
257
319
  * </template>
258
320
  * </CnIndexPage>
259
321
  *
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
- * }
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 }`
279
351
  */
280
352
  export default {
281
353
  name: 'CnIndexPage',
@@ -284,18 +356,22 @@ export default {
284
356
  NcLoadingIcon,
285
357
  NcEmptyContent,
286
358
  DatabaseSearch,
359
+ CnPageHeader,
360
+ CnActionsBar,
361
+ CnIcon,
287
362
  CnDataTable,
288
363
  CnCardGrid,
289
364
  CnPagination,
290
- CnFilterBar,
291
- CnFacetSidebar,
292
- CnViewModeToggle,
293
365
  CnRowActions,
294
- CnMassActionBar,
295
366
  CnMassDeleteDialog,
296
367
  CnMassCopyDialog,
297
368
  CnMassExportDialog,
298
369
  CnMassImportDialog,
370
+ CnDeleteDialog,
371
+ CnCopyDialog,
372
+ CnFormDialog,
373
+ CnAdvancedFormDialog,
374
+ CnContextMenu,
299
375
  },
300
376
 
301
377
  props: {
@@ -304,6 +380,24 @@ export default {
304
380
  type: String,
305
381
  required: true,
306
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
+ },
307
401
  /** Schema definition */
308
402
  schema: {
309
403
  type: Object,
@@ -329,36 +423,6 @@ export default {
329
423
  type: Boolean,
330
424
  default: false,
331
425
  },
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
426
  /** Whether rows/cards can be selected */
363
427
  selectable: {
364
428
  type: Boolean,
@@ -405,7 +469,7 @@ export default {
405
469
  type: Object,
406
470
  default: () => ({}),
407
471
  },
408
- /** Row action definitions */
472
+ /** Row action definitions (app-provided, merged with built-in actions) */
409
473
  actions: {
410
474
  type: Array,
411
475
  default: () => [],
@@ -415,21 +479,21 @@ export default {
415
479
  type: String,
416
480
  default: 'No items found',
417
481
  },
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
482
  /** Function returning CSS class(es) for a row */
429
483
  rowClass: {
430
484
  type: Function,
431
485
  default: null,
432
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
+ },
433
497
  /** Whether to show the built-in mass Import action */
434
498
  showMassImport: {
435
499
  type: Boolean,
@@ -450,11 +514,16 @@ export default {
450
514
  type: Boolean,
451
515
  default: true,
452
516
  },
453
- /** Property name used to display item names in mass action dialogs */
517
+ /** Property name used to display item names in dialogs */
454
518
  massActionNameField: {
455
519
  type: String,
456
520
  default: 'title',
457
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
+ },
458
527
  /** Available export formats for the export dialog */
459
528
  exportFormats: {
460
529
  type: Array,
@@ -468,41 +537,199 @@ export default {
468
537
  type: Array,
469
538
  default: () => [],
470
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
+ }
471
633
  },
472
634
 
473
635
  data() {
474
636
  return {
475
637
  currentViewMode: this.viewMode,
476
- showDeleteDialog: false,
477
- showCopyDialog: false,
638
+ internalSelectedIds: [...this.selectedIds],
639
+ // Mass action dialogs
640
+ showMassDeleteDialog: false,
641
+ showMassCopyDialog: false,
478
642
  showExportDialog: false,
479
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,
480
651
  }
481
652
  },
482
653
 
483
654
  computed: {
484
- countText() {
485
- if (!this.pagination) return ''
486
- return `Showing ${this.objects.length} of ${this.pagination.total}`
655
+ /** Resolved icon — explicit prop overrides schema.icon */
656
+ resolvedIcon() {
657
+ if (this.icon) return this.icon
658
+ return this.schema?.icon || ''
487
659
  },
488
660
 
489
- showSidebar() {
490
- return this.$scopedSlots.sidebar || this.facetData !== null
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]
491
718
  },
492
719
 
493
720
  hasRowActions() {
494
- return this.$scopedSlots['row-actions'] || this.actions.length > 0
721
+ return this.$scopedSlots['row-actions'] || this.mergedActions.length > 0
495
722
  },
496
723
 
497
724
  /** Whether all visible items are selected */
498
725
  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]))
726
+ if (this.objects.length === 0 || this.internalSelectedIds.length === 0) return false
727
+ return this.objects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))
501
728
  },
502
729
 
503
730
  /** Full objects for the selected IDs (used by mass action dialogs) */
504
731
  selectedObjects() {
505
- return this.objects.filter((o) => this.selectedIds.includes(o[this.rowKey]))
732
+ return this.objects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))
506
733
  },
507
734
 
508
735
  /** Column slot names that the parent has provided (for pass-through) */
@@ -511,79 +738,104 @@ export default {
511
738
  .filter((name) => name.startsWith('column-'))
512
739
  .map((name) => name.replace('column-', ''))
513
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
+ },
514
747
  },
515
748
 
516
749
  watch: {
517
750
  viewMode(val) {
518
751
  this.currentViewMode = val
519
752
  },
753
+ selectedIds(val) {
754
+ this.internalSelectedIds = [...val]
755
+ },
520
756
  },
521
757
 
522
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
+ */
523
784
  onViewModeChange(mode) {
524
785
  this.currentViewMode = mode
525
786
  this.$emit('view-mode-change', mode)
526
787
  },
527
788
 
528
789
  /**
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
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
532
793
  */
794
+ onSelect(ids) {
795
+ this.internalSelectedIds = ids
796
+ this.$emit('select', ids)
797
+ },
798
+
799
+ // --- Mass action handlers ---
800
+
533
801
  onMassDeleteConfirm(ids) {
534
802
  this.$emit('mass-delete', ids)
535
803
  },
536
804
 
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
805
  onMassCopyConfirm(payload) {
543
806
  this.$emit('mass-copy', payload)
544
807
  },
545
808
 
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
- }
809
+ onMassExportConfirm(payload) {
810
+ this.$emit('mass-export', payload)
555
811
  },
556
812
 
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
- }
813
+ onMassImportConfirm(payload) {
814
+ this.$emit('mass-import', payload)
566
815
  },
567
816
 
568
817
  /**
569
- * Handle mass export confirm.
570
- * @param {{ format: string }} payload
818
+ * @param {*} resultData Result data to pass to the dialog
819
+ * @public
571
820
  */
572
- onMassExportConfirm(payload) {
573
- this.$emit('mass-export', payload)
821
+ setMassDeleteResult(resultData) {
822
+ if (this.$refs.massDeleteDialog) {
823
+ this.$refs.massDeleteDialog.setResult(resultData)
824
+ }
574
825
  },
575
826
 
576
827
  /**
577
- * Handle mass import confirm.
578
- * @param {{ file: File, options: object }} payload
828
+ * @param {*} resultData Result data to pass to the dialog
829
+ * @public
579
830
  */
580
- onMassImportConfirm(payload) {
581
- this.$emit('mass-import', payload)
831
+ setMassCopyResult(resultData) {
832
+ if (this.$refs.massCopyDialog) {
833
+ this.$refs.massCopyDialog.setResult(resultData)
834
+ }
582
835
  },
583
836
 
584
837
  /**
585
- * Set the result of a mass export operation.
586
- * @param {{ success?: boolean, error?: string }} resultData
838
+ * @param {*} resultData Result data to pass to the dialog
587
839
  * @public
588
840
  */
589
841
  setExportResult(resultData) {
@@ -593,8 +845,7 @@ export default {
593
845
  },
594
846
 
595
847
  /**
596
- * Set the result of a mass import operation.
597
- * @param {{ success?: boolean, error?: string, summary?: object }} resultData
848
+ * @param {*} resultData Result data to pass to the dialog
598
849
  * @public
599
850
  */
600
851
  setImportResult(resultData) {
@@ -602,81 +853,128 @@ export default {
602
853
  this.$refs.importDialog.setResult(resultData)
603
854
  }
604
855
  },
605
- },
606
- }
607
- </script>
608
856
 
609
- <style scoped>
610
- .cn-index-page {
611
- padding: 20px;
612
- }
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
+ },
613
872
 
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
- }
873
+ // --- Single-object dialog handlers ---
622
874
 
623
- .cn-index-page__title-area {
624
- display: flex;
625
- align-items: baseline;
626
- gap: 8px;
627
- }
875
+ onSingleDeleteConfirm(id) {
876
+ this.$emit('delete', id)
877
+ },
628
878
 
629
- .cn-index-page__title {
630
- margin: 0;
631
- font-size: 22px;
632
- font-weight: 700;
633
- }
879
+ onSingleCopyConfirm(payload) {
880
+ this.$emit('copy', payload)
881
+ },
634
882
 
635
- .cn-index-page__count {
636
- font-size: 14px;
637
- color: var(--color-text-maxcontrast);
638
- }
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
+ },
639
905
 
640
- .cn-index-page__header-actions {
641
- display: flex;
642
- align-items: center;
643
- gap: 8px;
644
- }
906
+ closeSingleDelete() {
907
+ this.showSingleDeleteDialog = false
908
+ this.actionTargetItem = null
909
+ },
645
910
 
646
- .cn-index-page__body {
647
- display: flex;
648
- gap: 0;
649
- }
911
+ closeSingleCopy() {
912
+ this.showSingleCopyDialog = false
913
+ this.actionTargetItem = null
914
+ },
650
915
 
651
- .cn-index-page__body--with-sidebar {
652
- gap: 0;
653
- }
916
+ closeFormDialog() {
917
+ this.showFormDialogVisible = false
918
+ this.editItem = null
919
+ },
654
920
 
655
- .cn-index-page__sidebar {
656
- flex-shrink: 0;
657
- }
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
+ },
658
930
 
659
- .cn-index-page__main {
660
- flex: 1;
661
- min-width: 0;
662
- }
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
+ },
663
940
 
664
- .cn-index-page__search {
665
- margin-bottom: 16px;
666
- }
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
+ },
667
950
 
668
- .cn-index-page__loading {
669
- display: flex;
670
- justify-content: center;
671
- padding: 60px;
672
- }
951
+ // --- Context menu handlers ---
673
952
 
674
- .cn-index-page__empty {
675
- padding: 40px 20px;
676
- text-align: center;
677
- }
953
+ onRowContextMenu({ row, event }) {
954
+ this.openContextMenu({ item: row, event })
955
+ },
678
956
 
679
- .cn-index-page__pagination {
680
- margin-top: 16px;
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
+ },
681
977
  }
682
- </style>
978
+ </script>
979
+
980
+ <!-- Styles in css/index-page.css -->