@conduction/nextcloud-vue 0.1.0-beta.3 → 0.1.0-beta.5

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 (142) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs +67614 -0
  3. package/dist/nextcloud-vue.cjs.js +58386 -6112
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.cjs.map +1 -0
  6. package/dist/nextcloud-vue.css +1819 -285
  7. package/dist/nextcloud-vue.esm.js +58342 -6088
  8. package/dist/nextcloud-vue.esm.js.map +1 -1
  9. package/package.json +82 -62
  10. package/src/components/CnActionsBar/CnActionsBar.vue +17 -7
  11. package/src/components/CnActionsBar/index.js +1 -1
  12. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  13. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  14. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  17. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  18. package/src/components/CnCardGrid/CnCardGrid.vue +1 -1
  19. package/src/components/CnCardGrid/index.js +1 -1
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +1 -1
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  32. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  33. package/src/components/CnDetailCard/index.js +1 -0
  34. package/src/components/CnDetailPage/CnDetailPage.vue +285 -0
  35. package/src/components/CnDetailPage/index.js +1 -0
  36. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +9 -1
  37. package/src/components/CnFacetSidebar/index.js +1 -1
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +71 -3
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +121 -102
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/index.js +1 -1
  49. package/src/components/CnMassCopyDialog/index.js +1 -1
  50. package/src/components/CnMassDeleteDialog/index.js +1 -1
  51. package/src/components/CnMassExportDialog/index.js +1 -1
  52. package/src/components/CnMassImportDialog/index.js +1 -1
  53. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  54. package/src/components/CnNoteCard/index.js +1 -0
  55. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  56. package/src/components/CnNotesCard/index.js +1 -0
  57. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  58. package/src/components/CnObjectCard/index.js +1 -1
  59. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  60. package/src/components/CnObjectSidebar/index.js +1 -0
  61. package/src/components/CnPageHeader/index.js +1 -1
  62. package/src/components/CnPagination/index.js +1 -1
  63. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  64. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  65. package/src/components/CnRowActions/index.js +1 -1
  66. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  67. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  68. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  69. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  70. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  71. package/src/components/CnSchemaFormDialog/index.js +1 -0
  72. package/src/components/CnSettingsCard/index.js +1 -1
  73. package/src/components/CnSettingsSection/index.js +1 -1
  74. package/src/components/CnStatsBlock/CnStatsBlock.vue +62 -8
  75. package/src/components/CnStatsBlock/index.js +1 -1
  76. package/src/components/CnStatusBadge/index.js +1 -1
  77. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  78. package/src/components/CnTabbedFormDialog/index.js +1 -0
  79. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  80. package/src/components/CnTasksCard/index.js +1 -0
  81. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  82. package/src/components/CnTileWidget/index.js +1 -0
  83. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  84. package/src/components/CnTimelineStages/index.js +1 -0
  85. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  86. package/src/components/CnUserActionMenu/index.js +1 -0
  87. package/src/components/CnVersionInfoCard/index.js +1 -1
  88. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  89. package/src/components/CnWidgetRenderer/index.js +1 -0
  90. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  91. package/src/components/CnWidgetWrapper/index.js +1 -0
  92. package/src/components/index.js +43 -29
  93. package/src/composables/index.js +4 -3
  94. package/src/composables/useDashboardView.js +240 -0
  95. package/src/composables/useDetailView.js +289 -132
  96. package/src/composables/useListView.js +363 -362
  97. package/src/composables/useSubResource.js +142 -142
  98. package/src/constants/metadata.js +30 -30
  99. package/src/css/CnSchemaFormDialog.css +546 -0
  100. package/src/css/__sample_nextcloud_tokens.css +110 -0
  101. package/src/css/actions-bar.css +48 -48
  102. package/src/css/badge.css +51 -51
  103. package/src/css/card.css +128 -128
  104. package/src/css/dashboard.css +70 -0
  105. package/src/css/detail-page.css +168 -0
  106. package/src/css/detail.css +68 -68
  107. package/src/css/index-page.css +44 -32
  108. package/src/css/index-sidebar.css +193 -187
  109. package/src/css/index.css +16 -12
  110. package/src/css/layout.css +90 -90
  111. package/src/css/page-header.css +33 -33
  112. package/src/css/pagination.css +72 -72
  113. package/src/css/table.css +142 -142
  114. package/src/css/timeline-stages.css +218 -0
  115. package/src/css/utilities.css +46 -46
  116. package/src/index.js +72 -53
  117. package/src/store/createSubResourcePlugin.js +135 -135
  118. package/src/store/index.js +3 -3
  119. package/src/store/plugins/auditTrails.js +17 -17
  120. package/src/store/plugins/files.js +250 -186
  121. package/src/store/plugins/index.js +7 -5
  122. package/src/store/plugins/lifecycle.js +180 -180
  123. package/src/store/plugins/relations.js +68 -68
  124. package/src/store/plugins/search.js +372 -0
  125. package/src/store/plugins/selection.js +104 -0
  126. package/src/store/useObjectStore.js +829 -686
  127. package/src/types/auditTrail.d.ts +32 -32
  128. package/src/types/file.d.ts +23 -23
  129. package/src/types/index.d.ts +35 -35
  130. package/src/types/notification.d.ts +36 -36
  131. package/src/types/object.d.ts +40 -40
  132. package/src/types/organisation.d.ts +41 -41
  133. package/src/types/register.d.ts +25 -25
  134. package/src/types/schema.d.ts +39 -39
  135. package/src/types/shared.d.ts +79 -79
  136. package/src/types/source.d.ts +14 -14
  137. package/src/types/task.d.ts +31 -31
  138. package/src/utils/errors.js +96 -96
  139. package/src/utils/headers.js +68 -50
  140. package/src/utils/id.js +13 -0
  141. package/src/utils/index.js +3 -3
  142. package/src/utils/schema.js +422 -419
