@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.
- package/README.md +226 -226
- package/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +58386 -6112
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1819 -285
- package/dist/nextcloud-vue.esm.js +58342 -6088
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +82 -62
- package/src/components/CnActionsBar/CnActionsBar.vue +17 -7
- package/src/components/CnActionsBar/index.js +1 -1
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +1 -1
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +1 -1
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +285 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +9 -1
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
- package/src/components/CnIcon/index.js +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +71 -3
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +121 -102
- package/src/components/CnIndexSidebar/index.js +1 -1
- package/src/components/CnItemCard/CnItemCard.vue +132 -0
- package/src/components/CnItemCard/index.js +1 -0
- package/src/components/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
- package/src/components/CnObjectSidebar/index.js +1 -0
- package/src/components/CnPageHeader/index.js +1 -1
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
- package/src/components/CnRowActions/CnRowActions.vue +25 -3
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +62 -8
- package/src/components/CnStatsBlock/index.js +1 -1
- package/src/components/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +43 -29
- package/src/composables/index.js +4 -3
- package/src/composables/useDashboardView.js +240 -0
- package/src/composables/useDetailView.js +289 -132
- package/src/composables/useListView.js +363 -362
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -30
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +48 -48
- package/src/css/badge.css +51 -51
- package/src/css/card.css +128 -128
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +168 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -32
- package/src/css/index-sidebar.css +193 -187
- package/src/css/index.css +16 -12
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +33 -33
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -142
- package/src/css/timeline-stages.css +218 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +72 -53
- package/src/store/createSubResourcePlugin.js +135 -135
- package/src/store/index.js +3 -3
- package/src/store/plugins/auditTrails.js +17 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +7 -5
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +372 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +829 -686
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +35 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/headers.js +68 -50
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +3 -3
- 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="
|
|
100
|
-
:value="
|
|
99
|
+
:options="getEffectiveOptions(field)"
|
|
100
|
+
:value="getEffectiveSelectedOption(field)"
|
|
101
101
|
:clearable="!field.required"
|
|
102
102
|
:disabled="field.readOnly"
|
|
103
|
-
|
|
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="
|
|
120
|
-
:value="
|
|
133
|
+
:options="getEffectiveArrayOptions(field)"
|
|
134
|
+
:value="getEffectiveSelectedArrayOptions(field)"
|
|
121
135
|
:multiple="true"
|
|
122
136
|
:clearable="true"
|
|
123
137
|
:disabled="field.readOnly"
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|