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

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 (54) hide show
  1. package/README.md +226 -0
  2. package/dist/nextcloud-vue.cjs.js +7039 -2409
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +237 -52
  5. package/dist/nextcloud-vue.esm.js +7012 -2386
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +2 -4
  8. package/src/components/CnActionsBar/CnActionsBar.vue +225 -0
  9. package/src/components/CnActionsBar/index.js +1 -0
  10. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -0
  11. package/src/components/CnCopyDialog/index.js +1 -0
  12. package/src/components/CnDataTable/CnDataTable.vue +0 -5
  13. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -0
  14. package/src/components/CnDeleteDialog/index.js +1 -0
  15. package/src/components/CnFormDialog/CnFormDialog.vue +629 -0
  16. package/src/components/CnFormDialog/index.js +1 -0
  17. package/src/components/CnIcon/CnIcon.vue +89 -0
  18. package/src/components/CnIcon/index.js +1 -0
  19. package/src/components/CnIndexPage/CnIndexPage.vue +434 -300
  20. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +484 -0
  21. package/src/components/CnIndexSidebar/index.js +1 -0
  22. package/src/components/CnPageHeader/CnPageHeader.vue +57 -0
  23. package/src/components/CnPageHeader/index.js +1 -0
  24. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  25. package/src/components/CnRegisterMapping/index.js +1 -0
  26. package/src/components/index.js +8 -4
  27. package/src/constants/metadata.js +30 -0
  28. package/src/css/actions-bar.css +48 -0
  29. package/src/css/badge.css +4 -4
  30. package/src/css/card.css +23 -23
  31. package/src/css/detail.css +13 -13
  32. package/src/css/index-page.css +32 -0
  33. package/src/css/index-sidebar.css +187 -0
  34. package/src/css/index.css +4 -0
  35. package/src/css/layout.css +14 -14
  36. package/src/css/page-header.css +33 -0
  37. package/src/css/pagination.css +12 -12
  38. package/src/css/table.css +21 -22
  39. package/src/css/utilities.css +2 -2
  40. package/src/index.js +11 -8
  41. package/src/store/plugins/index.js +1 -0
  42. package/src/store/plugins/registerMapping.js +185 -0
  43. package/src/store/useObjectStore.js +122 -61
  44. package/src/utils/headers.js +7 -1
  45. package/src/utils/index.js +1 -1
  46. package/src/utils/schema.js +133 -1
  47. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  48. package/src/components/CnDetailViewLayout/index.js +0 -1
  49. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  50. package/src/components/CnEmptyState/index.js +0 -1
  51. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  52. package/src/components/CnListViewLayout/index.js +0 -1
  53. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  54. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -1,57 +1,62 @@
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" />
3
+ <!-- Header (hidden by default — shown in sidebar instead) -->
4
+ <CnPageHeader
5
+ v-if="showTitle"
6
+ :title="title"
7
+ :description="description"
8
+ :icon="resolvedIcon" />
9
+
10
+ <!-- Actions bar -->
11
+ <CnActionsBar
12
+ :pagination="pagination"
13
+ :object-count="objects.length"
14
+ :selectable="selectable"
15
+ :selected-ids="internalSelectedIds"
16
+ :add-label="resolvedAddLabel"
17
+ :add-icon="resolvedIcon"
18
+ :inline-action-count="inlineActionCount"
19
+ :show-mass-import="showMassImport"
20
+ :show-mass-export="showMassExport"
21
+ :show-mass-copy="showMassCopy"
22
+ :show-mass-delete="showMassDelete"
23
+ :view-mode="currentViewMode"
24
+ :show-view-toggle="showViewToggle"
25
+ @add="onAddClick"
26
+ @refresh="$emit('refresh')"
27
+ @show-import="showImportDialog = true"
28
+ @show-export="showExportDialog = true"
29
+ @show-copy="showMassCopyDialog = true"
30
+ @show-delete="showMassDeleteDialog = true"
31
+ @view-mode-change="onViewModeChange">
32
+ <template v-if="$scopedSlots['mass-actions']" #mass-actions="{ count, selectedIds: ids }">
33
+ <slot name="mass-actions" :count="count" :selected-ids="ids" />
34
+ </template>
35
+ <template v-if="$scopedSlots['action-items']" #action-items>
36
+ <slot name="action-items" />
37
+ </template>
38
+ <template v-if="$scopedSlots['header-actions']" #header-actions>
34
39
  <slot name="header-actions" />
