@conduction/nextcloud-vue 2.62.0 → 2.63.0
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/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +271 -32
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +65 -0
- package/dist/esm/composables/useDependencyCheck.js.map +1 -0
- package/dist/esm/composables/useWalkthrough.js +218 -6
- package/dist/esm/composables/useWalkthrough.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +43 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +43 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +449 -0
- package/dist/esm/store/useObjectStore.js +8 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +311 -0
- package/dist/esm/utils/columnFilters.js.map +1 -0
- package/dist/esm/utils/groupAutocomplete.js +123 -0
- package/dist/esm/utils/groupAutocomplete.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/pickerOptions.js +198 -0
- package/dist/esm/utils/pickerOptions.js.map +1 -0
- package/dist/esm/utils/schema.js +83 -7
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/timelineEvents.js +174 -0
- package/dist/esm/utils/timelineEvents.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/walkthroughSignals.js +88 -0
- package/dist/esm/utils/walkthroughSignals.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +9369 -5193
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +449 -0
- package/l10n/en.json +43 -0
- package/l10n/nl.json +43 -0
- package/package.json +1 -1
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +271 -32
- package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +3 -0
- package/src/components/CnDataTable/CnDataTable.vue +260 -4
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
- package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
- package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
- package/src/components/CnIndexPage/selfModeActions.js +3 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
- package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
- package/src/components/CnTimelineWidget/index.js +4 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +2 -0
- package/src/composables/useDependencyCheck.js +61 -0
- package/src/composables/useWalkthrough.js +217 -5
- package/src/css/dashboard.css +49 -0
- package/src/css/detail-page.css +19 -0
- package/src/css/table.css +81 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -1
- package/src/store/useObjectStore.js +8 -0
- package/src/utils/columnFilters.js +324 -0
- package/src/utils/groupAutocomplete.js +119 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/pickerOptions.js +194 -0
- package/src/utils/schema.js +83 -7
- package/src/utils/timelineEvents.js +171 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/walkthroughSignals.js +94 -0
|
@@ -64,12 +64,21 @@ var script = {
|
|
|
64
64
|
required: true,
|
|
65
65
|
},
|
|
66
66
|
|
|
67
|
-
/** Currently-selected object id (v-model)
|
|
67
|
+
/** Currently-selected object id (v-model), or an array of ids when `multiple`. */
|
|
68
68
|
modelValue: {
|
|
69
|
-
type: [String, Number],
|
|
69
|
+
type: [String, Number, Array],
|
|
70
70
|
default: '',
|
|
71
71
|
},
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Pick several objects. `modelValue` is then an array of ids, and a
|
|
75
|
+
* create adds the new object to the selection.
|
|
76
|
+
*/
|
|
77
|
+
multiple: {
|
|
78
|
+
type: Boolean,
|
|
79
|
+
default: false,
|
|
80
|
+
},
|
|
81
|
+
|
|
73
82
|
/** Object field used as the option label AND written on create. */
|
|
74
83
|
labelField: {
|
|
75
84
|
type: String,
|
|
@@ -190,6 +199,9 @@ var script = {
|
|
|
190
199
|
// The selected option object kept locally so the label shows even
|
|
191
200
|
// before/without a matching search result (e.g. a pre-set modelValue).
|
|
192
201
|
localSelected: null,
|
|
202
|
+
// Options seen so far, keyed by value, so a multiple selection
|
|
203
|
+
// keeps its labels when the search results move on.
|
|
204
|
+
known: {},
|
|
193
205
|
}
|
|
194
206
|
},
|
|
195
207
|
|
|
@@ -203,6 +215,19 @@ var script = {
|
|
|
203
215
|
}
|
|
204
216
|
},
|
|
205
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The selected ids as strings (multiple mode).
|
|
220
|
+
*
|
|
221
|
+
* @return {string[]}
|
|
222
|
+
*/
|
|
223
|
+
selectedIds() {
|
|
224
|
+
const value = this.modelValue;
|
|
225
|
+
if (Array.isArray(value)) {
|
|
226
|
+
return value.filter((v) => v !== null && v !== undefined && v !== '').map((v) => String(v))
|
|
227
|
+
}
|
|
228
|
+
return value ? [String(value)] : []
|
|
229
|
+
},
|
|
230
|
+
|
|
206
231
|
/** The `${register}-${schema}` object-type slug used by the store. */
|
|
207
232
|
typeSlug() {
|
|
208
233
|
return `${this.register}-${this.schema}`
|
|
@@ -210,6 +235,11 @@ var script = {
|
|
|
210
235
|
|
|
211
236
|
/** The currently-selected option, for NcSelect's model-value. */
|
|
212
237
|
selectedOption() {
|
|
238
|
+
if (this.multiple) {
|
|
239
|
+
return this.selectedIds.map((id) => this.known[id]
|
|
240
|
+
|| this.options.find((o) => o.value === id)
|
|
241
|
+
|| { value: id, label: id })
|
|
242
|
+
}
|
|
213
243
|
if (this.localSelected && this.localSelected.value === this.modelValue) {
|
|
214
244
|
return this.localSelected
|
|
215
245
|
}
|
|
@@ -281,9 +311,9 @@ var script = {
|
|
|
281
311
|
// The scope moved, so anything already selected may no longer be
|
|
282
312
|
// in it. Drop the stale options + selection, then re-seed.
|
|
283
313
|
this.options = [];
|
|
284
|
-
if (this.
|
|
314
|
+
if (this.selectedIds.length > 0) {
|
|
285
315
|
this.localSelected = null;
|
|
286
|
-
this.$emit('update:modelValue', '');
|
|
316
|
+
this.$emit('update:modelValue', this.multiple ? [] : '');
|
|
287
317
|
}
|
|
288
318
|
if (this.search.trim().length >= this.minChars) {
|
|
289
319
|
this.onSearch(this.search);
|
|
@@ -318,7 +348,7 @@ var script = {
|
|
|
318
348
|
const items = Array.isArray(collection)
|
|
319
349
|
? collection
|
|
320
350
|
: (this.objectStore.collections[this.typeSlug] || []);
|
|
321
|
-
this.options = items.map((o) => this.toOption(o));
|
|
351
|
+
this.options = this.remember(items.map((o) => this.toOption(o)));
|
|
322
352
|
} catch {
|
|
323
353
|
this.options = [];
|
|
324
354
|
} finally {
|
|
@@ -363,7 +393,7 @@ var script = {
|
|
|
363
393
|
const items = Array.isArray(collection)
|
|
364
394
|
? collection
|
|
365
395
|
: (this.objectStore.collections[this.typeSlug] || []);
|
|
366
|
-
this.options = items.map((o) => this.toOption(o));
|
|
396
|
+
this.options = this.remember(items.map((o) => this.toOption(o)));
|
|
367
397
|
} catch {
|
|
368
398
|
this.options = [];
|
|
369
399
|
} finally {
|
|
@@ -379,6 +409,10 @@ var script = {
|
|
|
379
409
|
* @return {Promise<void>}
|
|
380
410
|
*/
|
|
381
411
|
async onInput(option) {
|
|
412
|
+
if (this.multiple) {
|
|
413
|
+
await this.onMultipleInput(Array.isArray(option) ? option : (option ? [option] : []));
|
|
414
|
+
return
|
|
415
|
+
}
|
|
382
416
|
if (!option) {
|
|
383
417
|
this.localSelected = null;
|
|
384
418
|
/**
|
|
@@ -396,19 +430,58 @@ var script = {
|
|
|
396
430
|
this.$emit('update:modelValue', option.value);
|
|
397
431
|
},
|
|
398
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Handle a change of a multiple selection. A synthetic create option
|
|
435
|
+
* creates the object and adds it; the rest emit their ids.
|
|
436
|
+
*
|
|
437
|
+
* @param {Array<object>} chosen The selected options.
|
|
438
|
+
* @return {Promise<void>}
|
|
439
|
+
*/
|
|
440
|
+
async onMultipleInput(chosen) {
|
|
441
|
+
const createOption = chosen.find((o) => o && o.__create);
|
|
442
|
+
const ids = chosen.filter((o) => o && !o.__create).map((o) => String(o.value));
|
|
443
|
+
this.remember(chosen.filter((o) => o && !o.__create));
|
|
444
|
+
if (!createOption) {
|
|
445
|
+
this.$emit('update:modelValue', ids);
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
const created = await this.createFromTerm(createOption.label, ids);
|
|
449
|
+
if (!created) {
|
|
450
|
+
this.$emit('update:modelValue', ids);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Cache options by value so their labels survive a new search.
|
|
456
|
+
*
|
|
457
|
+
* @param {Array<object>} options Options to remember.
|
|
458
|
+
* @return {Array<object>} The same options.
|
|
459
|
+
*/
|
|
460
|
+
remember(options) {
|
|
461
|
+
const next = { ...this.known };
|
|
462
|
+
for (const o of options) {
|
|
463
|
+
if (o && o.value) {
|
|
464
|
+
next[o.value] = o;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
this.known = next;
|
|
468
|
+
return options
|
|
469
|
+
},
|
|
470
|
+
|
|
399
471
|
/**
|
|
400
472
|
* Create a new object from the typed term and select it.
|
|
401
473
|
*
|
|
402
474
|
* @param {string} term The term to write to `labelField`.
|
|
403
|
-
* @
|
|
475
|
+
* @param {string[]} [keep] Multiple mode: the ids already selected, which the new id joins.
|
|
476
|
+
* @return {Promise<object|null>} The created object, or null.
|
|
404
477
|
*/
|
|
405
|
-
async createFromTerm(term) {
|
|
478
|
+
async createFromTerm(term, keep = []) {
|
|
406
479
|
if (!this.objectStore && !this.createHandler) {
|
|
407
|
-
return
|
|
480
|
+
return null
|
|
408
481
|
}
|
|
409
482
|
const name = (term || '').trim();
|
|
410
483
|
if (!name) {
|
|
411
|
-
return
|
|
484
|
+
return null
|
|
412
485
|
}
|
|
413
486
|
this.loading = true;
|
|
414
487
|
try {
|
|
@@ -427,20 +500,26 @@ var script = {
|
|
|
427
500
|
created = await this.objectStore.saveObject(this.typeSlug, payload);
|
|
428
501
|
}
|
|
429
502
|
if (!created) {
|
|
430
|
-
return
|
|
503
|
+
return null
|
|
431
504
|
}
|
|
432
505
|
const option = this.toOption(created);
|
|
506
|
+
this.remember([option]);
|
|
433
507
|
this.options = [option, ...this.options.filter((o) => o.value !== option.value)];
|
|
434
508
|
this.localSelected = option;
|
|
435
|
-
this
|
|
509
|
+
this.search = '';
|
|
510
|
+
this.$emit('update:modelValue', this.multiple
|
|
511
|
+
? [...keep.filter((id) => id !== option.value), option.value]
|
|
512
|
+
: option.value);
|
|
436
513
|
/**
|
|
437
514
|
* @event create A new object was created from the search term.
|
|
438
515
|
* @type {object} The created OpenRegister object.
|
|
439
516
|
*/
|
|
440
517
|
this.$emit('create', created);
|
|
518
|
+
return created
|
|
441
519
|
} catch (e) {
|
|
442
520
|
// eslint-disable-next-line no-console
|
|
443
521
|
console.warn('[CnResourceSelect] create failed', e);
|
|
522
|
+
return null
|
|
444
523
|
} finally {
|
|
445
524
|
this.loading = false;
|
|
446
525
|
}
|
|
@@ -479,6 +558,10 @@ var script = {
|
|
|
479
558
|
* @return {Promise<void>}
|
|
480
559
|
*/
|
|
481
560
|
async ensureSelectedLoaded() {
|
|
561
|
+
if (this.multiple) {
|
|
562
|
+
await this.ensureManyLoaded();
|
|
563
|
+
return
|
|
564
|
+
}
|
|
482
565
|
if (!this.modelValue || this.selectedOption || !this.objectStore) {
|
|
483
566
|
return
|
|
484
567
|
}
|
|
@@ -495,6 +578,32 @@ var script = {
|
|
|
495
578
|
// Leave the id un-labelled rather than crash.
|
|
496
579
|
}
|
|
497
580
|
},
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Multiple mode: fetch each selected id whose label is not known yet.
|
|
584
|
+
*
|
|
585
|
+
* @return {Promise<void>}
|
|
586
|
+
*/
|
|
587
|
+
async ensureManyLoaded() {
|
|
588
|
+
if (!this.objectStore || typeof this.objectStore.fetchObject !== 'function') {
|
|
589
|
+
return
|
|
590
|
+
}
|
|
591
|
+
const missing = this.selectedIds.filter((id) => !this.known[id] && !this.options.some((o) => o.value === id));
|
|
592
|
+
if (missing.length === 0) {
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
this.ensureRegistered();
|
|
596
|
+
for (const id of missing) {
|
|
597
|
+
try {
|
|
598
|
+
const obj = await this.objectStore.fetchObject(this.typeSlug, id);
|
|
599
|
+
if (obj) {
|
|
600
|
+
this.remember([this.toOption(obj)]);
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
// Leave the id un-labelled rather than crash.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
},
|
|
498
607
|
},
|
|
499
608
|
};
|
|
500
609
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnResourceSelect.vue2.js","sources":["../../../../src/components/CnResourceSelect/CnResourceSelect.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcSelect\n\t\t:inputId=\"inputId\"\n\t\t:inputLabel=\"inputLabel\"\n\t\t:modelValue=\"selectedOption\"\n\t\t:options=\"displayOptions\"\n\t\t:loading=\"loading\"\n\t\t:clearable=\"clearable\"\n\t\t:disabled=\"disabled\"\n\t\t:placeholder=\"placeholder\"\n\t\t:filterable=\"false\"\n\t\tlabel=\"label\"\n\t\t@search=\"onSearch\"\n\t\t@update:modelValue=\"onInput\">\n\t\t<template #option=\"{ label: optLabel, __create }\">\n\t\t\t<span v-if=\"__create\" class=\"cn-resource-select__create\">\n\t\t\t\t<Plus :size=\"16\" />\n\t\t\t\t{{ createLabelFor(optLabel) }}\n\t\t\t</span>\n\t\t\t<span v-else>{{ optLabel }}</span>\n\t\t</template>\n\t</NcSelect>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { useObjectStore } from '../../store/index.js'\n\n/**\n * CnResourceSelect — an OpenRegister object picker that can CREATE from the\n * search term.\n *\n * Searches a `register` + `schema` for objects matching the typed text and\n * renders them as options. When the typed term matches no existing object, a\n * synthetic **\"Create '<term>'\"** option appears at the bottom; choosing it\n * persists a new object (via `useObjectStore().saveObject`, writing the term to\n * `labelField`) and selects it — so an agent who can't find a client just types\n * the name and makes one inline, never hitting a dead \"no results\" end.\n *\n * Emits `update:modelValue` (the selected object's id) and `create` (the\n * freshly-created object). The label of each option comes from `labelField`\n * (default `name`), the value from the object's id.\n *\n * Backwards-compatible, self-contained: it owns its own search + create flow,\n * so a consumer only needs to pass `register` + `schema` and bind `modelValue`.\n *\n * Three opt-in props cover the cases a plain object picker cannot:\n *\n * - `filters` SCOPES the options to a parent selection, which is what makes a\n * cascading pair work (pick a client, then only that client's contacts are\n * offered). Changing the scope clears a now-invalid selection.\n * - `preload` fetches a first page on mount so the field can be BROWSED, not\n * only searched — the behaviour a user expects from a select.\n * - `createHandler` replaces the built-in save. Creating from a bare term only\n * works while the term is enough to satisfy the schema; the moment a\n * required field must come from somewhere else (a server-minted key, a value\n * worth collecting in a full dialog) the consumer owns the create and\n * resolves the finished object back.\n *\n * Example:\n * ```vue\n * <CnResourceSelect\n * register=\"pipelinq\"\n * schema=\"client\"\n * label-field=\"name\"\n * :model-value=\"clientId\"\n * :input-label=\"t('pipelinq', 'Client')\"\n * @update:modelValue=\"clientId = $event\"\n * @create=\"onClientCreated\" />\n * ```\n */\nexport default {\n\tname: 'CnResourceSelect',\n\n\tcomponents: { NcSelect, Plus },\n\n\tprops: {\n\t\t/** OpenRegister register slug to search/create in. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** OpenRegister schema slug to search/create in. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Currently-selected object id (v-model). */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Object field used as the option label AND written on create. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\n\t\t/** Accessible input label for the underlying NcSelect. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** DOM id for the input (a11y association). */\n\t\tinputId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the selection can be cleared. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Minimum characters before searching / offering create. */\n\t\tminChars: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * Whether to offer the inline \"Create '<term>'\" option when the search\n\t\t * yields no exact match. Off → behaves like a plain async object select.\n\t\t */\n\t\tallowCreate: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Extra fields merged into the payload when creating a new object (e.g.\n\t\t * a fixed `type` or `status`). The `labelField` is always set to the term.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Field filters merged into the search query, so the option list can be\n\t\t * SCOPED to a parent selection — the cascading-select case (pick a\n\t\t * client, then only that client's contacts are offered). Changing this\n\t\t * reloads the options and clears a selection that no longer belongs to\n\t\t * the new scope.\n\t\t *\n\t\t * Example: `:filters=\"{ client: form.client }\"`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Load a first page of options on mount (and whenever `filters`\n\t\t * change) instead of waiting for `minChars` of typing. Lets the field\n\t\t * be BROWSED like a plain select while still searching server-side once\n\t\t * the user types. Off by default so existing consumers keep their\n\t\t * search-only behaviour and issue no extra request.\n\t\t */\n\t\tpreload: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Disable the input — e.g. a dependent select waiting on its parent.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder text for the underlying NcSelect. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override how a new object is created from the typed term. Receives\n\t\t * `(term, payload)` and MUST resolve to the created object (or a falsy\n\t\t * value to abort, e.g. the user cancelled a dialog).\n\t\t *\n\t\t * Without it the component posts `payload` straight to\n\t\t * `objectStore.saveObject`, which is wrong whenever the schema requires\n\t\t * a field the term cannot supply — a server-minted foreign key, say, or\n\t\t * anything the consumer would rather collect in a full create dialog.\n\t\t *\n\t\t * @type {((term: string, payload: object) => Promise<object|null>)|null}\n\t\t */\n\t\tcreateHandler: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'create'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsearch: '',\n\t\t\tloading: false,\n\t\t\toptions: [],\n\t\t\t// The selected option object kept locally so the label shows even\n\t\t\t// before/without a matching search result (e.g. a pre-set modelValue).\n\t\t\tlocalSelected: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Pinia object store handle (resolved lazily so a Pinia-less test still mounts). */\n\t\tobjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/** The `${register}-${schema}` object-type slug used by the store. */\n\t\ttypeSlug() {\n\t\t\treturn `${this.register}-${this.schema}`\n\t\t},\n\n\t\t/** The currently-selected option, for NcSelect's model-value. */\n\t\tselectedOption() {\n\t\t\tif (this.localSelected && this.localSelected.value === this.modelValue) {\n\t\t\t\treturn this.localSelected\n\t\t\t}\n\t\t\treturn this.options.find((o) => o.value === this.modelValue) || null\n\t\t},\n\n\t\t/**\n\t\t * The option list shown in the dropdown — the search results, plus a\n\t\t * synthetic \"Create '<term>'\" entry when create is allowed and the term\n\t\t * is long enough and matches no existing option label exactly.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdisplayOptions() {\n\t\t\tconst opts = [...this.options]\n\t\t\tconst term = this.search.trim()\n\t\t\tif (this.allowCreate && term.length >= this.minChars) {\n\t\t\t\tconst exact = opts.some((o) => (o.label || '').toLowerCase() === term.toLowerCase())\n\t\t\t\tif (!exact) {\n\t\t\t\t\topts.push({ value: '__create__', label: term, __create: true })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn opts\n\t\t},\n\n\t\t/**\n\t\t * Stable string identity for `filters`. Watching the object itself\n\t\t * would re-fire on every parent re-render (a fresh object literal is a\n\t\t * new identity even when the values are unchanged), reloading options\n\t\t * and clobbering the selection in a loop.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tfiltersKey() {\n\t\t\treturn JSON.stringify(this.filters || {})\n\t\t},\n\n\t\t/**\n\t\t * `filters` with empty entries dropped. A parent that has not been\n\t\t * chosen yet holds `null`, and forwarding `client=` as a query param\n\t\t * asks the API for objects whose client is the empty string — which\n\t\t * matches nothing and reads as \"no results\" rather than \"not scoped\".\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tactiveFilters() {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.filters || {})) {\n\t\t\t\tif (value !== null && value !== undefined && value !== '') {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.ensureSelectedLoaded()\n\t\t\t},\n\t\t},\n\n\t\tfiltersKey(next, prev) {\n\t\t\tif (next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The scope moved, so anything already selected may no longer be\n\t\t\t// in it. Drop the stale options + selection, then re-seed.\n\t\t\tthis.options = []\n\t\t\tif (this.modelValue) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t}\n\t\t\tif (this.search.trim().length >= this.minChars) {\n\t\t\t\tthis.onSearch(this.search)\n\t\t\t} else {\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadInitialOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Seed the dropdown with a first page of options so the field can be\n\t\t * browsed without typing. No-op unless `preload` is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadInitialOptions() {\n\t\t\tif (!this.preload || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = items.map((o) => this.toOption(o))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The translated \"Create 'X'\" label for the synthetic option.\n\t\t *\n\t\t * @param {string} term The typed term.\n\t\t * @return {string}\n\t\t */\n\t\tcreateLabelFor(term) {\n\t\t\treturn t('nextcloud-vue', 'Create \"{term}\"', { term })\n\t\t},\n\n\t\t/**\n\t\t * Run an object search (debounced via NcSelect's own throttling) for the\n\t\t * typed term. Failures degrade to an empty option list.\n\t\t *\n\t\t * @param {string} term The search term from NcSelect.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSearch(term) {\n\t\t\tthis.search = term || ''\n\t\t\tif (!this.objectStore || this.search.trim().length < this.minChars) {\n\t\t\t\t// Below the search threshold: fall back to the browsable first\n\t\t\t\t// page when preloading, rather than blanking the list.\n\t\t\t\tthis.options = []\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_search: this.search.trim(),\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = items.map((o) => this.toOption(o))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a selection. A real option emits its id; the synthetic create\n\t\t * option triggers object creation.\n\t\t *\n\t\t * @param {object|null} option The chosen option (or null on clear).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onInput(option) {\n\t\t\tif (!option) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected (or newly-created) object id, or '' when cleared.\n\t\t\t\t * @type {string}\n\t\t\t\t */\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (option.__create) {\n\t\t\t\tawait this.createFromTerm(option.label)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.localSelected = option\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Create a new object from the typed term and select it.\n\t\t *\n\t\t * @param {string} term The term to write to `labelField`.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync createFromTerm(term) {\n\t\t\tif (!this.objectStore && !this.createHandler) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst name = (term || '').trim()\n\t\t\tif (!name) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The active scope is part of the new object's identity: a\n\t\t\t\t// contact created while the list is filtered to one client\n\t\t\t\t// belongs to that client. An explicit createDefaults entry\n\t\t\t\t// still wins.\n\t\t\t\tconst payload = { ...this.activeFilters, ...this.createDefaults, [this.labelField]: name }\n\t\t\t\tlet created\n\t\t\t\tif (this.createHandler) {\n\t\t\t\t\t// A falsy resolve means the consumer aborted (cancelled\n\t\t\t\t\t// dialog) — leave the selection untouched.\n\t\t\t\t\tcreated = await this.createHandler(name, payload)\n\t\t\t\t} else {\n\t\t\t\t\tthis.ensureRegistered()\n\t\t\t\t\tcreated = await this.objectStore.saveObject(this.typeSlug, payload)\n\t\t\t\t}\n\t\t\t\tif (!created) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst option = this.toOption(created)\n\t\t\t\tthis.options = [option, ...this.options.filter((o) => o.value !== option.value)]\n\t\t\t\tthis.localSelected = option\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\t/**\n\t\t\t\t * @event create A new object was created from the search term.\n\t\t\t\t * @type {object} The created OpenRegister object.\n\t\t\t\t */\n\t\t\t\tthis.$emit('create', created)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnResourceSelect] create failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Map an OpenRegister object to an option `{ value, label }`.\n\t\t *\n\t\t * @param {object} obj The object.\n\t\t * @return {{value: string, label: string}}\n\t\t */\n\t\ttoOption(obj) {\n\t\t\tconst id = String(obj.id || (obj['@self'] && obj['@self'].id) || '')\n\t\t\tconst label = obj[this.labelField] || obj.name || obj.title || id\n\t\t\treturn { value: id, label }\n\t\t},\n\n\t\t/**\n\t\t * Register the object type with the store (idempotent) so search/create\n\t\t * route to the right register+schema.\n\t\t */\n\t\tensureRegistered() {\n\t\t\tif (this.objectStore && typeof this.objectStore.registerObjectType === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tthis.objectStore.registerObjectType(this.typeSlug, this.schema, this.register)\n\t\t\t\t} catch {\n\t\t\t\t\t// Already registered or store not ready — non-fatal.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * When a `modelValue` is set but no matching option is loaded, fetch the\n\t\t * single object so its label renders (e.g. a pre-filled client id).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSelectedLoaded() {\n\t\t\tif (!this.modelValue || this.selectedOption || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tif (typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, String(this.modelValue))\n\t\t\t\tif (obj) {\n\t\t\t\t\tthis.localSelected = this.toOption(obj)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-resource-select__create {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnResourceSelect.vue2.js","sources":["../../../../src/components/CnResourceSelect/CnResourceSelect.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcSelect\n\t\t:inputId=\"inputId\"\n\t\t:inputLabel=\"inputLabel\"\n\t\t:modelValue=\"selectedOption\"\n\t\t:options=\"displayOptions\"\n\t\t:loading=\"loading\"\n\t\t:clearable=\"clearable\"\n\t\t:disabled=\"disabled\"\n\t\t:placeholder=\"placeholder\"\n\t\t:filterable=\"false\"\n\t\t:multiple=\"multiple\"\n\t\t:keepOpen=\"multiple\"\n\t\tlabel=\"label\"\n\t\t@search=\"onSearch\"\n\t\t@update:modelValue=\"onInput\">\n\t\t<template #option=\"{ label: optLabel, __create }\">\n\t\t\t<span v-if=\"__create\" class=\"cn-resource-select__create\">\n\t\t\t\t<Plus :size=\"16\" />\n\t\t\t\t{{ createLabelFor(optLabel) }}\n\t\t\t</span>\n\t\t\t<span v-else>{{ optLabel }}</span>\n\t\t</template>\n\t</NcSelect>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { useObjectStore } from '../../store/index.js'\n\n/**\n * CnResourceSelect — an OpenRegister object picker that can CREATE from the\n * search term.\n *\n * Searches a `register` + `schema` for objects matching the typed text and\n * renders them as options. When the typed term matches no existing object, a\n * synthetic **\"Create '<term>'\"** option appears at the bottom; choosing it\n * persists a new object (via `useObjectStore().saveObject`, writing the term to\n * `labelField`) and selects it — so an agent who can't find a client just types\n * the name and makes one inline, never hitting a dead \"no results\" end.\n *\n * Emits `update:modelValue` (the selected object's id) and `create` (the\n * freshly-created object). The label of each option comes from `labelField`\n * (default `name`), the value from the object's id.\n *\n * Backwards-compatible, self-contained: it owns its own search + create flow,\n * so a consumer only needs to pass `register` + `schema` and bind `modelValue`.\n *\n * Three opt-in props cover the cases a plain object picker cannot:\n *\n * - `filters` SCOPES the options to a parent selection, which is what makes a\n * cascading pair work (pick a client, then only that client's contacts are\n * offered). Changing the scope clears a now-invalid selection.\n * - `preload` fetches a first page on mount so the field can be BROWSED, not\n * only searched — the behaviour a user expects from a select.\n * - `createHandler` replaces the built-in save. Creating from a bare term only\n * works while the term is enough to satisfy the schema; the moment a\n * required field must come from somewhere else (a server-minted key, a value\n * worth collecting in a full dialog) the consumer owns the create and\n * resolves the finished object back.\n *\n * Example:\n * ```vue\n * <CnResourceSelect\n * register=\"pipelinq\"\n * schema=\"client\"\n * label-field=\"name\"\n * :model-value=\"clientId\"\n * :input-label=\"t('pipelinq', 'Client')\"\n * @update:modelValue=\"clientId = $event\"\n * @create=\"onClientCreated\" />\n * ```\n */\nexport default {\n\tname: 'CnResourceSelect',\n\n\tcomponents: { NcSelect, Plus },\n\n\tprops: {\n\t\t/** OpenRegister register slug to search/create in. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** OpenRegister schema slug to search/create in. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Currently-selected object id (v-model), or an array of ids when `multiple`. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number, Array],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Pick several objects. `modelValue` is then an array of ids, and a\n\t\t * create adds the new object to the selection.\n\t\t */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Object field used as the option label AND written on create. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\n\t\t/** Accessible input label for the underlying NcSelect. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** DOM id for the input (a11y association). */\n\t\tinputId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the selection can be cleared. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Minimum characters before searching / offering create. */\n\t\tminChars: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * Whether to offer the inline \"Create '<term>'\" option when the search\n\t\t * yields no exact match. Off → behaves like a plain async object select.\n\t\t */\n\t\tallowCreate: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Extra fields merged into the payload when creating a new object (e.g.\n\t\t * a fixed `type` or `status`). The `labelField` is always set to the term.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Field filters merged into the search query, so the option list can be\n\t\t * SCOPED to a parent selection — the cascading-select case (pick a\n\t\t * client, then only that client's contacts are offered). Changing this\n\t\t * reloads the options and clears a selection that no longer belongs to\n\t\t * the new scope.\n\t\t *\n\t\t * Example: `:filters=\"{ client: form.client }\"`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Load a first page of options on mount (and whenever `filters`\n\t\t * change) instead of waiting for `minChars` of typing. Lets the field\n\t\t * be BROWSED like a plain select while still searching server-side once\n\t\t * the user types. Off by default so existing consumers keep their\n\t\t * search-only behaviour and issue no extra request.\n\t\t */\n\t\tpreload: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Disable the input — e.g. a dependent select waiting on its parent.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder text for the underlying NcSelect. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override how a new object is created from the typed term. Receives\n\t\t * `(term, payload)` and MUST resolve to the created object (or a falsy\n\t\t * value to abort, e.g. the user cancelled a dialog).\n\t\t *\n\t\t * Without it the component posts `payload` straight to\n\t\t * `objectStore.saveObject`, which is wrong whenever the schema requires\n\t\t * a field the term cannot supply — a server-minted foreign key, say, or\n\t\t * anything the consumer would rather collect in a full create dialog.\n\t\t *\n\t\t * @type {((term: string, payload: object) => Promise<object|null>)|null}\n\t\t */\n\t\tcreateHandler: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'create'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsearch: '',\n\t\t\tloading: false,\n\t\t\toptions: [],\n\t\t\t// The selected option object kept locally so the label shows even\n\t\t\t// before/without a matching search result (e.g. a pre-set modelValue).\n\t\t\tlocalSelected: null,\n\t\t\t// Options seen so far, keyed by value, so a multiple selection\n\t\t\t// keeps its labels when the search results move on.\n\t\t\tknown: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Pinia object store handle (resolved lazily so a Pinia-less test still mounts). */\n\t\tobjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The selected ids as strings (multiple mode).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tselectedIds() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\treturn value.filter((v) => v !== null && v !== undefined && v !== '').map((v) => String(v))\n\t\t\t}\n\t\t\treturn value ? [String(value)] : []\n\t\t},\n\n\t\t/** The `${register}-${schema}` object-type slug used by the store. */\n\t\ttypeSlug() {\n\t\t\treturn `${this.register}-${this.schema}`\n\t\t},\n\n\t\t/** The currently-selected option, for NcSelect's model-value. */\n\t\tselectedOption() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn this.selectedIds.map((id) => this.known[id]\n\t\t\t\t\t|| this.options.find((o) => o.value === id)\n\t\t\t\t\t|| { value: id, label: id })\n\t\t\t}\n\t\t\tif (this.localSelected && this.localSelected.value === this.modelValue) {\n\t\t\t\treturn this.localSelected\n\t\t\t}\n\t\t\treturn this.options.find((o) => o.value === this.modelValue) || null\n\t\t},\n\n\t\t/**\n\t\t * The option list shown in the dropdown — the search results, plus a\n\t\t * synthetic \"Create '<term>'\" entry when create is allowed and the term\n\t\t * is long enough and matches no existing option label exactly.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdisplayOptions() {\n\t\t\tconst opts = [...this.options]\n\t\t\tconst term = this.search.trim()\n\t\t\tif (this.allowCreate && term.length >= this.minChars) {\n\t\t\t\tconst exact = opts.some((o) => (o.label || '').toLowerCase() === term.toLowerCase())\n\t\t\t\tif (!exact) {\n\t\t\t\t\topts.push({ value: '__create__', label: term, __create: true })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn opts\n\t\t},\n\n\t\t/**\n\t\t * Stable string identity for `filters`. Watching the object itself\n\t\t * would re-fire on every parent re-render (a fresh object literal is a\n\t\t * new identity even when the values are unchanged), reloading options\n\t\t * and clobbering the selection in a loop.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tfiltersKey() {\n\t\t\treturn JSON.stringify(this.filters || {})\n\t\t},\n\n\t\t/**\n\t\t * `filters` with empty entries dropped. A parent that has not been\n\t\t * chosen yet holds `null`, and forwarding `client=` as a query param\n\t\t * asks the API for objects whose client is the empty string — which\n\t\t * matches nothing and reads as \"no results\" rather than \"not scoped\".\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tactiveFilters() {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.filters || {})) {\n\t\t\t\tif (value !== null && value !== undefined && value !== '') {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.ensureSelectedLoaded()\n\t\t\t},\n\t\t},\n\n\t\tfiltersKey(next, prev) {\n\t\t\tif (next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The scope moved, so anything already selected may no longer be\n\t\t\t// in it. Drop the stale options + selection, then re-seed.\n\t\t\tthis.options = []\n\t\t\tif (this.selectedIds.length > 0) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\tthis.$emit('update:modelValue', this.multiple ? [] : '')\n\t\t\t}\n\t\t\tif (this.search.trim().length >= this.minChars) {\n\t\t\t\tthis.onSearch(this.search)\n\t\t\t} else {\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadInitialOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Seed the dropdown with a first page of options so the field can be\n\t\t * browsed without typing. No-op unless `preload` is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadInitialOptions() {\n\t\t\tif (!this.preload || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = this.remember(items.map((o) => this.toOption(o)))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The translated \"Create 'X'\" label for the synthetic option.\n\t\t *\n\t\t * @param {string} term The typed term.\n\t\t * @return {string}\n\t\t */\n\t\tcreateLabelFor(term) {\n\t\t\treturn t('nextcloud-vue', 'Create \"{term}\"', { term })\n\t\t},\n\n\t\t/**\n\t\t * Run an object search (debounced via NcSelect's own throttling) for the\n\t\t * typed term. Failures degrade to an empty option list.\n\t\t *\n\t\t * @param {string} term The search term from NcSelect.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSearch(term) {\n\t\t\tthis.search = term || ''\n\t\t\tif (!this.objectStore || this.search.trim().length < this.minChars) {\n\t\t\t\t// Below the search threshold: fall back to the browsable first\n\t\t\t\t// page when preloading, rather than blanking the list.\n\t\t\t\tthis.options = []\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_search: this.search.trim(),\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = this.remember(items.map((o) => this.toOption(o)))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a selection. A real option emits its id; the synthetic create\n\t\t * option triggers object creation.\n\t\t *\n\t\t * @param {object|null} option The chosen option (or null on clear).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onInput(option) {\n\t\t\tif (this.multiple) {\n\t\t\t\tawait this.onMultipleInput(Array.isArray(option) ? option : (option ? [option] : []))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!option) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected (or newly-created) object id, or '' when cleared.\n\t\t\t\t * @type {string}\n\t\t\t\t */\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (option.__create) {\n\t\t\t\tawait this.createFromTerm(option.label)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.localSelected = option\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Handle a change of a multiple selection. A synthetic create option\n\t\t * creates the object and adds it; the rest emit their ids.\n\t\t *\n\t\t * @param {Array<object>} chosen The selected options.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onMultipleInput(chosen) {\n\t\t\tconst createOption = chosen.find((o) => o && o.__create)\n\t\t\tconst ids = chosen.filter((o) => o && !o.__create).map((o) => String(o.value))\n\t\t\tthis.remember(chosen.filter((o) => o && !o.__create))\n\t\t\tif (!createOption) {\n\t\t\t\tthis.$emit('update:modelValue', ids)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst created = await this.createFromTerm(createOption.label, ids)\n\t\t\tif (!created) {\n\t\t\t\tthis.$emit('update:modelValue', ids)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Cache options by value so their labels survive a new search.\n\t\t *\n\t\t * @param {Array<object>} options Options to remember.\n\t\t * @return {Array<object>} The same options.\n\t\t */\n\t\tremember(options) {\n\t\t\tconst next = { ...this.known }\n\t\t\tfor (const o of options) {\n\t\t\t\tif (o && o.value) {\n\t\t\t\t\tnext[o.value] = o\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.known = next\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * Create a new object from the typed term and select it.\n\t\t *\n\t\t * @param {string} term The term to write to `labelField`.\n\t\t * @param {string[]} [keep] Multiple mode: the ids already selected, which the new id joins.\n\t\t * @return {Promise<object|null>} The created object, or null.\n\t\t */\n\t\tasync createFromTerm(term, keep = []) {\n\t\t\tif (!this.objectStore && !this.createHandler) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst name = (term || '').trim()\n\t\t\tif (!name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The active scope is part of the new object's identity: a\n\t\t\t\t// contact created while the list is filtered to one client\n\t\t\t\t// belongs to that client. An explicit createDefaults entry\n\t\t\t\t// still wins.\n\t\t\t\tconst payload = { ...this.activeFilters, ...this.createDefaults, [this.labelField]: name }\n\t\t\t\tlet created\n\t\t\t\tif (this.createHandler) {\n\t\t\t\t\t// A falsy resolve means the consumer aborted (cancelled\n\t\t\t\t\t// dialog) — leave the selection untouched.\n\t\t\t\t\tcreated = await this.createHandler(name, payload)\n\t\t\t\t} else {\n\t\t\t\t\tthis.ensureRegistered()\n\t\t\t\t\tcreated = await this.objectStore.saveObject(this.typeSlug, payload)\n\t\t\t\t}\n\t\t\t\tif (!created) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst option = this.toOption(created)\n\t\t\t\tthis.remember([option])\n\t\t\t\tthis.options = [option, ...this.options.filter((o) => o.value !== option.value)]\n\t\t\t\tthis.localSelected = option\n\t\t\t\tthis.search = ''\n\t\t\t\tthis.$emit('update:modelValue', this.multiple\n\t\t\t\t\t? [...keep.filter((id) => id !== option.value), option.value]\n\t\t\t\t\t: option.value)\n\t\t\t\t/**\n\t\t\t\t * @event create A new object was created from the search term.\n\t\t\t\t * @type {object} The created OpenRegister object.\n\t\t\t\t */\n\t\t\t\tthis.$emit('create', created)\n\t\t\t\treturn created\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnResourceSelect] create failed', e)\n\t\t\t\treturn null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Map an OpenRegister object to an option `{ value, label }`.\n\t\t *\n\t\t * @param {object} obj The object.\n\t\t * @return {{value: string, label: string}}\n\t\t */\n\t\ttoOption(obj) {\n\t\t\tconst id = String(obj.id || (obj['@self'] && obj['@self'].id) || '')\n\t\t\tconst label = obj[this.labelField] || obj.name || obj.title || id\n\t\t\treturn { value: id, label }\n\t\t},\n\n\t\t/**\n\t\t * Register the object type with the store (idempotent) so search/create\n\t\t * route to the right register+schema.\n\t\t */\n\t\tensureRegistered() {\n\t\t\tif (this.objectStore && typeof this.objectStore.registerObjectType === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tthis.objectStore.registerObjectType(this.typeSlug, this.schema, this.register)\n\t\t\t\t} catch {\n\t\t\t\t\t// Already registered or store not ready — non-fatal.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * When a `modelValue` is set but no matching option is loaded, fetch the\n\t\t * single object so its label renders (e.g. a pre-filled client id).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSelectedLoaded() {\n\t\t\tif (this.multiple) {\n\t\t\t\tawait this.ensureManyLoaded()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.modelValue || this.selectedOption || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tif (typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, String(this.modelValue))\n\t\t\t\tif (obj) {\n\t\t\t\t\tthis.localSelected = this.toOption(obj)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Multiple mode: fetch each selected id whose label is not known yet.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureManyLoaded() {\n\t\t\tif (!this.objectStore || typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst missing = this.selectedIds.filter((id) => !this.known[id] && !this.options.some((o) => o.value === id))\n\t\t\tif (missing.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.ensureRegistered()\n\t\t\tfor (const id of missing) {\n\t\t\t\ttry {\n\t\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, id)\n\t\t\t\t\tif (obj) {\n\t\t\t\t\t\tthis.remember([this.toOption(obj)])\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-resource-select__create {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
|
|
@@ -20,6 +20,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
20
20
|
disabled: $props.disabled,
|
|
21
21
|
placeholder: $props.placeholder,
|
|
22
22
|
filterable: false,
|
|
23
|
+
multiple: $props.multiple,
|
|
24
|
+
keepOpen: $props.multiple,
|
|
23
25
|
label: "label",
|
|
24
26
|
onSearch: $options.onSearch,
|
|
25
27
|
"onUpdate:modelValue": $options.onInput
|
|
@@ -33,7 +35,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
33
35
|
: (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(optLabel), 1 /* TEXT */))
|
|
34
36
|
]),
|
|
35
37
|
_: 1 /* STABLE */
|
|
36
|
-
}, 8 /* PROPS */, ["inputId", "inputLabel", "modelValue", "options", "loading", "clearable", "disabled", "placeholder", "onSearch", "onUpdate:modelValue"]))
|
|
38
|
+
}, 8 /* PROPS */, ["inputId", "inputLabel", "modelValue", "options", "loading", "clearable", "disabled", "placeholder", "multiple", "keepOpen", "onSearch", "onUpdate:modelValue"]))
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export { render };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnResourceSelect.vue3.js","sources":["../../../../src/components/CnResourceSelect/CnResourceSelect.vue?vue&type=template&id=37b7ff6f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcSelect\n\t\t:inputId=\"inputId\"\n\t\t:inputLabel=\"inputLabel\"\n\t\t:modelValue=\"selectedOption\"\n\t\t:options=\"displayOptions\"\n\t\t:loading=\"loading\"\n\t\t:clearable=\"clearable\"\n\t\t:disabled=\"disabled\"\n\t\t:placeholder=\"placeholder\"\n\t\t:filterable=\"false\"\n\t\tlabel=\"label\"\n\t\t@search=\"onSearch\"\n\t\t@update:modelValue=\"onInput\">\n\t\t<template #option=\"{ label: optLabel, __create }\">\n\t\t\t<span v-if=\"__create\" class=\"cn-resource-select__create\">\n\t\t\t\t<Plus :size=\"16\" />\n\t\t\t\t{{ createLabelFor(optLabel) }}\n\t\t\t</span>\n\t\t\t<span v-else>{{ optLabel }}</span>\n\t\t</template>\n\t</NcSelect>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { useObjectStore } from '../../store/index.js'\n\n/**\n * CnResourceSelect — an OpenRegister object picker that can CREATE from the\n * search term.\n *\n * Searches a `register` + `schema` for objects matching the typed text and\n * renders them as options. When the typed term matches no existing object, a\n * synthetic **\"Create '<term>'\"** option appears at the bottom; choosing it\n * persists a new object (via `useObjectStore().saveObject`, writing the term to\n * `labelField`) and selects it — so an agent who can't find a client just types\n * the name and makes one inline, never hitting a dead \"no results\" end.\n *\n * Emits `update:modelValue` (the selected object's id) and `create` (the\n * freshly-created object). The label of each option comes from `labelField`\n * (default `name`), the value from the object's id.\n *\n * Backwards-compatible, self-contained: it owns its own search + create flow,\n * so a consumer only needs to pass `register` + `schema` and bind `modelValue`.\n *\n * Three opt-in props cover the cases a plain object picker cannot:\n *\n * - `filters` SCOPES the options to a parent selection, which is what makes a\n * cascading pair work (pick a client, then only that client's contacts are\n * offered). Changing the scope clears a now-invalid selection.\n * - `preload` fetches a first page on mount so the field can be BROWSED, not\n * only searched — the behaviour a user expects from a select.\n * - `createHandler` replaces the built-in save. Creating from a bare term only\n * works while the term is enough to satisfy the schema; the moment a\n * required field must come from somewhere else (a server-minted key, a value\n * worth collecting in a full dialog) the consumer owns the create and\n * resolves the finished object back.\n *\n * Example:\n * ```vue\n * <CnResourceSelect\n * register=\"pipelinq\"\n * schema=\"client\"\n * label-field=\"name\"\n * :model-value=\"clientId\"\n * :input-label=\"t('pipelinq', 'Client')\"\n * @update:modelValue=\"clientId = $event\"\n * @create=\"onClientCreated\" />\n * ```\n */\nexport default {\n\tname: 'CnResourceSelect',\n\n\tcomponents: { NcSelect, Plus },\n\n\tprops: {\n\t\t/** OpenRegister register slug to search/create in. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** OpenRegister schema slug to search/create in. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Currently-selected object id (v-model). */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Object field used as the option label AND written on create. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\n\t\t/** Accessible input label for the underlying NcSelect. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** DOM id for the input (a11y association). */\n\t\tinputId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the selection can be cleared. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Minimum characters before searching / offering create. */\n\t\tminChars: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * Whether to offer the inline \"Create '<term>'\" option when the search\n\t\t * yields no exact match. Off → behaves like a plain async object select.\n\t\t */\n\t\tallowCreate: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Extra fields merged into the payload when creating a new object (e.g.\n\t\t * a fixed `type` or `status`). The `labelField` is always set to the term.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Field filters merged into the search query, so the option list can be\n\t\t * SCOPED to a parent selection — the cascading-select case (pick a\n\t\t * client, then only that client's contacts are offered). Changing this\n\t\t * reloads the options and clears a selection that no longer belongs to\n\t\t * the new scope.\n\t\t *\n\t\t * Example: `:filters=\"{ client: form.client }\"`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Load a first page of options on mount (and whenever `filters`\n\t\t * change) instead of waiting for `minChars` of typing. Lets the field\n\t\t * be BROWSED like a plain select while still searching server-side once\n\t\t * the user types. Off by default so existing consumers keep their\n\t\t * search-only behaviour and issue no extra request.\n\t\t */\n\t\tpreload: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Disable the input — e.g. a dependent select waiting on its parent.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder text for the underlying NcSelect. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override how a new object is created from the typed term. Receives\n\t\t * `(term, payload)` and MUST resolve to the created object (or a falsy\n\t\t * value to abort, e.g. the user cancelled a dialog).\n\t\t *\n\t\t * Without it the component posts `payload` straight to\n\t\t * `objectStore.saveObject`, which is wrong whenever the schema requires\n\t\t * a field the term cannot supply — a server-minted foreign key, say, or\n\t\t * anything the consumer would rather collect in a full create dialog.\n\t\t *\n\t\t * @type {((term: string, payload: object) => Promise<object|null>)|null}\n\t\t */\n\t\tcreateHandler: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'create'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsearch: '',\n\t\t\tloading: false,\n\t\t\toptions: [],\n\t\t\t// The selected option object kept locally so the label shows even\n\t\t\t// before/without a matching search result (e.g. a pre-set modelValue).\n\t\t\tlocalSelected: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Pinia object store handle (resolved lazily so a Pinia-less test still mounts). */\n\t\tobjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/** The `${register}-${schema}` object-type slug used by the store. */\n\t\ttypeSlug() {\n\t\t\treturn `${this.register}-${this.schema}`\n\t\t},\n\n\t\t/** The currently-selected option, for NcSelect's model-value. */\n\t\tselectedOption() {\n\t\t\tif (this.localSelected && this.localSelected.value === this.modelValue) {\n\t\t\t\treturn this.localSelected\n\t\t\t}\n\t\t\treturn this.options.find((o) => o.value === this.modelValue) || null\n\t\t},\n\n\t\t/**\n\t\t * The option list shown in the dropdown — the search results, plus a\n\t\t * synthetic \"Create '<term>'\" entry when create is allowed and the term\n\t\t * is long enough and matches no existing option label exactly.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdisplayOptions() {\n\t\t\tconst opts = [...this.options]\n\t\t\tconst term = this.search.trim()\n\t\t\tif (this.allowCreate && term.length >= this.minChars) {\n\t\t\t\tconst exact = opts.some((o) => (o.label || '').toLowerCase() === term.toLowerCase())\n\t\t\t\tif (!exact) {\n\t\t\t\t\topts.push({ value: '__create__', label: term, __create: true })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn opts\n\t\t},\n\n\t\t/**\n\t\t * Stable string identity for `filters`. Watching the object itself\n\t\t * would re-fire on every parent re-render (a fresh object literal is a\n\t\t * new identity even when the values are unchanged), reloading options\n\t\t * and clobbering the selection in a loop.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tfiltersKey() {\n\t\t\treturn JSON.stringify(this.filters || {})\n\t\t},\n\n\t\t/**\n\t\t * `filters` with empty entries dropped. A parent that has not been\n\t\t * chosen yet holds `null`, and forwarding `client=` as a query param\n\t\t * asks the API for objects whose client is the empty string — which\n\t\t * matches nothing and reads as \"no results\" rather than \"not scoped\".\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tactiveFilters() {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.filters || {})) {\n\t\t\t\tif (value !== null && value !== undefined && value !== '') {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.ensureSelectedLoaded()\n\t\t\t},\n\t\t},\n\n\t\tfiltersKey(next, prev) {\n\t\t\tif (next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The scope moved, so anything already selected may no longer be\n\t\t\t// in it. Drop the stale options + selection, then re-seed.\n\t\t\tthis.options = []\n\t\t\tif (this.modelValue) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t}\n\t\t\tif (this.search.trim().length >= this.minChars) {\n\t\t\t\tthis.onSearch(this.search)\n\t\t\t} else {\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadInitialOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Seed the dropdown with a first page of options so the field can be\n\t\t * browsed without typing. No-op unless `preload` is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadInitialOptions() {\n\t\t\tif (!this.preload || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = items.map((o) => this.toOption(o))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The translated \"Create 'X'\" label for the synthetic option.\n\t\t *\n\t\t * @param {string} term The typed term.\n\t\t * @return {string}\n\t\t */\n\t\tcreateLabelFor(term) {\n\t\t\treturn t('nextcloud-vue', 'Create \"{term}\"', { term })\n\t\t},\n\n\t\t/**\n\t\t * Run an object search (debounced via NcSelect's own throttling) for the\n\t\t * typed term. Failures degrade to an empty option list.\n\t\t *\n\t\t * @param {string} term The search term from NcSelect.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSearch(term) {\n\t\t\tthis.search = term || ''\n\t\t\tif (!this.objectStore || this.search.trim().length < this.minChars) {\n\t\t\t\t// Below the search threshold: fall back to the browsable first\n\t\t\t\t// page when preloading, rather than blanking the list.\n\t\t\t\tthis.options = []\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_search: this.search.trim(),\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = items.map((o) => this.toOption(o))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a selection. A real option emits its id; the synthetic create\n\t\t * option triggers object creation.\n\t\t *\n\t\t * @param {object|null} option The chosen option (or null on clear).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onInput(option) {\n\t\t\tif (!option) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected (or newly-created) object id, or '' when cleared.\n\t\t\t\t * @type {string}\n\t\t\t\t */\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (option.__create) {\n\t\t\t\tawait this.createFromTerm(option.label)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.localSelected = option\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Create a new object from the typed term and select it.\n\t\t *\n\t\t * @param {string} term The term to write to `labelField`.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync createFromTerm(term) {\n\t\t\tif (!this.objectStore && !this.createHandler) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst name = (term || '').trim()\n\t\t\tif (!name) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The active scope is part of the new object's identity: a\n\t\t\t\t// contact created while the list is filtered to one client\n\t\t\t\t// belongs to that client. An explicit createDefaults entry\n\t\t\t\t// still wins.\n\t\t\t\tconst payload = { ...this.activeFilters, ...this.createDefaults, [this.labelField]: name }\n\t\t\t\tlet created\n\t\t\t\tif (this.createHandler) {\n\t\t\t\t\t// A falsy resolve means the consumer aborted (cancelled\n\t\t\t\t\t// dialog) — leave the selection untouched.\n\t\t\t\t\tcreated = await this.createHandler(name, payload)\n\t\t\t\t} else {\n\t\t\t\t\tthis.ensureRegistered()\n\t\t\t\t\tcreated = await this.objectStore.saveObject(this.typeSlug, payload)\n\t\t\t\t}\n\t\t\t\tif (!created) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst option = this.toOption(created)\n\t\t\t\tthis.options = [option, ...this.options.filter((o) => o.value !== option.value)]\n\t\t\t\tthis.localSelected = option\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\t/**\n\t\t\t\t * @event create A new object was created from the search term.\n\t\t\t\t * @type {object} The created OpenRegister object.\n\t\t\t\t */\n\t\t\t\tthis.$emit('create', created)\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnResourceSelect] create failed', e)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Map an OpenRegister object to an option `{ value, label }`.\n\t\t *\n\t\t * @param {object} obj The object.\n\t\t * @return {{value: string, label: string}}\n\t\t */\n\t\ttoOption(obj) {\n\t\t\tconst id = String(obj.id || (obj['@self'] && obj['@self'].id) || '')\n\t\t\tconst label = obj[this.labelField] || obj.name || obj.title || id\n\t\t\treturn { value: id, label }\n\t\t},\n\n\t\t/**\n\t\t * Register the object type with the store (idempotent) so search/create\n\t\t * route to the right register+schema.\n\t\t */\n\t\tensureRegistered() {\n\t\t\tif (this.objectStore && typeof this.objectStore.registerObjectType === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tthis.objectStore.registerObjectType(this.typeSlug, this.schema, this.register)\n\t\t\t\t} catch {\n\t\t\t\t\t// Already registered or store not ready — non-fatal.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * When a `modelValue` is set but no matching option is loaded, fetch the\n\t\t * single object so its label renders (e.g. a pre-filled client id).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSelectedLoaded() {\n\t\t\tif (!this.modelValue || this.selectedOption || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tif (typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, String(this.modelValue))\n\t\t\t\tif (obj) {\n\t\t\t\t\tthis.localSelected = this.toOption(obj)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-resource-select__create {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n}\n</style>\n"],"names":["_withCtx","_openBlock","_createElementBlock","_createVNode","_toDisplayString"],"mappings":";;;;AAuByB,EAAA,KAAK,EAAC,4BAAK;;;;;;;;;AAbjC,IAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,IAAA,UAAU,EAAE,MAAQ,CAAA,UAAA;AACpB,IAAA,UAAU,EAAE,QAAM,CAAA,cAAA;AAClB,IAAA,OAAO,EAAE,QAAc,CAAA,cAAA;AACvB,IAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,IAAA,SAAS,EAAE,MAAA,CAAA,SAAA;AACX,IAAA,QAAQ,EAAE,MAAM,CAAA,QAAA;AAChB,IAAA,WAAW,EAAE,MAAW,CAAA,WAAA;AACxB,IAAA,UAAU,EAAE,KAAK;SACb,EAAE,OAAM;AACX,IAAA,QAAI,EAAG,QAAQ,CAAA,QAAA;yBACC,EAAE,QAAO,CAAA,OAAA;;AACpB,IAAA,MAAA,EACNA,OAAA,CAAA,GADM,KAAA,EAAA,QAAA,EAAA,QAAA,EAAA,KAAA;MACM,QAAQ;AAApB,WAAAC,SAAA,EAAA,EAAAC,kBAAA,SAAA;YACCC,WAAQ,CAAA,eAAA,EAAA,EAAD,IAAC,EAAA,EAAA,EAAA,CAAA;4BAAA,GACR,GAAAC,eAAA,CAAG,QAAI,CAAA,cAAA,CAAA,QAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnResourceSelect.vue3.js","sources":["../../../../src/components/CnResourceSelect/CnResourceSelect.vue?vue&type=template&id=37b7ff6f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<NcSelect\n\t\t:inputId=\"inputId\"\n\t\t:inputLabel=\"inputLabel\"\n\t\t:modelValue=\"selectedOption\"\n\t\t:options=\"displayOptions\"\n\t\t:loading=\"loading\"\n\t\t:clearable=\"clearable\"\n\t\t:disabled=\"disabled\"\n\t\t:placeholder=\"placeholder\"\n\t\t:filterable=\"false\"\n\t\t:multiple=\"multiple\"\n\t\t:keepOpen=\"multiple\"\n\t\tlabel=\"label\"\n\t\t@search=\"onSearch\"\n\t\t@update:modelValue=\"onInput\">\n\t\t<template #option=\"{ label: optLabel, __create }\">\n\t\t\t<span v-if=\"__create\" class=\"cn-resource-select__create\">\n\t\t\t\t<Plus :size=\"16\" />\n\t\t\t\t{{ createLabelFor(optLabel) }}\n\t\t\t</span>\n\t\t\t<span v-else>{{ optLabel }}</span>\n\t\t</template>\n\t</NcSelect>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport { useObjectStore } from '../../store/index.js'\n\n/**\n * CnResourceSelect — an OpenRegister object picker that can CREATE from the\n * search term.\n *\n * Searches a `register` + `schema` for objects matching the typed text and\n * renders them as options. When the typed term matches no existing object, a\n * synthetic **\"Create '<term>'\"** option appears at the bottom; choosing it\n * persists a new object (via `useObjectStore().saveObject`, writing the term to\n * `labelField`) and selects it — so an agent who can't find a client just types\n * the name and makes one inline, never hitting a dead \"no results\" end.\n *\n * Emits `update:modelValue` (the selected object's id) and `create` (the\n * freshly-created object). The label of each option comes from `labelField`\n * (default `name`), the value from the object's id.\n *\n * Backwards-compatible, self-contained: it owns its own search + create flow,\n * so a consumer only needs to pass `register` + `schema` and bind `modelValue`.\n *\n * Three opt-in props cover the cases a plain object picker cannot:\n *\n * - `filters` SCOPES the options to a parent selection, which is what makes a\n * cascading pair work (pick a client, then only that client's contacts are\n * offered). Changing the scope clears a now-invalid selection.\n * - `preload` fetches a first page on mount so the field can be BROWSED, not\n * only searched — the behaviour a user expects from a select.\n * - `createHandler` replaces the built-in save. Creating from a bare term only\n * works while the term is enough to satisfy the schema; the moment a\n * required field must come from somewhere else (a server-minted key, a value\n * worth collecting in a full dialog) the consumer owns the create and\n * resolves the finished object back.\n *\n * Example:\n * ```vue\n * <CnResourceSelect\n * register=\"pipelinq\"\n * schema=\"client\"\n * label-field=\"name\"\n * :model-value=\"clientId\"\n * :input-label=\"t('pipelinq', 'Client')\"\n * @update:modelValue=\"clientId = $event\"\n * @create=\"onClientCreated\" />\n * ```\n */\nexport default {\n\tname: 'CnResourceSelect',\n\n\tcomponents: { NcSelect, Plus },\n\n\tprops: {\n\t\t/** OpenRegister register slug to search/create in. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** OpenRegister schema slug to search/create in. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Currently-selected object id (v-model), or an array of ids when `multiple`. */\n\t\tmodelValue: {\n\t\t\ttype: [String, Number, Array],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Pick several objects. `modelValue` is then an array of ids, and a\n\t\t * create adds the new object to the selection.\n\t\t */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Object field used as the option label AND written on create. */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\n\t\t/** Accessible input label for the underlying NcSelect. */\n\t\tinputLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** DOM id for the input (a11y association). */\n\t\tinputId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the selection can be cleared. */\n\t\tclearable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Minimum characters before searching / offering create. */\n\t\tminChars: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * Whether to offer the inline \"Create '<term>'\" option when the search\n\t\t * yields no exact match. Off → behaves like a plain async object select.\n\t\t */\n\t\tallowCreate: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Extra fields merged into the payload when creating a new object (e.g.\n\t\t * a fixed `type` or `status`). The `labelField` is always set to the term.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcreateDefaults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Field filters merged into the search query, so the option list can be\n\t\t * SCOPED to a parent selection — the cascading-select case (pick a\n\t\t * client, then only that client's contacts are offered). Changing this\n\t\t * reloads the options and clears a selection that no longer belongs to\n\t\t * the new scope.\n\t\t *\n\t\t * Example: `:filters=\"{ client: form.client }\"`.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfilters: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Load a first page of options on mount (and whenever `filters`\n\t\t * change) instead of waiting for `minChars` of typing. Lets the field\n\t\t * be BROWSED like a plain select while still searching server-side once\n\t\t * the user types. Off by default so existing consumers keep their\n\t\t * search-only behaviour and issue no extra request.\n\t\t */\n\t\tpreload: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Disable the input — e.g. a dependent select waiting on its parent.\n\t\t */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Placeholder text for the underlying NcSelect. */\n\t\tplaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Override how a new object is created from the typed term. Receives\n\t\t * `(term, payload)` and MUST resolve to the created object (or a falsy\n\t\t * value to abort, e.g. the user cancelled a dialog).\n\t\t *\n\t\t * Without it the component posts `payload` straight to\n\t\t * `objectStore.saveObject`, which is wrong whenever the schema requires\n\t\t * a field the term cannot supply — a server-minted foreign key, say, or\n\t\t * anything the consumer would rather collect in a full create dialog.\n\t\t *\n\t\t * @type {((term: string, payload: object) => Promise<object|null>)|null}\n\t\t */\n\t\tcreateHandler: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'create'],\n\n\tdata() {\n\t\treturn {\n\t\t\tsearch: '',\n\t\t\tloading: false,\n\t\t\toptions: [],\n\t\t\t// The selected option object kept locally so the label shows even\n\t\t\t// before/without a matching search result (e.g. a pre-set modelValue).\n\t\t\tlocalSelected: null,\n\t\t\t// Options seen so far, keyed by value, so a multiple selection\n\t\t\t// keeps its labels when the search results move on.\n\t\t\tknown: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Pinia object store handle (resolved lazily so a Pinia-less test still mounts). */\n\t\tobjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The selected ids as strings (multiple mode).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tselectedIds() {\n\t\t\tconst value = this.modelValue\n\t\t\tif (Array.isArray(value)) {\n\t\t\t\treturn value.filter((v) => v !== null && v !== undefined && v !== '').map((v) => String(v))\n\t\t\t}\n\t\t\treturn value ? [String(value)] : []\n\t\t},\n\n\t\t/** The `${register}-${schema}` object-type slug used by the store. */\n\t\ttypeSlug() {\n\t\t\treturn `${this.register}-${this.schema}`\n\t\t},\n\n\t\t/** The currently-selected option, for NcSelect's model-value. */\n\t\tselectedOption() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn this.selectedIds.map((id) => this.known[id]\n\t\t\t\t\t|| this.options.find((o) => o.value === id)\n\t\t\t\t\t|| { value: id, label: id })\n\t\t\t}\n\t\t\tif (this.localSelected && this.localSelected.value === this.modelValue) {\n\t\t\t\treturn this.localSelected\n\t\t\t}\n\t\t\treturn this.options.find((o) => o.value === this.modelValue) || null\n\t\t},\n\n\t\t/**\n\t\t * The option list shown in the dropdown — the search results, plus a\n\t\t * synthetic \"Create '<term>'\" entry when create is allowed and the term\n\t\t * is long enough and matches no existing option label exactly.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdisplayOptions() {\n\t\t\tconst opts = [...this.options]\n\t\t\tconst term = this.search.trim()\n\t\t\tif (this.allowCreate && term.length >= this.minChars) {\n\t\t\t\tconst exact = opts.some((o) => (o.label || '').toLowerCase() === term.toLowerCase())\n\t\t\t\tif (!exact) {\n\t\t\t\t\topts.push({ value: '__create__', label: term, __create: true })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn opts\n\t\t},\n\n\t\t/**\n\t\t * Stable string identity for `filters`. Watching the object itself\n\t\t * would re-fire on every parent re-render (a fresh object literal is a\n\t\t * new identity even when the values are unchanged), reloading options\n\t\t * and clobbering the selection in a loop.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tfiltersKey() {\n\t\t\treturn JSON.stringify(this.filters || {})\n\t\t},\n\n\t\t/**\n\t\t * `filters` with empty entries dropped. A parent that has not been\n\t\t * chosen yet holds `null`, and forwarding `client=` as a query param\n\t\t * asks the API for objects whose client is the empty string — which\n\t\t * matches nothing and reads as \"no results\" rather than \"not scoped\".\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tactiveFilters() {\n\t\t\tconst out = {}\n\t\t\tfor (const [key, value] of Object.entries(this.filters || {})) {\n\t\t\t\tif (value !== null && value !== undefined && value !== '') {\n\t\t\t\t\tout[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\t},\n\n\twatch: {\n\t\tmodelValue: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.ensureSelectedLoaded()\n\t\t\t},\n\t\t},\n\n\t\tfiltersKey(next, prev) {\n\t\t\tif (next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// The scope moved, so anything already selected may no longer be\n\t\t\t// in it. Drop the stale options + selection, then re-seed.\n\t\t\tthis.options = []\n\t\t\tif (this.selectedIds.length > 0) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\tthis.$emit('update:modelValue', this.multiple ? [] : '')\n\t\t\t}\n\t\t\tif (this.search.trim().length >= this.minChars) {\n\t\t\t\tthis.onSearch(this.search)\n\t\t\t} else {\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t}\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.loadInitialOptions()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Seed the dropdown with a first page of options so the field can be\n\t\t * browsed without typing. No-op unless `preload` is set.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadInitialOptions() {\n\t\t\tif (!this.preload || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = this.remember(items.map((o) => this.toOption(o)))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The translated \"Create 'X'\" label for the synthetic option.\n\t\t *\n\t\t * @param {string} term The typed term.\n\t\t * @return {string}\n\t\t */\n\t\tcreateLabelFor(term) {\n\t\t\treturn t('nextcloud-vue', 'Create \"{term}\"', { term })\n\t\t},\n\n\t\t/**\n\t\t * Run an object search (debounced via NcSelect's own throttling) for the\n\t\t * typed term. Failures degrade to an empty option list.\n\t\t *\n\t\t * @param {string} term The search term from NcSelect.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSearch(term) {\n\t\t\tthis.search = term || ''\n\t\t\tif (!this.objectStore || this.search.trim().length < this.minChars) {\n\t\t\t\t// Below the search threshold: fall back to the browsable first\n\t\t\t\t// page when preloading, rather than blanking the list.\n\t\t\t\tthis.options = []\n\t\t\t\tthis.loadInitialOptions()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tconst collection = await this.objectStore.fetchCollectionForOptions(this.typeSlug, {\n\t\t\t\t\t...this.activeFilters,\n\t\t\t\t\t_search: this.search.trim(),\n\t\t\t\t\t_limit: 20,\n\t\t\t\t})\n\t\t\t\tconst items = Array.isArray(collection)\n\t\t\t\t\t? collection\n\t\t\t\t\t: (this.objectStore.collections[this.typeSlug] || [])\n\t\t\t\tthis.options = this.remember(items.map((o) => this.toOption(o)))\n\t\t\t} catch {\n\t\t\t\tthis.options = []\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Handle a selection. A real option emits its id; the synthetic create\n\t\t * option triggers object creation.\n\t\t *\n\t\t * @param {object|null} option The chosen option (or null on clear).\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onInput(option) {\n\t\t\tif (this.multiple) {\n\t\t\t\tawait this.onMultipleInput(Array.isArray(option) ? option : (option ? [option] : []))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!option) {\n\t\t\t\tthis.localSelected = null\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected (or newly-created) object id, or '' when cleared.\n\t\t\t\t * @type {string}\n\t\t\t\t */\n\t\t\t\tthis.$emit('update:modelValue', '')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (option.__create) {\n\t\t\t\tawait this.createFromTerm(option.label)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.localSelected = option\n\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t},\n\n\t\t/**\n\t\t * Handle a change of a multiple selection. A synthetic create option\n\t\t * creates the object and adds it; the rest emit their ids.\n\t\t *\n\t\t * @param {Array<object>} chosen The selected options.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onMultipleInput(chosen) {\n\t\t\tconst createOption = chosen.find((o) => o && o.__create)\n\t\t\tconst ids = chosen.filter((o) => o && !o.__create).map((o) => String(o.value))\n\t\t\tthis.remember(chosen.filter((o) => o && !o.__create))\n\t\t\tif (!createOption) {\n\t\t\t\tthis.$emit('update:modelValue', ids)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst created = await this.createFromTerm(createOption.label, ids)\n\t\t\tif (!created) {\n\t\t\t\tthis.$emit('update:modelValue', ids)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Cache options by value so their labels survive a new search.\n\t\t *\n\t\t * @param {Array<object>} options Options to remember.\n\t\t * @return {Array<object>} The same options.\n\t\t */\n\t\tremember(options) {\n\t\t\tconst next = { ...this.known }\n\t\t\tfor (const o of options) {\n\t\t\t\tif (o && o.value) {\n\t\t\t\t\tnext[o.value] = o\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.known = next\n\t\t\treturn options\n\t\t},\n\n\t\t/**\n\t\t * Create a new object from the typed term and select it.\n\t\t *\n\t\t * @param {string} term The term to write to `labelField`.\n\t\t * @param {string[]} [keep] Multiple mode: the ids already selected, which the new id joins.\n\t\t * @return {Promise<object|null>} The created object, or null.\n\t\t */\n\t\tasync createFromTerm(term, keep = []) {\n\t\t\tif (!this.objectStore && !this.createHandler) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst name = (term || '').trim()\n\t\t\tif (!name) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\ttry {\n\t\t\t\t// The active scope is part of the new object's identity: a\n\t\t\t\t// contact created while the list is filtered to one client\n\t\t\t\t// belongs to that client. An explicit createDefaults entry\n\t\t\t\t// still wins.\n\t\t\t\tconst payload = { ...this.activeFilters, ...this.createDefaults, [this.labelField]: name }\n\t\t\t\tlet created\n\t\t\t\tif (this.createHandler) {\n\t\t\t\t\t// A falsy resolve means the consumer aborted (cancelled\n\t\t\t\t\t// dialog) — leave the selection untouched.\n\t\t\t\t\tcreated = await this.createHandler(name, payload)\n\t\t\t\t} else {\n\t\t\t\t\tthis.ensureRegistered()\n\t\t\t\t\tcreated = await this.objectStore.saveObject(this.typeSlug, payload)\n\t\t\t\t}\n\t\t\t\tif (!created) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst option = this.toOption(created)\n\t\t\t\tthis.remember([option])\n\t\t\t\tthis.options = [option, ...this.options.filter((o) => o.value !== option.value)]\n\t\t\t\tthis.localSelected = option\n\t\t\t\tthis.search = ''\n\t\t\t\tthis.$emit('update:modelValue', this.multiple\n\t\t\t\t\t? [...keep.filter((id) => id !== option.value), option.value]\n\t\t\t\t\t: option.value)\n\t\t\t\t/**\n\t\t\t\t * @event create A new object was created from the search term.\n\t\t\t\t * @type {object} The created OpenRegister object.\n\t\t\t\t */\n\t\t\t\tthis.$emit('create', created)\n\t\t\t\treturn created\n\t\t\t} catch (e) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnResourceSelect] create failed', e)\n\t\t\t\treturn null\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Map an OpenRegister object to an option `{ value, label }`.\n\t\t *\n\t\t * @param {object} obj The object.\n\t\t * @return {{value: string, label: string}}\n\t\t */\n\t\ttoOption(obj) {\n\t\t\tconst id = String(obj.id || (obj['@self'] && obj['@self'].id) || '')\n\t\t\tconst label = obj[this.labelField] || obj.name || obj.title || id\n\t\t\treturn { value: id, label }\n\t\t},\n\n\t\t/**\n\t\t * Register the object type with the store (idempotent) so search/create\n\t\t * route to the right register+schema.\n\t\t */\n\t\tensureRegistered() {\n\t\t\tif (this.objectStore && typeof this.objectStore.registerObjectType === 'function') {\n\t\t\t\ttry {\n\t\t\t\t\tthis.objectStore.registerObjectType(this.typeSlug, this.schema, this.register)\n\t\t\t\t} catch {\n\t\t\t\t\t// Already registered or store not ready — non-fatal.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * When a `modelValue` is set but no matching option is loaded, fetch the\n\t\t * single object so its label renders (e.g. a pre-filled client id).\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureSelectedLoaded() {\n\t\t\tif (this.multiple) {\n\t\t\t\tawait this.ensureManyLoaded()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.modelValue || this.selectedOption || !this.objectStore) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.ensureRegistered()\n\t\t\t\tif (typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, String(this.modelValue))\n\t\t\t\tif (obj) {\n\t\t\t\t\tthis.localSelected = this.toOption(obj)\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Multiple mode: fetch each selected id whose label is not known yet.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync ensureManyLoaded() {\n\t\t\tif (!this.objectStore || typeof this.objectStore.fetchObject !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst missing = this.selectedIds.filter((id) => !this.known[id] && !this.options.some((o) => o.value === id))\n\t\t\tif (missing.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.ensureRegistered()\n\t\t\tfor (const id of missing) {\n\t\t\t\ttry {\n\t\t\t\t\tconst obj = await this.objectStore.fetchObject(this.typeSlug, id)\n\t\t\t\t\tif (obj) {\n\t\t\t\t\t\tthis.remember([this.toOption(obj)])\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Leave the id un-labelled rather than crash.\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-resource-select__create {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tcolor: var(--color-primary-element);\n\tfont-weight: 600;\n}\n</style>\n"],"names":["_withCtx","_openBlock","_createElementBlock","_createVNode","_toDisplayString"],"mappings":";;;;AAyByB,EAAA,KAAK,EAAC,4BAAK;;;;;;;;;AAfjC,IAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,IAAA,UAAU,EAAE,MAAQ,CAAA,UAAA;AACpB,IAAA,UAAU,EAAE,QAAM,CAAA,cAAA;AAClB,IAAA,OAAO,EAAE,QAAc,CAAA,cAAA;AACvB,IAAA,OAAO,EAAE,KAAO,CAAA,OAAA;AAChB,IAAA,SAAS,EAAE,MAAO,CAAA,SAAA;AAClB,IAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,IAAA,WAAW,EAAA,MAAA,CAAA,WAAA;AACX,IAAA,UAAU,EAAE,KAAI;AAChB,IAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,IAAA,QAAS,EAAC,MAAQ,CAAA,QAAA;SACd,EAAE,OAAM;AACX,IAAA,QAAI,EAAG,QAAQ,CAAA,QAAA;yBACC,EAAE,QAAO,CAAA,OAAA;;AACpB,IAAA,MAAA,EACNA,OAAA,CAAA,GADM,KAAA,EAAA,QAAA,EAAA,QAAA,EAAA,KAAA;MACM,QAAQ;AAApB,WAAAC,SAAA,EAAA,EAAAC,kBAAA,SAAA;YACCC,WAAQ,CAAA,eAAA,EAAA,EAAD,IAAC,EAAA,EAAA,EAAA,CAAA;4BAAA,GACR,GAAAC,eAAA,CAAG,QAAI,CAAA,cAAA,CAAA,QAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;;;"}
|