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

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