35
- </div>
36
- </div>
40
+ </template>
41
+ </CnActionsBar>
37
42
 
38
43
  <!-- Mass delete dialog -->
39
44
  <CnMassDeleteDialog
40
- v-if="showDeleteDialog"
41
- ref="deleteDialog"
45
+ v-if="showMassDeleteDialog"
46
+ ref="massDeleteDialog"
42
47
  :items="selectedObjects"
43
48
  :name-field="massActionNameField"
44
49
  @confirm="onMassDeleteConfirm"
45
- @close="showDeleteDialog = false" />
50
+ @close="showMassDeleteDialog = false" />
46
51
 
47
52
  <!-- Mass copy dialog -->
48
53
  <CnMassCopyDialog
49
- v-if="showCopyDialog"
50
- ref="copyDialog"
54
+ v-if="showMassCopyDialog"
55
+ ref="massCopyDialog"
51
56
  :items="selectedObjects"
52
57
  :name-field="massActionNameField"
53
58
  @confirm="onMassCopyConfirm"
54
- @close="showCopyDialog = false" />
59
+ @close="showMassCopyDialog = false" />
55
60
 
56
61
  <!-- Mass export dialog -->
57
62
  <CnMassExportDialog
@@ -73,35 +78,60 @@
73
78
  </template>
74
79
  </CnMassImportDialog>
75
80
 
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 -->
81
+ <!-- Single delete dialog (overridable via slot) -->
82
+ <slot
83
+ name="delete-dialog"
84
+ :item="actionTargetItem"
85
+ :close="closeSingleDelete">
86
+ <CnDeleteDialog
87
+ v-if="showSingleDeleteDialog && actionTargetItem"
88
+ ref="singleDeleteDialog"
89
+ :item="actionTargetItem"
90
+ :name-field="massActionNameField"
91
+ @confirm="onSingleDeleteConfirm"
92
+ @close="closeSingleDelete" />
93
+ </slot>
94
+
95
+ <!-- Single copy dialog (overridable via slot) -->
96
+ <slot
97
+ name="copy-dialog"
98
+ :item="actionTargetItem"
99
+ :close="closeSingleCopy">
100
+ <CnCopyDialog
101
+ v-if="showSingleCopyDialog && actionTargetItem"
102
+ ref="singleCopyDialog"
103
+ :item="actionTargetItem"
104
+ :name-field="massActionNameField"
105
+ @confirm="onSingleCopyConfirm"
106
+ @close="closeSingleCopy" />
107
+ </slot>
108
+
109
+ <!-- Form dialog for create/edit (overridable via slot) -->
110
+ <slot
111
+ name="form-dialog"
112
+ :item="editItem"
113
+ :schema="schema"
114
+ :close="closeFormDialog">
115
+ <CnFormDialog
116
+ v-if="showFormDialogVisible"
117
+ ref="formDialog"
118
+ :schema="schema"
119
+ :item="editItem"
120
+ :exclude-fields="excludeFields"
121
+ :include-fields="includeFields"
122
+ :field-overrides="fieldOverrides"
123
+ :name-field="massActionNameField"
124
+ @confirm="onFormConfirm"
125
+ @close="closeFormDialog">
126
+ <template v-if="$scopedSlots['form-fields']" #form="scope">
127
+ <slot name="form-fields" v-bind="scope" />
128
+ </template>
129
+ </CnFormDialog>
130
+ </slot>
131
+
132
+ <!-- Body -->
133
+ <div class="cn-index-page__body">
93
134
  <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
135
  <!-- Loading state -->
106
136
  <div v-if="loading" class="cn-index-page__loading">
107
137
  <NcLoadingIcon :size="32" />
@@ -112,7 +142,8 @@
112
142
  <slot name="empty">
113
143
  <NcEmptyContent :name="emptyText">
114
144
  <template #icon>
115
- <DatabaseSearch :size="64" />
145
+ <CnIcon v-if="resolvedIcon" :name="resolvedIcon" :size="64" />
146
+ <DatabaseSearch v-else :size="64" />
116
147
  </template>
117
148
  </NcEmptyContent>