@@ -89,18 +89,32 @@
89
89
  </span>
90
90
  </div>
91
91
 
92
- <!-- Select (enum) -->
92
+ <!-- Select (enum, supports async function) -->
93
93
  <div v-else-if="field.widget === 'select'" class="cn-form-dialog__select-wrapper">
94
94
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
95
95
  {{ field.label }}{{ field.required ? ' *' : '' }}
96
96
  </label>
97
97
  <NcSelect
98
98
  :input-id="'cn-form-' + field.key"
99
- :options="getEnumOptions(field)"
100
- :value="getSelectedEnumOption(field)"
99
+ :options="getEffectiveOptions(field)"
100
+ :value="getEffectiveSelectedOption(field)"
101
101
  :clearable="!field.required"
102
102
  :disabled="field.readOnly"
103
- @input="onSelectChange(field.key, $event)" />
103
+ :loading="isFieldLoading(field)"
104
+ :filterable="!isAsyncEnum(field)"
105
+ @input="onEffectiveSelectChange(field, $event)"
106
+ @search="isAsyncEnum(field) ? onAsyncSearch(field, $event) : undefined">
107
+ <template
108
+ v-if="$scopedSlots['field-' + field.key + '-option']"
109
+ #option="optionProps">
110
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
111
+ </template>
112
+ <template
113
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
114
+ #selected-option="optionProps">
115
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
116
+ </template>
117
+ </NcSelect>
104
118
  <span
105
119
  v-if="errors[field.key] || field.description"
106
120
  class="cn-form-dialog__helper"
@@ -109,19 +123,33 @@
109
123
  </span>
110
124
  </div>
111
125
 
112
- <!-- Multiselect (array with enum items) -->
126
+ <!-- Multiselect (array with enum items, supports async function) -->
113
127
  <div v-else-if="field.widget === 'multiselect'" class="cn-form-dialog__select-wrapper">
114
128
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
115
129
  {{ field.label }}{{ field.required ? ' *' : '' }}
116
130
  </label>
117
131
  <NcSelect
118
132
  :input-id="'cn-form-' + field.key"
119
- :options="getArrayEnumOptions(field)"
120
- :value="getSelectedArrayOptions(field)"
133
+ :options="getEffectiveArrayOptions(field)"
134
+ :value="getEffectiveSelectedArrayOptions(field)"
121
135
  :multiple="true"
