@conduction/nextcloud-vue 0.1.0-beta.4 → 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/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +9554 -8980
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1231 -1231
- package/dist/nextcloud-vue.esm.js +9554 -8980
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +11 -4
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
- package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
- package/src/components/CnItemCard/CnItemCard.vue +132 -132
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
- package/src/components/CnRowActions/CnRowActions.vue +95 -73
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
- package/src/index.js +1 -1
- package/src/types/notification.d.ts +13 -13
- package/src/types/organisation.d.ts +15 -15
- package/src/types/schema.d.ts +13 -13
- package/src/types/task.d.ts +6 -6
|
@@ -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,89 +1,89 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<component :is="resolvedComponent" :size="size" />
|
|
3
|
-
</template>
|
|
4
|
-
|
|
5
|
-
<script>
|
|
6
|
-
import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Mutable icon registry.
|
|
10
|
-
*
|
|
11
|
-
* Pre-populated with HelpCircleOutline (the default fallback).
|
|
12
|
-
* Apps extend this at boot via registerIcons() — import only the
|
|
13
|
-
* icons you need, keeping bundles small.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* import { registerIcons } from '@conduction/nextcloud-vue'
|
|
17
|
-
* import Sword from 'vue-material-design-icons/Sword.vue'
|
|
18
|
-
* registerIcons({ Sword })
|
|
19
|
-
*/
|
|
20
|
-
const _registry = {
|
|
21
|
-
HelpCircleOutline,
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Register one or more MDI icon components for use with CnIcon.
|
|
26
|
-
*
|
|
27
|
-
* Call this in your app's main.js before mounting the Vue instance.
|
|
28
|
-
* Each key must be the PascalCase icon name matching the
|
|
29
|
-
* vue-material-design-icons file name (e.g. "Sword" for Sword.vue).
|
|
30
|
-
*
|
|
31
|
-
* @param {Record<string, import('vue').Component>} icons
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* import { registerIcons } from '@conduction/nextcloud-vue'
|
|
35
|
-
* import Sword from 'vue-material-design-icons/Sword.vue'
|
|
36
|
-
* import MagicStaff from 'vue-material-design-icons/MagicStaff.vue'
|
|
37
|
-
* registerIcons({ Sword, MagicStaff })
|
|
38
|
-
*/
|
|
39
|
-
export function registerIcons(icons) {
|
|
40
|
-
Object.assign(_registry, icons)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Read-only reference to the current icon registry.
|
|
45
|
-
* Useful for checking which icons are available.
|
|
46
|
-
*
|
|
47
|
-
* @type {Record<string, import('vue').Component>}
|
|
48
|
-
*/
|
|
49
|
-
export const ICON_MAP = _registry
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* CnIcon — Renders a Material Design Icon by PascalCase name.
|
|
53
|
-
*
|
|
54
|
-
* Looks up the name in the shared registry. If not found, renders
|
|
55
|
-
* the fallback icon (HelpCircleOutline by default).
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* <CnIcon name="AccountGroup" :size="24" />
|
|
59
|
-
*
|
|
60
|
-
* @see https://pictogrammers.com/library/mdi/
|
|
61
|
-
*/
|
|
62
|
-
export default {
|
|
63
|
-
name: 'CnIcon',
|
|
64
|
-
|
|
65
|
-
props: {
|
|
66
|
-
/** MDI icon name in PascalCase (e.g. "AccountGroup") */
|
|
67
|
-
name: {
|
|
68
|
-
type: String,
|
|
69
|
-
required: true,
|
|
70
|
-
},
|
|
71
|
-
/** Icon pixel size */
|
|
72
|
-
size: {
|
|
73
|
-
type: Number,
|
|
74
|
-
default: 20,
|
|
75
|
-
},
|
|
76
|
-
/** Fallback icon name if `name` is not found in the registry */
|
|
77
|
-
fallback: {
|
|
78
|
-
type: String,
|
|
79
|
-
default: 'HelpCircleOutline',
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
computed: {
|
|
84
|
-
resolvedComponent() {
|
|
85
|
-
return _registry[this.name] || _registry[this.fallback] || HelpCircleOutline
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="resolvedComponent" :size="size" />
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mutable icon registry.
|
|
10
|
+
*
|
|
11
|
+
* Pre-populated with HelpCircleOutline (the default fallback).
|
|
12
|
+
* Apps extend this at boot via registerIcons() — import only the
|
|
13
|
+
* icons you need, keeping bundles small.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { registerIcons } from '@conduction/nextcloud-vue'
|
|
17
|
+
* import Sword from 'vue-material-design-icons/Sword.vue'
|
|
18
|
+
* registerIcons({ Sword })
|
|
19
|
+
*/
|
|
20
|
+
const _registry = {
|
|
21
|
+
HelpCircleOutline,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register one or more MDI icon components for use with CnIcon.
|
|
26
|
+
*
|
|
27
|
+
* Call this in your app's main.js before mounting the Vue instance.
|
|
28
|
+
* Each key must be the PascalCase icon name matching the
|
|
29
|
+
* vue-material-design-icons file name (e.g. "Sword" for Sword.vue).
|
|
30
|
+
*
|
|
31
|
+
* @param {Record<string, import('vue').Component>} icons
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* import { registerIcons } from '@conduction/nextcloud-vue'
|
|
35
|
+
* import Sword from 'vue-material-design-icons/Sword.vue'
|
|
36
|
+
* import MagicStaff from 'vue-material-design-icons/MagicStaff.vue'
|
|
37
|
+
* registerIcons({ Sword, MagicStaff })
|
|
38
|
+
*/
|
|
39
|
+
export function registerIcons(icons) {
|
|
40
|
+
Object.assign(_registry, icons)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read-only reference to the current icon registry.
|
|
45
|
+
* Useful for checking which icons are available.
|
|
46
|
+
*
|
|
47
|
+
* @type {Record<string, import('vue').Component>}
|
|
48
|
+
*/
|
|
49
|
+
export const ICON_MAP = _registry
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* CnIcon — Renders a Material Design Icon by PascalCase name.
|
|
53
|
+
*
|
|
54
|
+
* Looks up the name in the shared registry. If not found, renders
|
|
55
|
+
* the fallback icon (HelpCircleOutline by default).
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <CnIcon name="AccountGroup" :size="24" />
|
|
59
|
+
*
|
|
60
|
+
* @see https://pictogrammers.com/library/mdi/
|
|
61
|
+
*/
|
|
62
|
+
export default {
|
|
63
|
+
name: 'CnIcon',
|
|
64
|
+
|
|
65
|
+
props: {
|
|
66
|
+
/** MDI icon name in PascalCase (e.g. "AccountGroup") */
|
|
67
|
+
name: {
|
|
68
|
+
type: String,
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
/** Icon pixel size */
|
|
72
|
+
size: {
|
|
73
|
+
type: Number,
|
|
74
|
+
default: 20,
|
|
75
|
+
},
|
|
76
|
+
/** Fallback icon name if `name` is not found in the registry */
|
|
77
|
+
fallback: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: 'HelpCircleOutline',
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
computed: {
|
|
84
|
+
resolvedComponent() {
|
|
85
|
+
return _registry[this.name] || _registry[this.fallback] || HelpCircleOutline
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
</script>
|