118
149
  </slot>
@@ -127,7 +158,7 @@
127
158
  :sort-key="sortKey"
128
159
  :sort-order="sortOrder"
129
160
  :selectable="selectable"
130
- :selected-ids="selectedIds"
161
+ :selected-ids="internalSelectedIds"
131
162
  :row-key="rowKey"
132
163
  :empty-text="emptyText"
133
164
  :exclude-columns="excludeColumns"
@@ -135,7 +166,7 @@
135
166
  :column-overrides="columnOverrides"
136
167
  :row-class="rowClass"
137
168
  @sort="$emit('sort', $event)"
138
- @select="$emit('select', $event)"
169
+ @select="onSelect"
139
170
  @row-click="$emit('row-click', $event)">
140
171
  <!-- Pass through column slots -->
141
172
  <template
@@ -148,8 +179,7 @@
148
179
  <template v-if="hasRowActions" #row-actions="{ row }">
149
180
  <slot name="row-actions" :row="row">
150
181
  <CnRowActions
151
- v-if="actions.length > 0"
152
- :actions="actions"
182
+ :actions="mergedActions"
153
183
  :row="row"
154
184
  @action="$emit('action', $event)" />
155
185
  </slot>
@@ -162,19 +192,18 @@
162
192
  :objects="objects"
163
193
  :schema="schema"
164
194
  :selectable="selectable"
165
- :selected-ids="selectedIds"
195
+ :selected-ids="internalSelectedIds"
166
196
  :row-key="rowKey"
167
197
  :empty-text="emptyText"
168
198
  @click="$emit('row-click', $event)"
169
- @select="$emit('select', $event)">
199
+ @select="onSelect">
170
200
  <template v-if="$scopedSlots.card" #card="{ object, selected }">
171
201
  <slot name="card" :object="object" :selected="selected" />
172
202
  </template>
173
203
  <template v-if="hasRowActions" #card-actions="{ object }">
174
204
  <slot name="row-actions" :row="object">
175
205
  <CnRowActions
176
- v-if="actions.length > 0"
177
- :actions="actions"
206
+ :actions="mergedActions"
178
207
  :row="object"
179
208
  @action="$emit('action', $event)" />
180
209
  </slot>
@@ -199,83 +228,88 @@
199
228
  <script>
200
229
  import { NcLoadingIcon, NcEmptyContent } from '@nextcloud/vue'
201
230
  import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
231
+ import Eye from 'vue-material-design-icons/Eye.vue'
232
+ import Pencil from 'vue-material-design-icons/Pencil.vue'
233
+ import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
234
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
235
+ import { CnPageHeader } from '../CnPageHeader/index.js'
236
+ import { CnActionsBar } from '../CnActionsBar/index.js'
237
+ import { CnIcon, ICON_MAP } from '../CnIcon/index.js'
202
238
  import { CnDataTable } from '../CnDataTable/index.js'
203
239
  import { CnCardGrid } from '../CnCardGrid/index.js'
204
240
  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
241
  import { CnRowActions } from '../CnRowActions/index.js'
209
- import { CnMassActionBar } from '../CnMassActionBar/index.js'
210
242
  import { CnMassDeleteDialog } from '../CnMassDeleteDialog/index.js'
211
243
  import { CnMassCopyDialog } from '../CnMassCopyDialog/index.js'
212
244
  import { CnMassExportDialog } from '../CnMassExportDialog/index.js'
213
245
  import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
246
+ import { CnDeleteDialog } from '../CnDeleteDialog/index.js'
247
+ import { CnCopyDialog } from '../CnCopyDialog/index.js'
248
+ import { CnFormDialog } from '../CnFormDialog/index.js'
214
249
 