122
136
  :clearable="true"
123
137
  :disabled="field.readOnly"
124
- @input="onMultiSelectChange(field.key, $event)" />
138
+ :loading="isFieldLoading(field)"
139
+ :filterable="!isAsyncItemsEnum(field)"
140
+ @input="onEffectiveMultiSelectChange(field, $event)"
141
+ @search="isAsyncItemsEnum(field) ? onAsyncSearch(field, $event) : undefined">
142
+ <template
143
+ v-if="$scopedSlots['field-' + field.key + '-option']"
144
+ #option="optionProps">
145
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
146
+ </template>
147
+ <template
148
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
149
+ #selected-option="optionProps">
150
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
151
+ </template>
152
+ </NcSelect>
125
153
  <span
126
154
  v-if="errors[field.key] || field.description"
127
155
  class="cn-form-dialog__helper"
@@ -130,19 +158,35 @@
130
158
  </span>
131
159
  </div>
132
160
 
133
- <!-- Tags (array, freeform) -->
161
+ <!-- Tags (array, freeform, supports async suggestions) -->
134
162
  <div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
135
163
  <label :for="'cn-form-' + field.key" class="cn-form-dialog__label">
136
164
  {{ field.label }}{{ field.required ? ' *' : '' }}
137
165
  </label>
166
+ <!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
138
167
  <NcSelect
139
168
  :input-id="'cn-form-' + field.key"
140
169
  :value="formData[field.key] || []"
170
+ :options="isFieldAsync(field) ? ((asyncState[field.key] && asyncState[field.key].options) || []) : []"
141
171
  :multiple="true"
142
172
  :taggable="true"
143
173
  :clearable="true"
144
174
  :disabled="field.readOnly"
145
- @input="updateField(field.key, $event)" />
175
+ :loading="isFieldLoading(field)"
176
+ :filterable="!isFieldAsync(field)"
177
+ @input="updateField(field.key, $event)"
178
+ @search="isFieldAsync(field) ? onAsyncSearch(field, $event) : undefined">
179
+ <template
180
+ v-if="$scopedSlots['field-' + field.key + '-option']"
181
+ #option="optionProps">
182
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
183
+ </template>
184
+ <template
185
+ v-if="$scopedSlots['field-' + field.key + '-selected-option']"
186
+ #selected-option="optionProps">
187
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
188
+ </template>
189
+ </NcSelect>
146
190
  <span
147
191
  v-if="errors[field.key] || field.description"
148
192
  class="cn-form-dialog__helper"
@@ -235,8 +279,24 @@ import { fieldsFromSchema } from '../../utils/schema.js'
235
279
  *
236
280
  * - `#form` — Replace the entire form content
237
281
  * - `#field-{key}` — Replace a single auto-generated field
282
+ * - `#field-{key}-option` — Customize dropdown option rendering for a select/multiselect/tags field
283
+ * - `#field-{key}-selected-option` — Customize selected option display for a select/multiselect/tags field
238
284
  * - `#before-fields` / `#after-fields` — Inject content around fields
239
285
  *
286
+ * ## Async select support
287
+ *
288
+ * Select, multiselect, and tags fields support async options by setting `field.enum`
289
+ * (or `field.items.enum` for multiselect) to an async function instead of a static array:
290
+ *
291
+ * ```js
292
+ * { key: 'org', widget: 'select', enum: async (query) => fetchOrgs(query) }
293
+ * ```
294
+ *
295
+ * The function receives the search query and must return an array of option objects
296
+ * (each must have a `label` property for default display). Options are loaded on mount
297
+ * (with empty query) and on each search input (debounced, default 300ms, configurable
298
+ * via `field.debounce`). Async selects store the full option object in formData.
299
+ *
240
300
  * The dialog does NOT perform the save itself — it emits a `confirm` event
241
301
  * with the form data. The parent performs the actual API call and calls
242
302
  * `setResult()` via a ref.
@@ -266,6 +326,27 @@ import { fieldsFromSchema } from '../../utils/schema.js'
266
326
  * this.$refs.formDialog.setResult({ error: e.message })