215
250
  /**
216
251
  * CnIndexPage — Top-level schema-driven index page component.
217
252
  *
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.
253
+ * Assembles sub-components (CnPageHeader, CnActionsBar, table, cards,
254
+ * pagination, mass actions, single-object dialogs) into a single
255
+ * zero-config page.
256
+ *
257
+ * Dialogs are overridable via named slots:
258
+ * - `#form-dialog` — Replace the create/edit dialog entirely
259
+ * - `#delete-dialog` — Replace the single-item delete dialog
260
+ * - `#copy-dialog` — Replace the single-item copy dialog
261
+ * - `#form-fields` — Replace only the form content inside the built-in form dialog
221
262
  *
222
- * @example Minimal usage
263
+ * @example Minimal usage (auto-generated dialogs from schema)
223
264
  * <CnIndexPage
224
- * title="Publications"
265
+ * title="Clients"
225
266
  * :schema="schema"
226
- * :objects="publications"
267
+ * :objects="clients"
227
268
  * :pagination="pagination"
228
269
  * :loading="loading"
229
- * :search-value="search"
230
- * @search="onSearch"
231
- * @row-click="openPublication"
270
+ * @create="onCreate"
271
+ * @edit="onEdit"
272
+ * @delete="onDelete"
273
+ * @refresh="fetchClients"
274
+ * @row-click="openClient"
232
275
  * @page-changed="onPage" />
233
276
  *
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>
277
+ * @example With custom form dialog
278
+ * <CnIndexPage ...>
279
+ * <template #form-dialog="{ item, schema, close }">
280
+ * <MyCustomFormDialog :item="item" @close="close" />
257
281
  * </template>
258
282
  * </CnIndexPage>
259
283
  *
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
- * }
284
+ * @event {void} add — Add button clicked (backward compat, only if listener attached)
285
+ * @event {object} create — Form dialog create confirmed. Payload: formData object
286
+ * @event {object} edit — Form dialog edit confirmed. Payload: formData object (includes id)
287
+ * @event {string} delete — Single delete confirmed. Payload: item ID
288
+ * @event {{ id: string, newName: string }} copy — Single copy confirmed
289
+ * @event {string[]} mass-delete Mass delete confirmed. Payload: array of IDs
290
+ * @event {object} mass-copy — Mass copy confirmed. Payload: { ids, pattern }
291
+ * @event {object} mass-export — Mass export confirmed. Payload: { ids, format }
292
+ * @event {object} mass-import — Mass import confirmed. Payload: import data
293
+ * @event {void} refresh Refresh button clicked
294
+ * @event {object} row-click — Table row or card clicked. Payload: row object
295
+ * @event {{ key: string, order: string }} sort — Column sort changed
296
+ * @event {number} page-changed Pagination page changed
297
+ * @event {number} page-size-changed — Pagination page size changed
298
+ * @event {string[]} select — Selection changed. Payload: array of selected IDs
299
+ * @event {object} action Row action triggered. Payload: { action, row }
300
+ *
301
+ * @slot mass-actions — Extra mass action buttons (shown when items are selected)
302
+ * @slot action-items — Extra action bar buttons
303
+ * @slot header-actions — Extra buttons in the page header
304
+ * @slot delete-dialog — Replace the single-item delete dialog. Scope: `{ item, close }`
305
+ * @slot copy-dialog — Replace the single-item copy dialog. Scope: `{ item, close }`
306
+ * @slot form-dialog — Replace the create/edit form dialog. Scope: `{ item, schema, close }`
307
+ * @slot form-fields — Replace form content inside the built-in CnFormDialog. Scope: `{ fields, formData, errors, updateField }`
308
+ * @slot import-fields — Extra fields in the import dialog
309
+ * @slot empty — Custom empty state content
310
+ * @slot card — Custom card template for card view. Scope: `{ row }`
311
+ * @slot row-actions — Custom row actions. Scope: `{ row }`
312
+ * @slot column-{key} — Custom cell renderer for a specific column. Scope: `{ row, value }`
279
313
  */
280
314
  export default {
281
315
  name: 'CnIndexPage',
@@ -284,18 +318,20 @@ export default {
284
318
  NcLoadingIcon,
285
319
  NcEmptyContent,
286
320
  DatabaseSearch,
321
+ CnPageHeader,
322
+ CnActionsBar,
323
+ CnIcon,
287
324
  CnDataTable,
288
325
  CnCardGrid,
289
326
  CnPagination,
290
- CnFilterBar,
291
- CnFacetSidebar,
292
- CnViewModeToggle,
293
327
  CnRowActions,
294
- CnMassActionBar,
295
328
  CnMassDeleteDialog,
296
329
  CnMassCopyDialog,
297
330
  CnMassExportDialog,
298
331
  CnMassImportDialog,
332
+ CnDeleteDialog,
333
+ CnCopyDialog,
334
+ CnFormDialog,
299
335
  },
300
336
 
301
337
  props: {
@@ -304,6 +340,24 @@ export default {
304
340
  type: String,
305
341
  required: true,
306
342
  },
343
+ /** Optional description shown below the title */
344
+ description: {
345
+ type: String,
346
+ default: '',
347
+ },
348
+ /**
349
+ * Whether to show the page header (icon, title, description) inline.
350
+ * When false (default), the title is shown in the sidebar header instead.
351
+ */
352
+ showTitle: {
353
+ type: Boolean,
354
+ default: false,
355
+ },
356
+ /** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
357
+ icon: {
358
+ type: String,
359
+ default: '',
360
+ },
307
361
  /** Schema definition */
308
362
  schema: {
309
363
  type: Object,
@@ -329,36 +383,6 @@ export default {
329
383
  type: Boolean,
330
384
  default: false,
331
385
  },
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
386
  /** Whether rows/cards can be selected */
363
387
  selectable: {
364
388
  type: Boolean,
@@ -405,7 +429,7 @@ export default {
405
429
  type: Object,
406
430
  default: () => ({}),
407
431
  },
408
- /** Row action definitions */
432
+ /** Row action definitions (app-provided, merged with built-in actions) */
409
433
  actions: {
410
434
  type: Array,
411
435
  default: () => [],
@@ -415,21 +439,21 @@ export default {
415
439
  type: String,
416
440
  default: 'No items found',
417
441
  },
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
442
  /** Function returning CSS class(es) for a row */
429
443
  rowClass: {
430
444
  type: Function,
431
445
  default: null,
432
446
  },
447
+ /** Override label for the Add button. Defaults to "Add {schema.title}" */
448
+ addLabel: {
449
+ type: String,
450
+ default: '',
451
+ },
452
+ /** How many action buttons to show inline (rest go in overflow dropdown) */
453
+ inlineActionCount: {
454
+ type: Number,
455
+ default: 2,
456
+ },
433
457
  /** Whether to show the built-in mass Import action */
434
458
  showMassImport: {
435
459
  type: Boolean,
@@ -450,7 +474,7 @@ export default {
450
474
  type: Boolean,
451
475
  default: true,
452
476
  },
453
- /** Property name used to display item names in mass action dialogs */
477
+ /** Property name used to display item names in dialogs */
454
478
  massActionNameField: {
455
479
  type: String,
456
480
  default: 'title',
@@ -468,41 +492,146 @@ export default {
468
492
  type: Array,
469
493
  default: () => [],
470
494
  },
495
+ /** Whether to show the built-in form dialog for Add/Edit */
496
+ showFormDialog: {
497
+ type: Boolean,
498
+ default: true,
499
+ },
500
+ /** Whether to add an Edit action to row actions */
501
+ showEditAction: {
502
+ type: Boolean,
503
+ default: true,
504
+ },
505
+ /** Whether to add a Copy action to row actions */
506
+ showCopyAction: {
507
+ type: Boolean,
508
+ default: true,
509
+ },
510
+ /** Whether to add a Delete action to row actions */
511
+ showDeleteAction: {
512
+ type: Boolean,
513
+ default: true,
514
+ },
515
+ /** Field keys to exclude from the form dialog */
516
+ excludeFields: {
517
+ type: Array,
518
+ default: () => [],
519
+ },
520
+ /** Field keys to include in the form dialog (whitelist mode) */
521
+ includeFields: {
522
+ type: Array,
523
+ default: null,
524
+ },
525
+ /** Per-field overrides passed to CnFormDialog */
526
+ fieldOverrides: {
527
+ type: Object,
528
+ default: () => ({}),
529
+ },
530
+ /** Whether to show the Cards/Table view toggle in the actions bar */
531
+ showViewToggle: {
532
+ type: Boolean,
533
+ default: true,
534
+ },
471
535
  },
472
536
 
473
537
  data() {
474
538
  return {
475
539
  currentViewMode: this.viewMode,
476
- showDeleteDialog: false,
477
- showCopyDialog: false,
540
+ internalSelectedIds: [...this.selectedIds],
541
+ // Mass action dialogs
542
+ showMassDeleteDialog: false,
543
+ showMassCopyDialog: false,
478
544
  showExportDialog: false,
479
545
  showImportDialog: false,
546
+ // Single-object dialogs
547
+ showSingleDeleteDialog: false,
548
+ showSingleCopyDialog: false,
549
+ showFormDialogVisible: false,
550
+ // Dialog targets
551
+ actionTargetItem: null,
552
+ editItem: null,
480
553
  }
481
554
  },
482
555
 
483
556
  computed: {
484
- countText() {
485
- if (!this.pagination) return ''
486
- return `Showing ${this.objects.length} of ${this.pagination.total}`
557
+ /** Resolved icon — explicit prop overrides schema.icon */
558
+ resolvedIcon() {
559
+ if (this.icon) return this.icon
560
+ return this.schema?.icon || ''
561
+ },
562
+
563
+ /** Resolved schema icon component for View action */
564
+ schemaIconComponent() {
565
+ if (this.resolvedIcon && ICON_MAP[this.resolvedIcon]) {
566
+ return ICON_MAP[this.resolvedIcon]
567
+ }
568
+ return Eye
569
+ },
570
+
571
+ /** Built-in row actions based on show*Action props */
572
+ defaultActions() {
573
+ const builtIn = []
574
+ if (this.$listeners && this.$listeners['row-click']) {
575
+ builtIn.push({
576
+ label: 'View',
577
+ icon: this.schemaIconComponent,
578
+ handler: (row) => {
579
+ this.$emit('row-click', row)
580
+ },
581
+ })
582
+ }
583
+ if (this.showEditAction) {
584
+ builtIn.push({
585
+ label: 'Edit',
586
+ icon: Pencil,
587
+ handler: (row) => {
588
+ this.editItem = row
589
+ this.showFormDialogVisible = true
590
+ },
591
+ })
592
+ }
593
+ if (this.showCopyAction) {
594
+ builtIn.push({
595
+ label: 'Copy',
596
+ icon: ContentCopy,
597
+ handler: (row) => {
598
+ this.actionTargetItem = row
599
+ this.showSingleCopyDialog = true
600
+ },
601
+ })
602
+ }
603
+ if (this.showDeleteAction) {
604
+ builtIn.push({
605
+ label: 'Delete',
606
+ icon: TrashCanOutline,
607
+ destructive: true,
608
+ handler: (row) => {
609
+ this.actionTargetItem = row
610
+ this.showSingleDeleteDialog = true
611
+ },
612
+ })
613
+ }
614
+ return builtIn
487
615
  },
488
616
 
489
- showSidebar() {
490
- return this.$scopedSlots.sidebar || this.facetData !== null
617
+ /** Merged actions: app-provided first, then built-in defaults */
618
+ mergedActions() {
619
+ return [...this.actions, ...this.defaultActions]
491
620
  },
492
621
 
493
622
  hasRowActions() {
494
- return this.$scopedSlots['row-actions'] || this.actions.length > 0
623
+ return this.$scopedSlots['row-actions'] || this.mergedActions.length > 0
495
624
  },
496
625
 
497
626
  /** Whether all visible items are selected */
498
627
  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]))
628
+ if (this.objects.length === 0 || this.internalSelectedIds.length === 0) return false
629
+ return this.objects.every((o) => this.internalSelectedIds.includes(o[this.rowKey]))
501
630
  },
502
631
 
503
632
  /** Full objects for the selected IDs (used by mass action dialogs) */
504
633
  selectedObjects() {
505
- return this.objects.filter((o) => this.selectedIds.includes(o[this.rowKey]))
634
+ return this.objects.filter((o) => this.internalSelectedIds.includes(o[this.rowKey]))
506
635
  },
507
636
 
508
637
  /** Column slot names that the parent has provided (for pass-through) */
@@ -511,172 +640,177 @@ export default {
511
640
  .filter((name) => name.startsWith('column-'))
512
641
  .map((name) => name.replace('column-', ''))
513
642
  },
643
+
644
+ /** Add button label — derived from schema.title if not explicitly set */
645
+ resolvedAddLabel() {
646
+ if (this.addLabel) return this.addLabel
647
+ return 'Add ' + (this.schema?.title || 'Item')
648
+ },
514
649
  },
515
650
 
516
651
  watch: {
517
652
  viewMode(val) {
518
653
  this.currentViewMode = val
519
654
  },
655
+ selectedIds(val) {
656
+ this.internalSelectedIds = [...val]
657
+ },
520
658
  },
521
659
 
522
660
  methods: {
661
+ /**
662
+ * Handle the Add button click. If the consumer listens to @add,
663
+ * emit the event (backward compatible). Otherwise open the form dialog.
664
+ */
665
+ onAddClick() {
666
+ if (this.$listeners && this.$listeners.add) {
667
+ this.$emit('add')
668
+ } else if (this.showFormDialog) {
669
+ this.editItem = null
670
+ this.showFormDialogVisible = true
671
+ }
672
+ },
673
+
674
+ /**
675
+ * Handle view mode toggle.
676
+ * @param {string} mode 'table' or 'cards'
677
+ */
523
678
  onViewModeChange(mode) {
524
679
  this.currentViewMode = mode
525
680
  this.$emit('view-mode-change', mode)
526
681
  },
527
682
 
528
683
  /**
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
684
+ * Handle selection changes from CnDataTable/CnCardGrid.
685
+ * Updates internal state and re-emits for parent.
686
+ * @param {Array} ids Array of selected row IDs
532
687
  */
688
+ onSelect(ids) {
689
+ this.internalSelectedIds = ids
690
+ this.$emit('select', ids)
691
+ },
692
+
693
+ // --- Mass action handlers ---
694
+
533
695
  onMassDeleteConfirm(ids) {
534
696
  this.$emit('mass-delete', ids)
535
697
  },
536
698
 
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
699
  onMassCopyConfirm(payload) {
543
700
  this.$emit('mass-copy', payload)
544
701
  },
545
702
 
546
- /**
547
- * Set the result of a mass delete operation. Call from parent after API call.
548
- * @param {{ success?: boolean, error?: string }} resultData
549
- * @public
550
- */
551
- setDeleteResult(resultData) {
552
- if (this.$refs.deleteDialog) {
553
- this.$refs.deleteDialog.setResult(resultData)
554
- }
555
- },
556
-
557
- /**
558
- * Set the result of a mass copy operation. Call from parent after API call.
559
- * @param {{ success?: boolean, error?: string }} resultData
560
- * @public
561
- */
562
- setCopyResult(resultData) {
563
- if (this.$refs.copyDialog) {
564
- this.$refs.copyDialog.setResult(resultData)
565
- }
566
- },
567
-
568
- /**
569
- * Handle mass export confirm.
570
- * @param {{ format: string }} payload
571
- */
572
703
  onMassExportConfirm(payload) {
573
704
  this.$emit('mass-export', payload)
574
705
  },
575
706
 
576
- /**
577
- * Handle mass import confirm.
578
- * @param {{ file: File, options: object }} payload
579
- */
580
707
  onMassImportConfirm(payload) {
581
708
  this.$emit('mass-import', payload)
582
709
  },
583
710
 
584
- /**
585
- * Set the result of a mass export operation.
586
- * @param {{ success?: boolean, error?: string }} resultData
587
- * @public
588
- */
711
+ /** @public Forward result to mass delete dialog */
712
+ setMassDeleteResult(resultData) {
713
+ if (this.$refs.massDeleteDialog) {
714
+ this.$refs.massDeleteDialog.setResult(resultData)
715
+ }
716
+ },
717
+
718
+ /** @public Forward result to mass copy dialog */
719
+ setMassCopyResult(resultData) {
720
+ if (this.$refs.massCopyDialog) {
721
+ this.$refs.massCopyDialog.setResult(resultData)
722
+ }
723
+ },
724
+
725
+ /** @public Forward result to export dialog */
589
726
  setExportResult(resultData) {
590
727
  if (this.$refs.exportDialog) {
591
728
  this.$refs.exportDialog.setResult(resultData)
592
729
  }
593
730
  },
594
731
 
595
- /**
596
- * Set the result of a mass import operation.
597
- * @param {{ success?: boolean, error?: string, summary?: object }} resultData
598
- * @public
599
- */
732
+ /** @public Forward result to import dialog */
600
733
  setImportResult(resultData) {
601
734
  if (this.$refs.importDialog) {
602
735
  this.$refs.importDialog.setResult(resultData)
603
736
  }
604
737
  },
605
- },
606
- }
607
- </script>
608
738
 
609
- <style scoped>
610
- .cn-index-page {
611
- padding: 20px;
612
- }
613
-
614
- .cn-index-page__header {
615
- display: flex;
616
- justify-content: space-between;
617
- align-items: center;
618
- margin-bottom: 16px;
619
- flex-wrap: wrap;
620
- gap: 12px;
621
- }
739
+ // --- Backward-compatible aliases ---
740
+ /** @public @deprecated Use setMassDeleteResult instead */
741
+ setDeleteResult(resultData) {
742
+ this.setMassDeleteResult(resultData)
743
+ },
744
+ /** @public @deprecated Use setMassCopyResult instead */
745
+ setCopyResult(resultData) {
746
+ this.setMassCopyResult(resultData)
747
+ },
622
748
 
623
- .cn-index-page__title-area {
624
- display: flex;
625
- align-items: baseline;
626
- gap: 8px;
627
- }
749
+ // --- Single-object dialog handlers ---
628
750
 
629
- .cn-index-page__title {
630
- margin: 0;
631
- font-size: 22px;
632
- font-weight: 700;
633
- }
751
+ onSingleDeleteConfirm(id) {
752
+ this.$emit('delete', id)
753
+ },
634
754
 
635
- .cn-index-page__count {
636
- font-size: 14px;
637
- color: var(--color-text-maxcontrast);
638
- }
755
+ onSingleCopyConfirm(payload) {
756
+ this.$emit('copy', payload)
757
+ },
639
758
 
640
- .cn-index-page__header-actions {
641
- display: flex;
642
- align-items: center;
643
- gap: 8px;
644
- }
759
+ onFormConfirm(formData) {
760
+ if (this.editItem) {
761
+ this.$emit('edit', formData)
762
+ } else {
763
+ this.$emit('create', formData)
764
+ }
765
+ },
645
766
 
646
- .cn-index-page__body {
647
- display: flex;
648
- gap: 0;
649
- }
767
+ closeSingleDelete() {
768
+ this.showSingleDeleteDialog = false
769
+ this.actionTargetItem = null
770
+ },
650
771
 
651
- .cn-index-page__body--with-sidebar {
652
- gap: 0;
653
- }
772
+ closeSingleCopy() {
773
+ this.showSingleCopyDialog = false
774
+ this.actionTargetItem = null
775
+ },
654
776
 
655
- .cn-index-page__sidebar {
656
- flex-shrink: 0;
657
- }
777
+ closeFormDialog() {
778
+ this.showFormDialogVisible = false
779
+ this.editItem = null
780
+ },
658
781
 
659
- .cn-index-page__main {
660
- flex: 1;
661
- min-width: 0;
662
- }
782
+ /** @public Forward result to single delete dialog */
783
+ setSingleDeleteResult(resultData) {
784
+ if (this.$refs.singleDeleteDialog) {
785
+ this.$refs.singleDeleteDialog.setResult(resultData)
786
+ }
787
+ },
663
788
 
664
- .cn-index-page__search {
665
- margin-bottom: 16px;
666
- }
789
+ /** @public Forward result to single copy dialog */
790
+ setSingleCopyResult(resultData) {
791
+ if (this.$refs.singleCopyDialog) {
792
+ this.$refs.singleCopyDialog.setResult(resultData)
793
+ }
794
+ },
667
795
 
668
- .cn-index-page__loading {
669
- display: flex;
670
- justify-content: center;
671
- padding: 60px;
672
- }
796
+ /** @public Forward result to form dialog */
797
+ setFormResult(resultData) {
798
+ if (this.$refs.formDialog) {
799
+ this.$refs.formDialog.setResult(resultData)
800
+ }
801
+ },
673
802
 
674
- .cn-index-page__empty {
675
- padding: 40px 20px;
676
- text-align: center;
803
+ /**
804
+ * Programmatically open the form dialog.
805
+ * @param {object|null} item Pass null for create mode, or an object for edit mode
806
+ * @public
807
+ */
808
+ openFormDialog(item = null) {
809
+ this.editItem = item
810
+ this.showFormDialogVisible = true
811
+ },
812
+ },
677
813
  }
814
+ </script>
678
815
 
679
- .cn-index-page__pagination {
680
- margin-top: 16px;
681
- }
682
- </style>
816
+ <!-- Styles in css/index-page.css -->