267
327
  * }
268
328
  * }
329
+ *
330
+ * @example <caption>Async select with custom option rendering</caption>
331
+ * <CnFormDialog :fields="[{
332
+ * key: 'organisation',
333
+ * widget: 'select',
334
+ * label: 'Organisation',
335
+ * required: true,
336
+ * enum: async (query) => {
337
+ * const results = await store.search(query)
338
+ * return results.map(o => ({ label: o.name, id: o.uuid, ...o }))
339
+ * },
340
+ * debounce: 500,
341
+ * }]" @confirm="onConfirm">
342
+ * <template #field-organisation-option="{ name, description }">
343
+ * <strong>{{ name }}</strong>
344
+ * <p>{{ description }}</p>
345
+ * </template>
346
+ * <template #field-organisation-selected-option="{ name }">
347
+ * {{ name }}
348
+ * </template>
349
+ * </CnFormDialog>
269
350
  */
270
351
  export default {
271
352
  name: 'CnFormDialog',
@@ -349,6 +430,8 @@ export default {
349
430
  loading: false,
350
431
  result: null,
351
432
  closeTimeout: null,
433
+ /** Per-field async state: { [fieldKey]: { options: [], loading: false, searchTimeout: null } } */
434
+ asyncState: {},
352
435
  }
353
436
  },
354
437
 
@@ -400,6 +483,13 @@ export default {
400
483
  },
401
484
  },
402
485
 
486
+ beforeDestroy() {
487
+ for (const state of Object.values(this.asyncState)) {
488
+ if (state.searchTimeout) clearTimeout(state.searchTimeout)
489
+ }
490
+ if (this.closeTimeout) clearTimeout(this.closeTimeout)
491
+ },
492
+
403
493
  methods: {
404
494
  initFormData(item) {
405
495
  if (item) {
@@ -422,6 +512,7 @@ export default {
422
512
  this.formData = data
423
513
  }
424
514
  this.errors = {}
515
+ this.initAsyncFields()
425
516
  },
426
517
 
427
518
  updateField(key, value) {
@@ -468,6 +559,205 @@ export default {
468
559
  this.updateField(key, (options || []).map((o) => o.id))
469
560
  },
470
561
 
562
+ /**
563
+ * Check if a field has an async enum (function instead of static array).
564
+ *
565
+ * @param {object} field The field definition
566
+ * @return {boolean}
567
+ */
568
+ isAsyncEnum(field) {
569
+ return typeof field.enum === 'function'
570
+ },
571
+
572
+ /**
573
+ * Check if an array field has an async items enum.
574
+ *
575
+ * @param {object} field The field definition
576
+ * @return {boolean}
577
+ */
578
+ isAsyncItemsEnum(field) {
579
+ return !!(field.items && typeof field.items.enum === 'function')
580
+ },
581
+
582
+ /**
583
+ * Initialize async state for all async fields and trigger initial load.
584
+ */
585
+ initAsyncFields() {
586
+ // Clean up existing timeouts
587
+ for (const state of Object.values(this.asyncState)) {
588
+ if (state.searchTimeout) clearTimeout(state.searchTimeout)
589
+ }
590
+
591
+ const newState = {}
592
+ for (const field of this.resolvedFields) {
593
+ if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
594
+ newState[field.key] = { options: [], loading: false, searchTimeout: null }
595
+ }
596
+ }
597
+ this.asyncState = newState
598
+
599
+ // Trigger initial load for each async field
600
+ this.$nextTick(() => {
601
+ for (const field of this.resolvedFields) {
602
+ if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
603
+ this.loadAsyncOptions(field, '')
604
+ }
605
+ }
606
+ })
607
+ },
608
+
609
+ /**
610
+ * Load async options for a field by calling its enum function.
611
+ *
612
+ * @param {object} field The field definition
613
+ * @param {string} query Search query
614
+ */
615
+ async loadAsyncOptions(field, query) {
616
+ const state = this.asyncState[field.key]
617
+ if (!state) return
618
+
619
+ this.$set(state, 'loading', true)
620
+
621
+ try {
622
+ const enumFn = this.isAsyncEnum(field) ? field.enum : field.items.enum
623
+ const results = await enumFn(query)
624
+ this.$set(state, 'options', Array.isArray(results) ? results : [])
625
+ } catch (err) {
626
+ console.error(`CnFormDialog: async enum error for field "${field.key}":`, err)
627
+ this.$set(state, 'options', [])
628
+ } finally {
629
+ this.$set(state, 'loading', false)
630
+ }
631
+ },
632
+
633
+ /**
634
+ * Handle search input on an async select with debounce.
635
+ *
636
+ * @param {object} field The field definition
637
+ * @param {string} query Search query
638
+ */
639
+ onAsyncSearch(field, query) {
640
+ const state = this.asyncState[field.key]
641
+ if (!state) return
642
+
643
+ if (state.searchTimeout) {
644
+ clearTimeout(state.searchTimeout)
645
+ }
646
+
647
+ const debounceMs = field.debounce || 300
648
+
649
+ state.searchTimeout = setTimeout(() => {
650
+ this.loadAsyncOptions(field, query || '')
651
+ }, debounceMs)
652
+ },
653
+
654
+ /**
655
+ * Get the effective options for a select field (async or static).
656
+ *
657
+ * @param {object} field The field definition
658
+ * @return {Array}
659
+ */
660
+ getEffectiveOptions(field) {
661
+ if (this.isAsyncEnum(field)) {
662
+ // TODO: restore to `this.asyncState[field.key]?.options || []` once on Vue 3 (buble doesn't support optional chaining)
663
+ return (this.asyncState[field.key] && this.asyncState[field.key].options) || []
664
+ }
665
+ return this.getEnumOptions(field)
666
+ },
667
+
668
+ /**
669
+ * Get the effective selected value for a select field (async or static).
670
+ *
671
+ * @param {object} field The field definition
672
+ * @return {object|null}
673
+ */
674
+ getEffectiveSelectedOption(field) {
675
+ if (this.isAsyncEnum(field)) {
676
+ // For async fields, formData stores the full option object
677
+ return this.formData[field.key] || null
678
+ }
679
+ return this.getSelectedEnumOption(field)
680
+ },
681
+
682
+ /**
683
+ * Handle select change for both async and static fields.
684
+ *
685
+ * @param {object} field The field definition
686
+ * @param {object|null} option The selected option
687
+ */
688
+ onEffectiveSelectChange(field, option) {
689
+ if (this.isAsyncEnum(field)) {
690
+ // Store full option object for async selects
691
+ this.updateField(field.key, option || null)
692
+ } else {
693
+ this.onSelectChange(field.key, option)
694
+ }
695
+ },
696
+
697
+ /**
698
+ * Get effective options for a multiselect field (async or static).
699
+ *
700
+ * @param {object} field The field definition
701
+ * @return {Array}
702
+ */
703
+ getEffectiveArrayOptions(field) {
704
+ if (this.isAsyncItemsEnum(field)) {
705
+ // TODO: restore to `this.asyncState[field.key]?.options || []` once on Vue 3 (buble doesn't support optional chaining)
706
+ return (this.asyncState[field.key] && this.asyncState[field.key].options) || []
707
+ }
708
+ return this.getArrayEnumOptions(field)
709
+ },
710
+
711
+ /**
712
+ * Get effective selected values for a multiselect field (async or static).
713
+ *
714
+ * @param {object} field The field definition
715
+ * @return {Array}
716
+ */
717
+ getEffectiveSelectedArrayOptions(field) {
718
+ if (this.isAsyncItemsEnum(field)) {
719
+ // For async fields, formData stores array of full option objects
720
+ return this.formData[field.key] || []
721
+ }
722
+ return this.getSelectedArrayOptions(field)
723
+ },
724
+
725
+ /**
726
+ * Handle multiselect change for both async and static fields.
727
+ *
728
+ * @param {object} field The field definition
729
+ * @param {Array} options The selected options
730
+ */
731
+ onEffectiveMultiSelectChange(field, options) {
732
+ if (this.isAsyncItemsEnum(field)) {
733
+ // Store full option objects for async multiselect
734
+ this.updateField(field.key, options || [])
735
+ } else {
736
+ this.onMultiSelectChange(field.key, options)
737
+ }
738
+ },
739
+
740
+ /**
741
+ * Whether a field's async options are currently loading.
742
+ *
743
+ * @param {object} field The field definition
744
+ * @return {boolean}
745
+ */
746
+ isFieldLoading(field) {
747
+ // TODO: restore to `this.asyncState[field.key]?.loading || false` once on Vue 3 (buble doesn't support optional chaining)
748
+ return (this.asyncState[field.key] && this.asyncState[field.key].loading) || false
749
+ },
750
+
751
+ /**
752
+ * Whether a field has any async behavior (enum or items.enum is a function).
753
+ *
754
+ * @param {object} field The field definition
755
+ * @return {boolean}
756
+ */
757
+ isFieldAsync(field) {
758
+ return this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)
759
+ },
760
+
471
761
  /**
472
762
  * Run client-side validation on all form fields.
473
763
  * Checks required, minLength, maxLength, pattern, minimum, maximum.
@@ -508,7 +798,8 @@ export default {
508
798
  if (!new RegExp(v.pattern).test(value)) {
509
799
  newErrors[field.key] = 'Invalid format.'
510
800
  }
511
- } catch {
801
+ // TODO: restore to `catch {` (optional catch binding) once on Vue 3 (buble doesn't support it)
802
+ } catch (e) {
512
803
  // Ignore invalid regex patterns
513
804
  }
514
805
  }
@@ -1 +1 @@
1
- export { default as CnIcon, ICON_MAP, registerIcons } from './CnIcon.vue'
1
+ export { default as CnIcon, ICON_MAP, registerIcons } from './CnIcon.vue'
@@ -7,6 +7,11 @@
7
7
  :description="description"
8
8
  :icon="resolvedIcon" />
9
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
+
10
15
  <!-- Actions bar -->
11
16
  <CnActionsBar
12
17
  :pagination="pagination"
@@ -22,6 +27,7 @@
22
27
  :show-mass-delete="showMassDelete"
23
28
  :view-mode="currentViewMode"
24
29
  :show-view-toggle="showViewToggle"
30
+ :refreshing="refreshing"
25
31
  @add="onAddClick"
26
32
  @refresh="$emit('refresh')"
27
33
  @show-import="showImportDialog = true"
@@ -113,7 +119,7 @@
113
119
  :schema="schema"
114
120
  :close="closeFormDialog">
115
121
  <CnFormDialog
116
- v-if="showFormDialogVisible"
122
+ v-if="showFormDialogVisible && !useAdvancedFormDialog"
117
123
  ref="formDialog"
118
124
  :schema="schema"
119
125
  :item="editItem"
@@ -127,6 +133,17 @@
127
133
  <slot name="form-fields" v-bind="scope" />
128
134
  </template>
129
135
  </CnFormDialog>
136
+ <CnAdvancedFormDialog
137
+ v-if="showFormDialogVisible && useAdvancedFormDialog"
138
+ ref="formDialog"
139
+ :schema="schema"
140
+ :item="editItem"
141
+ :exclude-fields="excludeFields"
142
+ :include-fields="includeFields"
143
+ :field-overrides="fieldOverrides"
144
+ :name-field="massActionNameField"
145
+ @confirm="onFormConfirm"
146
+ @close="closeFormDialog" />
130
147
  </slot>
131
148
 
132
149
  <!-- Body -->
@@ -246,6 +263,7 @@ import { CnMassImportDialog } from '../CnMassImportDialog/index.js'
246
263
  import { CnDeleteDialog } from '../CnDeleteDialog/index.js'
247
264
  import { CnCopyDialog } from '../CnCopyDialog/index.js'
248
265
  import { CnFormDialog } from '../CnFormDialog/index.js'
266
+ import { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'
249
267
 
250
268
  /**
251
269
  * CnIndexPage — Top-level schema-driven index page component.
@@ -258,7 +276,9 @@ import { CnFormDialog } from '../CnFormDialog/index.js'
258
276
  * - `#form-dialog` — Replace the create/edit dialog entirely
259
277
  * - `#delete-dialog` — Replace the single-item delete dialog
260
278
  * - `#copy-dialog` — Replace the single-item copy dialog
261
- * - `#form-fields` — Replace only the form content inside the built-in form dialog
279
+ * - `#form-fields` — Replace only the form content inside the built-in form dialog (CnFormDialog only)
280
+ *
281
+ * Use the `useAdvancedFormDialog` prop to use CnAdvancedFormDialog for create/edit (properties table, JSON tab, optional metadata).
262
282
  *
263
283
  * @example Minimal usage (auto-generated dialogs from schema)
264
284
  * <CnIndexPage
@@ -332,6 +352,7 @@ export default {
332
352
  CnDeleteDialog,
333
353
  CnCopyDialog,
334
354
  CnFormDialog,
355
+ CnAdvancedFormDialog,
335
356
  },
336
357
 
337
358
  props: {
@@ -497,6 +518,11 @@ export default {
497
518
  type: Boolean,
498
519
  default: true,
499
520
  },
521
+ /** Use CnAdvancedFormDialog (properties table, JSON tab, optional metadata) instead of CnFormDialog for Add/Edit */
522
+ useAdvancedFormDialog: {
523
+ type: Boolean,
524
+ default: false,
525
+ },
500
526
  /** Whether to add an Edit action to row actions */
501
527
  showEditAction: {
502
528
  type: Boolean,
@@ -532,6 +558,23 @@ export default {
532
558
  type: Boolean,
533
559
  default: true,
534
560
  },
561
+ /** Whether the refresh action is currently in progress */
562
+ refreshing: {
563
+ type: Boolean,
564
+ default: false,
565
+ },
566
+ /**
567
+ * Store instance for automatic save integration. When provided alongside
568
+ * objectType, the form dialog saves directly to the store instead of
569
+ * emitting create/edit events. The object type must already be registered
570
+ * in the store via registerObjectType() before passing the store here.
571
+ */
572
+ store: { type: Object, default: null },
573
+ /**
574
+ * Object type slug for store integration (e.g. `${registerId}-${schemaId}`).
575
+ * Required when store is set — a console warning is emitted if missing.
576
+ */
577
+ objectType: { type: String, default: '' },
535
578
  },
536
579
 
537
580
  data() {
@@ -756,7 +799,22 @@ export default {
756
799
  this.$emit('copy', payload)
757
800
  },
758
801
 
759
- onFormConfirm(formData) {
802
+ async onFormConfirm(formData) {
803
+ if (this.store) {
804
+ if (!this.objectType) {
805
+ console.warn('[CnIndexPage] store prop is set but objectType is missing. Cannot save to store.')
806
+ return
807
+ }
808
+ const saved = await this.store.saveObject(this.objectType, formData)
809
+ if (saved) {
810
+ this.setFormResult({ success: true })
811
+ this.$emit(this.editItem ? 'edit' : 'create', saved)
812
+ } else {
813
+ const err = this.store.getError?.(this.objectType)
814
+ this.setFormResult({ error: (err && err.message) || 'Save failed' })
815
+ }
816
+ return
817
+ }
760
818
  if (this.editItem) {
761
819
  this.$emit('edit', formData)
762
820
  } else {
@@ -809,6 +867,16 @@ export default {
809
867
  this.editItem = item
810
868
  this.showFormDialogVisible = true
811
869
  },
870
+
871
+ /**
872
+ * Programmatically open the single-item delete dialog.
873
+ * @param {object} item The item to delete
874
+ * @public
875
+ */
876
+ openDeleteDialog(item) {
877
+ this.actionTargetItem = item
878
+ this.showSingleDeleteDialog = true
879
+ },
812
880
  },
813
881
  }
814
882
  </script>
@@ -1 +1 @@
1
- export { default as CnIndexPage } from './CnIndexPage.vue'
1
+ export { default as CnIndexPage } from './CnIndexPage.vue'