@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/nextcloud-vue.cjs +67614 -0
  2. package/dist/nextcloud-vue.cjs.js +9559 -8983
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.cjs.map +1 -0
  5. package/dist/nextcloud-vue.css +1231 -1231
  6. package/dist/nextcloud-vue.esm.js +9559 -8983
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
  15. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  16. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  17. package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
  18. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  19. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  20. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
  21. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
  22. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  23. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  24. package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
  25. package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
  26. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
  27. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  28. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  29. package/src/components/CnIcon/CnIcon.vue +89 -89
  30. package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
  31. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
  32. package/src/components/CnItemCard/CnItemCard.vue +132 -132
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  34. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  35. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  36. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  37. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  38. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  39. package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
  40. package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
  41. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  42. package/src/components/CnObjectCard/eslint-setup.md +235 -0
  43. package/src/components/CnObjectCard/package.json-or.json +132 -0
  44. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
  45. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  46. package/src/components/CnPagination/CnPagination.vue +252 -252
  47. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  48. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  49. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
  50. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
  51. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
  52. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
  53. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
  54. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  55. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  56. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
  57. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  58. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
  59. package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
  60. package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
  61. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
  62. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
  63. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  64. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
  65. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
  66. package/src/index.js +1 -1
  67. package/src/types/notification.d.ts +13 -13
  68. package/src/types/organisation.d.ts +15 -15
  69. package/src/types/schema.d.ts +13 -13
  70. package/src/types/task.d.ts +6 -6
  71. package/src/utils/headers.js +5 -3
@@ -1,792 +1,792 @@
1
- <template>
2
- <CnSettingsSection
3
- :name="name"
4
- :description="description"
5
- :doc-url="docUrl"
6
- :loading="registersLoading"
7
- loading-message="Loading registers..."
8
- :error="!!registersError"
9
- :error-message="registersError || ''"
10
- :on-retry="loadRegisters">
11
- <!-- Action buttons -->
12
- <template #actions>
13
- <NcButton
14
- v-if="showSaveButton"
15
- type="primary"
16
- :disabled="saving || !hasChanges"
17
- @click="handleSave">
18
- <template #icon>
19
- <NcLoadingIcon v-if="saving" :size="20" />
20
- <ContentSave v-else :size="20" />
21
- </template>
22
- {{ saving ? 'Saving...' : saveButtonText }}
23
- </NcButton>
24
- <NcButton
25
- v-if="showReimportButton"
26
- type="secondary"
27
- :disabled="reimporting"
28
- @click="$emit('reimport')">
29
- <template #icon>
30
- <NcLoadingIcon v-if="reimporting" :size="20" />
31
- <Refresh v-else :size="20" />
32
- </template>
33
- {{ reimporting ? 'Importing...' : reimportButtonText }}
34
- </NcButton>
35
- <slot name="actions" />
36
- </template>
37
-
38
- <!-- Group sections -->
39
- <div v-if="!registersLoading && !registersError" class="cn-register-mapping">
40
- <div
41
- v-for="(group, groupIdx) in groups"
42
- :key="groupIdx"
43
- class="cn-register-mapping__group">
44
- <!-- Group header -->
45
- <slot name="group-header"
46
- :group="group"
47
- :configured-count="configuredCount(groupIdx)"
48
- :total-count="group.types.length">
49
- <div class="cn-register-mapping__group-header">
50
- <h4 class="cn-register-mapping__group-title">
51
- {{ group.name }}
52
- </h4>
53
- <span class="cn-register-mapping__group-status">
54
- {{ configuredCount(groupIdx) }}/{{ group.types.length }} {{ labels.partiallyConfigured }}
55
- </span>
56
- </div>
57
- </slot>
58
-
59
- <!-- Group description -->
60
- <p v-if="group.description" class="cn-register-mapping__group-description">
61
- {{ group.description }}
62
- </p>
63
-
64
- <!-- Register selector -->
65
- <div class="cn-register-mapping__register-select">
66
- <label class="cn-register-mapping__label">{{ labels.register }}</label>
67
- <NcSelect
68
- :value="selectedRegister(groupIdx)"
69
- :options="registerSelectOptions"
70
- :placeholder="labels.selectRegister"
71
- :loading="registersLoading"
72
- label="label"
73
- track-by="value"
74
- @input="handleRegisterChange(groupIdx, $event)" />
75
- </div>
76
-
77
- <!-- Type list -->
78
- <div v-if="selectedRegister(groupIdx)" class="cn-register-mapping__type-list">
79
- <!-- Header row -->
80
- <div class="cn-register-mapping__type-list-header">
81
- <span>Name</span>
82
- <span>Schema</span>
83
- <span />
84
- <span />
85
- </div>
86
-
87
- <!-- Type rows -->
88
- <template v-for="type in group.types">
89
- <div
90
- :key="type.slug + '-row'"
91
- class="cn-register-mapping__type-row"
92
- :class="{ 'cn-register-mapping__type-row--expanded': isExpanded(groupIdx, type.slug) }"
93
- @click="toggleExpand(groupIdx, type.slug)">
94
- <span class="cn-register-mapping__type-name">{{ type.label }}</span>
95
- <span class="cn-register-mapping__type-schema">
96
- {{ schemaLabel(groupIdx, type) || labels.notConfigured }}
97
- </span>
98
- <span class="cn-register-mapping__type-status">
99
- <span
100
- class="cn-register-mapping__status-dot"
101
- :class="schemaValue(groupIdx, type)
102
- ? 'cn-register-mapping__status-dot--configured'
103
- : 'cn-register-mapping__status-dot--unconfigured'" />
104
- </span>
105
- <span class="cn-register-mapping__type-chevron">
106
- <ChevronUp v-if="isExpanded(groupIdx, type.slug)" :size="20" />
107
- <ChevronDown v-else :size="20" />
108
- </span>
109
- </div>
110
-
111
- <!-- Expanded detail -->
112
- <transition :key="type.slug + '-detail'" name="slide">
113
- <div
114
- v-if="isExpanded(groupIdx, type.slug)"
115
- class="cn-register-mapping__type-detail">
116
- <p v-if="type.description" class="cn-register-mapping__type-description">
117
- {{ type.description }}
118
- </p>
119
- <NcSelect
120
- :value="selectedSchema(groupIdx, type)"
121
- :options="schemaSelectOptions(groupIdx)"
122
- :placeholder="labels.selectSchema"
123
- label="label"
124
- track-by="value"
125
- @input="handleSchemaChange(groupIdx, type, $event)" />
126
- </div>
127
- </transition>
128
- </template>
129
- </div>
130
-
131
- <!-- No register selected -->
132
- <NcNoteCard v-else-if="!registersLoading" type="info">
133
- {{ labels.selectRegister }}
134
- </NcNoteCard>
135
-
136
- <!-- No schemas available -->
137
- <NcNoteCard
138
- v-if="selectedRegister(groupIdx) && schemaSelectOptions(groupIdx).length === 0 && !registersLoading"
139
- type="warning">
140
- {{ labels.noSchemas }}
141
- </NcNoteCard>
142
- </div>
143
- </div>
144
-
145
- <!-- Footer -->
146
- <template v-if="$slots.footer" #footer>
147
- <slot name="footer" />
148
- </template>
149
- </CnSettingsSection>
150
- </template>
151
-
152
- <script>
153
- import { CnSettingsSection } from '../CnSettingsSection/index.js'
154
- import { NcButton, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'
155
- import ContentSave from 'vue-material-design-icons/ContentSave.vue'
156
- import Refresh from 'vue-material-design-icons/Refresh.vue'
157
- import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
158
- import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
159
- import { buildHeaders } from '../../utils/headers.js'
160
-
161
- /**
162
- * CnRegisterMapping - OpenRegister register/schema configuration component.
163
- *
164
- * Displays and manages register-to-schema mappings for app object types.
165
- * Self-fetches available registers and schemas from the OpenRegister API.
166
- * Supports multiple register groups (stacked sections) with expandable
167
- * type rows for manual schema override.
168
- *
169
- * @example Single register (Pipelinq)
170
- * <CnRegisterMapping
171
- * name="Register Configuration"
172
- * :groups="[{
173
- * name: 'Core Objects',
174
- * types: [
175
- * { slug: 'client', label: 'Client' },
176
- * { slug: 'contact', label: 'Contact' },
177
- * ],
178
- * }]"
179
- * :configuration="config"
180
- * :show-reimport-button="true"
181
- * @save="saveConfig"
182
- * @reimport="reimport" />
183
- *
184
- * @example Multi-register (SoftwareCatalog)
185
- * <CnRegisterMapping
186
- * :groups="[
187
- * { name: 'Voorzieningen', registerConfigKey: 'voorzieningen_register', types: [...] },
188
- * { name: 'AMEF', registerConfigKey: 'amef_register', types: [...] },
189
- * ]"
190
- * :configuration="config"
191
- * @save="saveConfig" />
192
- */
193
- export default {
194
- name: 'CnRegisterMapping',
195
-
196
- components: {
197
- CnSettingsSection,
198
- NcButton,
199
- NcLoadingIcon,
200
- NcNoteCard,
201
- NcSelect,
202
- ContentSave,
203
- Refresh,
204
- ChevronDown,
205
- ChevronUp,
206
- },
207
-
208
- props: {
209
- /** Section title */
210
- name: {
211
- type: String,
212
- default: 'Register Configuration',
213
- },
214
- /** Section description */
215
- description: {
216
- type: String,
217
- default: 'Configure OpenRegister schema mappings for your object types',
218
- },
219
- /** Documentation URL */
220
- docUrl: {
221
- type: String,
222
- default: '',
223
- },
224
- /**
225
- * Groups of object types that share a register.
226
- * @type {Array<{ name: string, description?: string, registerConfigKey?: string, types: Array<{ slug: string, label: string, description?: string, configKey?: string }> }>}
227
- */
228
- groups: {
229
- type: Array,
230
- required: true,
231
- validator: (groups) => groups.length > 0
232
- && groups.every((g) => g.name && Array.isArray(g.types) && g.types.length > 0),
233
- },
234
- /** Current configuration values: { register: '5', client_schema: '28', ... } */
235
- configuration: {
236
- type: Object,
237
- default: () => ({}),
238
- },
239
- /** Show save button */
240
- showSaveButton: {
241
- type: Boolean,
242
- default: true,
243
- },
244
- /** Whether save is in progress */
245
- saving: {
246
- type: Boolean,
247
- default: false,
248
- },
249
- /** Show reimport button */
250
- showReimportButton: {
251
- type: Boolean,
252
- default: false,
253
- },
254
- /** Whether reimport is in progress */
255
- reimporting: {
256
- type: Boolean,
257
- default: false,
258
- },
259
- /** Save button text */
260
- saveButtonText: {
261
- type: String,
262
- default: 'Save Configuration',
263
- },
264
- /** Reimport button text */
265
- reimportButtonText: {
266
- type: String,
267
- default: 'Re-import configuration',
268
- },
269
- /** Auto-match schema titles to type slugs on register change */
270
- autoMatch: {
271
- type: Boolean,
272
- default: true,
273
- },
274
- /** UI labels (i18n) */
275
- labels: {
276
- type: Object,
277
- default: () => ({
278
- register: 'Register',
279
- schema: 'Schema',
280
- configured: 'Configured',
281
- notConfigured: 'Not configured',
282
- noSchemas: 'No schemas available in this register',
283
- selectRegister: 'Select a register',
284
- selectSchema: 'Select a schema',
285
- allConfigured: 'All types configured',
286
- partiallyConfigured: 'configured',
287
- }),
288
- },
289
- },
290
-
291
- emits: ['update:configuration', 'save', 'reimport'],
292
-
293
- data() {
294
- return {
295
- // Fetched data
296
- registers: [],
297
- schemasByRegister: {},
298
- registersLoading: false,
299
- registersError: null,
300
- // Local state
301
- localConfig: {},
302
- expandedRows: {},
303
- }
304
- },
305
-
306
- computed: {
307
- /** Register options for NcSelect */
308
- registerSelectOptions() {
309
- return this.registers.map((r) => ({
310
- label: r.title || r.slug || `Register ${r.id}`,
311
- value: String(r.id),
312
- }))
313
- },
314
-
315
- /** Whether local config differs from prop */
316
- hasChanges() {
317
- return JSON.stringify(this.localConfig) !== JSON.stringify(this.configuration)
318
- },
319
- },
320
-
321
- watch: {
322
- configuration: {
323
- handler(newVal) {
324
- this.localConfig = { ...newVal }
325
- },
326
- immediate: true,
327
- deep: true,
328
- },
329
- },
330
-
331
- async mounted() {
332
- await this.loadRegisters()
333
- },
334
-
335
- methods: {
336
- /**
337
- * Get the config key for a group's register.
338
- *
339
- * @param {number} groupIdx Group index
340
- * @return {string} Config key
341
- */
342
- registerConfigKey(groupIdx) {
343
- const group = this.groups[groupIdx]
344
- if (group.registerConfigKey) return group.registerConfigKey
345
- if (this.groups.length === 1) return 'register'
346
- return group.name.toLowerCase().replace(/[^a-z0-9]+/g, '_') + '_register'
347
- },
348
-
349
- /**
350
- * Get the config key for a type's schema.
351
- *
352
- * @param {object} type Type definition
353
- * @return {string} Config key
354
- */
355
- schemaConfigKey(type) {
356
- return type.configKey || type.slug + '_schema'
357
- },
358
-
359
- /**
360
- * Get the selected register option for a group.
361
- *
362
- * @param {number} groupIdx Group index
363
- * @return {object|null} NcSelect option
364
- */
365
- selectedRegister(groupIdx) {
366
- const key = this.registerConfigKey(groupIdx)
367
- const value = String(this.localConfig[key] || '')
368
- if (!value) return null
369
- return this.registerSelectOptions.find((o) => o.value === value) || null
370
- },
371
-
372
- /**
373
- * Get the selected schema option for a type.
374
- *
375
- * @param {number} groupIdx Group index
376
- * @param {object} type Type definition
377
- * @return {object|null} NcSelect option
378
- */
379
- selectedSchema(groupIdx, type) {
380
- const key = this.schemaConfigKey(type)
381
- const value = String(this.localConfig[key] || '')
382
- if (!value) return null
383
- const options = this.schemaSelectOptions(groupIdx)
384
- return options.find((o) => o.value === value) || null
385
- },
386
-
387
- /**
388
- * Get the schema value (ID) for a type.
389
- *
390
- * @param {number} groupIdx Group index
391
- * @param {object} type Type definition
392
- * @return {string} Schema ID or empty string
393
- */
394
- schemaValue(groupIdx, type) {
395
- const key = this.schemaConfigKey(type)
396
- return this.localConfig[key] || ''
397
- },
398
-
399
- /**
400
- * Get the display label for a type's current schema.
401
- *
402
- * @param {number} groupIdx Group index
403
- * @param {object} type Type definition
404
- * @return {string} Schema label or empty string
405
- */
406
- schemaLabel(groupIdx, type) {
407
- const selected = this.selectedSchema(groupIdx, type)
408
- if (selected) return selected.label
409
- const value = this.schemaValue(groupIdx, type)
410
- return value ? `Schema ${value}` : ''
411
- },
412
-
413
- /**
414
- * Get schema options for a group's selected register.
415
- *
416
- * @param {number} groupIdx Group index
417
- * @return {Array<{label: string, value: string}>} NcSelect options
418
- */
419
- schemaSelectOptions(groupIdx) {
420
- const reg = this.selectedRegister(groupIdx)
421
- if (!reg) return []
422
- const schemas = this.schemasByRegister[reg.value] || []
423
- return schemas.map((s) => ({
424
- label: s.title || s.slug || `Schema ${s.id}`,
425
- value: String(s.id),
426
- }))
427
- },
428
-
429
- /**
430
- * Count configured types in a group.
431
- *
432
- * @param {number} groupIdx Group index
433
- * @return {number} Count
434
- */
435
- configuredCount(groupIdx) {
436
- const group = this.groups[groupIdx]
437
- return group.types.filter((type) => !!this.schemaValue(groupIdx, type)).length
438
- },
439
-
440
- /**
441
- * Check if a type row is expanded.
442
- *
443
- * @param {number} groupIdx Group index
444
- * @param {string} slug Type slug
445
- * @return {boolean}
446
- */
447
- isExpanded(groupIdx, slug) {
448
- return !!this.expandedRows[groupIdx + '-' + slug]
449
- },
450
-
451
- /**
452
- * Toggle a type row expansion.
453
- *
454
- * @param {number} groupIdx Group index
455
- * @param {string} slug Type slug
456
- */
457
- toggleExpand(groupIdx, slug) {
458
- const key = groupIdx + '-' + slug
459
- this.expandedRows = {
460
- ...this.expandedRows,
461
- [key]: !this.expandedRows[key],
462
- }
463
- },
464
-
465
- /**
466
- * Handle register selection change for a group.
467
- *
468
- * @param {number} groupIdx Group index
469
- * @param {object|null} option NcSelect option
470
- */
471
- async handleRegisterChange(groupIdx, option) {
472
- const key = this.registerConfigKey(groupIdx)
473
- const value = option?.value || ''
474
-
475
- this.localConfig = { ...this.localConfig, [key]: value }
476
-
477
- // Clear schema selections for this group
478
- const group = this.groups[groupIdx]
479
- for (const type of group.types) {
480
- const schemaKey = this.schemaConfigKey(type)
481
- this.localConfig = { ...this.localConfig, [schemaKey]: '' }
482
- }
483
-
484
- // Fetch schemas for the new register
485
- if (value) {
486
- await this.loadSchemasForRegister(value)
487
-
488
- // Auto-match schemas
489
- if (this.autoMatch) {
490
- this.autoMatchSchemas(groupIdx)
491
- }
492
- }
493
-
494
- this.$emit('update:configuration', { ...this.localConfig })
495
- },
496
-
497
- /**
498
- * Handle schema selection change for a type.
499
- *
500
- * @param {number} groupIdx Group index
501
- * @param {object} type Type definition
502
- * @param {object|null} option NcSelect option
503
- */
504
- handleSchemaChange(groupIdx, type, option) {
505
- const key = this.schemaConfigKey(type)
506
- const value = option?.value || ''
507
-
508
- this.localConfig = { ...this.localConfig, [key]: value }
509
- this.$emit('update:configuration', { ...this.localConfig })
510
- },
511
-
512
- /**
513
- * Auto-match schema titles to type slugs/labels (case-insensitive).
514
- *
515
- * @param {number} groupIdx Group index
516
- */
517
- autoMatchSchemas(groupIdx) {
518
- const group = this.groups[groupIdx]
519
- const options = this.schemaSelectOptions(groupIdx)
520
-
521
- for (const type of group.types) {
522
- const schemaKey = this.schemaConfigKey(type)
523
- // Skip if already has a value
524
- if (this.localConfig[schemaKey]) continue
525
-
526
- const slug = type.slug.toLowerCase()
527
- const label = type.label.toLowerCase()
528
-
529
- const match = options.find((o) => {
530
- const optLabel = o.label.toLowerCase()
531
- return optLabel === slug || optLabel === label
532
- || optLabel.includes(slug) || slug.includes(optLabel)
533
- })
534
-
535
- if (match) {
536
- this.localConfig = { ...this.localConfig, [schemaKey]: match.value }
537
- }
538
- }
539
- },
540
-
541
- /** Emit save event with current config */
542
- handleSave() {
543
- this.$emit('save', { ...this.localConfig })
544
- },
545
-
546
- /**
547
- * Fetch all registers from OpenRegister API.
548
- */
549
- async loadRegisters() {
550
- this.registersLoading = true
551
- this.registersError = null
552
-
553
- try {
554
- const response = await fetch('/apps/openregister/api/registers?_extend[]=schemas', {
555
- method: 'GET',
556
- headers: buildHeaders(),
557
- })
558
-
559
- if (!response.ok) {
560
- this.registersError = `Failed to fetch registers: ${response.statusText}`
561
- return
562
- }
563
-
564
- const data = await response.json()
565
- const results = data.results || data
566
- this.registers = Array.isArray(results) ? results : []
567
-
568
- // Cache expanded schemas
569
- for (const reg of this.registers) {
570
- if (Array.isArray(reg.schemas) && reg.schemas.length > 0) {
571
- const schemas = reg.schemas.filter((s) => s && typeof s === 'object' && s.id)
572
- if (schemas.length > 0) {
573
- this.schemasByRegister = {
574
- ...this.schemasByRegister,
575
- [String(reg.id)]: schemas,
576
- }
577
- }
578
- }
579
- }
580
- } catch (error) {
581
- this.registersError = error.message || 'Network error fetching registers'
582
- } finally {
583
- this.registersLoading = false
584
- }
585
- },
586
-
587
- /**
588
- * Fetch schemas for a specific register.
589
- *
590
- * @param {string} registerId Register ID
591
- */
592
- async loadSchemasForRegister(registerId) {
593
- const id = String(registerId)
594
-
595
- // Return cached
596
- if (this.schemasByRegister[id]?.length > 0) return
597
-
598
- try {
599
- const response = await fetch(
600
- `/apps/openregister/api/registers/${id}?_extend[]=schemas`,
601
- { method: 'GET', headers: buildHeaders() },
602
- )
603
- if (!response.ok) return
604
-
605
- const data = await response.json()
606
- const schemas = (data.schemas || []).filter((s) => s && typeof s === 'object' && s.id)
607
- this.schemasByRegister = { ...this.schemasByRegister, [id]: schemas }
608
- } catch {
609
- // Silently fail — register already selected, schemas just won't populate
610
- }
611
- },
612
- },
613
- }
614
- </script>
615
-
616
- <style scoped>
617
- .cn-register-mapping__group {
618
- margin-bottom: 24px;
619
- border: 1px solid var(--color-border);
620
- border-radius: var(--border-radius-large);
621
- padding: 20px;
622
- background: var(--color-background-hover);
623
- }
624
-
625
- .cn-register-mapping__group:last-child {
626
- margin-bottom: 0;
627
- }
628
-
629
- .cn-register-mapping__group-header {
630
- display: flex;
631
- align-items: center;
632
- justify-content: space-between;
633
- margin-bottom: 12px;
634
- }
635
-
636
- .cn-register-mapping__group-title {
637
- font-size: 16px;
638
- font-weight: 600;
639
- margin: 0;
640
- color: var(--color-main-text);
641
- }
642
-
643
- .cn-register-mapping__group-status {
644
- font-size: 13px;
645
- color: var(--color-text-maxcontrast);
646
- }
647
-
648
- .cn-register-mapping__group-description {
649
- color: var(--color-text-maxcontrast);
650
- font-size: 13px;
651
- margin: 0 0 12px 0;
652
- }
653
-
654
- .cn-register-mapping__label {
655
- display: block;
656
- font-weight: 500;
657
- font-size: 13px;
658
- color: var(--color-text-maxcontrast);
659
- margin-bottom: 4px;
660
- }
661
-
662
- .cn-register-mapping__register-select {
663
- margin-bottom: 16px;
664
- max-width: 400px;
665
- }
666
-
667
- .cn-register-mapping__type-list {
668
- border: 1px solid var(--color-border);
669
- border-radius: var(--border-radius);
670
- overflow: hidden;
671
- background: var(--color-main-background);
672
- }
673
-
674
- .cn-register-mapping__type-list-header {
675
- display: grid;
676
- grid-template-columns: 1fr 1fr 40px 32px;
677
- align-items: center;
678
- padding: 8px 16px;
679
- font-size: 12px;
680
- font-weight: 600;
681
- color: var(--color-text-maxcontrast);
682
- text-transform: uppercase;
683
- letter-spacing: 0.5px;
684
- border-bottom: 1px solid var(--color-border);
685
- background: var(--color-background-hover);
686
- }
687
-
688
- .cn-register-mapping__type-row {
689
- display: grid;
690
- grid-template-columns: 1fr 1fr 40px 32px;
691
- align-items: center;
692
- padding: 10px 16px;
693
- border-bottom: 1px solid var(--color-border);
694
- cursor: pointer;
695
- transition: background-color 0.15s ease;
696
- }
697
-
698
- .cn-register-mapping__type-row:hover {
699
- background: var(--color-background-hover);
700
- }
701
-
702
- .cn-register-mapping__type-row--expanded {
703
- background: var(--color-background-hover);
704
- }
705
-
706
- .cn-register-mapping__type-name {
707
- font-weight: 500;
708
- color: var(--color-main-text);
709
- }
710
-
711
- .cn-register-mapping__type-schema {
712
- color: var(--color-text-maxcontrast);
713
- font-size: 13px;
714
- }
715
-
716
- .cn-register-mapping__type-status {
717
- display: flex;
718
- justify-content: center;
719
- }
720
-
721
- .cn-register-mapping__type-chevron {
722
- display: flex;
723
- justify-content: center;
724
- color: var(--color-text-maxcontrast);
725
- }
726
-
727
- .cn-register-mapping__status-dot {
728
- display: inline-block;
729
- width: 10px;
730
- height: 10px;
731
- border-radius: 50%;
732
- }
733
-
734
- .cn-register-mapping__status-dot--configured {
735
- background-color: var(--color-success);
736
- }
737
-
738
- .cn-register-mapping__status-dot--unconfigured {
739
- background-color: var(--color-warning);
740
- }
741
-
742
- .cn-register-mapping__type-detail {
743
- padding: 12px 16px 16px;
744
- border-bottom: 1px solid var(--color-border);
745
- background: var(--color-main-background);
746
- }
747
-
748
- .cn-register-mapping__type-description {
749
- color: var(--color-text-maxcontrast);
750
- font-size: 13px;
751
- margin: 0 0 8px 0;
752
- }
753
-
754
- /* Slide transition */
755
- .slide-enter-active,
756
- .slide-leave-active {
757
- transition: all 0.2s ease;
758
- max-height: 200px;
759
- overflow: hidden;
760
- }
761
-
762
- .slide-enter,
763
- .slide-leave-to {
764
- max-height: 0;
765
- padding-top: 0;
766
- padding-bottom: 0;
767
- opacity: 0;
768
- }
769
-
770
- /* Last row in list should not have bottom border */
771
- .cn-register-mapping__type-list > :last-child {
772
- border-bottom: none;
773
- }
774
-
775
- @media (max-width: 768px) {
776
- .cn-register-mapping__type-list-header {
777
- display: none;
778
- }
779
-
780
- .cn-register-mapping__type-row {
781
- grid-template-columns: 1fr auto 32px;
782
- }
783
-
784
- .cn-register-mapping__type-schema {
785
- display: none;
786
- }
787
-
788
- .cn-register-mapping__register-select {
789
- max-width: none;
790
- }
791
- }
792
- </style>
1
+ <template>
2
+ <CnSettingsSection
3
+ :name="name"
4
+ :description="description"
5
+ :doc-url="docUrl"
6
+ :loading="registersLoading"
7
+ loading-message="Loading registers..."
8
+ :error="!!registersError"
9
+ :error-message="registersError || ''"
10
+ :on-retry="loadRegisters">
11
+ <!-- Action buttons -->
12
+ <template #actions>
13
+ <NcButton
14
+ v-if="showSaveButton"
15
+ type="primary"
16
+ :disabled="saving || !hasChanges"
17
+ @click="handleSave">
18
+ <template #icon>
19
+ <NcLoadingIcon v-if="saving" :size="20" />
20
+ <ContentSave v-else :size="20" />
21
+ </template>
22
+ {{ saving ? 'Saving...' : saveButtonText }}
23
+ </NcButton>
24
+ <NcButton
25
+ v-if="showReimportButton"
26
+ type="secondary"
27
+ :disabled="reimporting"
28
+ @click="$emit('reimport')">
29
+ <template #icon>
30
+ <NcLoadingIcon v-if="reimporting" :size="20" />
31
+ <Refresh v-else :size="20" />
32
+ </template>
33
+ {{ reimporting ? 'Importing...' : reimportButtonText }}
34
+ </NcButton>
35
+ <slot name="actions" />
36
+ </template>
37
+
38
+ <!-- Group sections -->
39
+ <div v-if="!registersLoading && !registersError" class="cn-register-mapping">
40
+ <div
41
+ v-for="(group, groupIdx) in groups"
42
+ :key="groupIdx"
43
+ class="cn-register-mapping__group">
44
+ <!-- Group header -->
45
+ <slot name="group-header"
46
+ :group="group"
47
+ :configured-count="configuredCount(groupIdx)"
48
+ :total-count="group.types.length">
49
+ <div class="cn-register-mapping__group-header">
50
+ <h4 class="cn-register-mapping__group-title">
51
+ {{ group.name }}
52
+ </h4>
53
+ <span class="cn-register-mapping__group-status">
54
+ {{ configuredCount(groupIdx) }}/{{ group.types.length }} {{ labels.partiallyConfigured }}
55
+ </span>
56
+ </div>
57
+ </slot>
58
+
59
+ <!-- Group description -->
60
+ <p v-if="group.description" class="cn-register-mapping__group-description">
61
+ {{ group.description }}
62
+ </p>
63
+
64
+ <!-- Register selector -->
65
+ <div class="cn-register-mapping__register-select">
66
+ <label class="cn-register-mapping__label">{{ labels.register }}</label>
67
+ <NcSelect
68
+ :value="selectedRegister(groupIdx)"
69
+ :options="registerSelectOptions"
70
+ :placeholder="labels.selectRegister"
71
+ :loading="registersLoading"
72
+ label="label"
73
+ track-by="value"
74
+ @input="handleRegisterChange(groupIdx, $event)" />
75
+ </div>
76
+
77
+ <!-- Type list -->
78
+ <div v-if="selectedRegister(groupIdx)" class="cn-register-mapping__type-list">
79
+ <!-- Header row -->
80
+ <div class="cn-register-mapping__type-list-header">
81
+ <span>Name</span>
82
+ <span>Schema</span>
83
+ <span />
84
+ <span />
85
+ </div>
86
+
87
+ <!-- Type rows -->
88
+ <template v-for="type in group.types">
89
+ <div
90
+ :key="type.slug + '-row'"
91
+ class="cn-register-mapping__type-row"
92
+ :class="{ 'cn-register-mapping__type-row--expanded': isExpanded(groupIdx, type.slug) }"
93
+ @click="toggleExpand(groupIdx, type.slug)">
94
+ <span class="cn-register-mapping__type-name">{{ type.label }}</span>
95
+ <span class="cn-register-mapping__type-schema">
96
+ {{ schemaLabel(groupIdx, type) || labels.notConfigured }}
97
+ </span>
98
+ <span class="cn-register-mapping__type-status">
99
+ <span
100
+ class="cn-register-mapping__status-dot"
101
+ :class="schemaValue(groupIdx, type)
102
+ ? 'cn-register-mapping__status-dot--configured'
103
+ : 'cn-register-mapping__status-dot--unconfigured'" />
104
+ </span>
105
+ <span class="cn-register-mapping__type-chevron">
106
+ <ChevronUp v-if="isExpanded(groupIdx, type.slug)" :size="20" />
107
+ <ChevronDown v-else :size="20" />
108
+ </span>
109
+ </div>
110
+
111
+ <!-- Expanded detail -->
112
+ <transition :key="type.slug + '-detail'" name="slide">
113
+ <div
114
+ v-if="isExpanded(groupIdx, type.slug)"
115
+ class="cn-register-mapping__type-detail">
116
+ <p v-if="type.description" class="cn-register-mapping__type-description">
117
+ {{ type.description }}
118
+ </p>
119
+ <NcSelect
120
+ :value="selectedSchema(groupIdx, type)"
121
+ :options="schemaSelectOptions(groupIdx)"
122
+ :placeholder="labels.selectSchema"
123
+ label="label"
124
+ track-by="value"
125
+ @input="handleSchemaChange(groupIdx, type, $event)" />
126
+ </div>
127
+ </transition>
128
+ </template>
129
+ </div>
130
+
131
+ <!-- No register selected -->
132
+ <NcNoteCard v-else-if="!registersLoading" type="info">
133
+ {{ labels.selectRegister }}
134
+ </NcNoteCard>
135
+
136
+ <!-- No schemas available -->
137
+ <NcNoteCard
138
+ v-if="selectedRegister(groupIdx) && schemaSelectOptions(groupIdx).length === 0 && !registersLoading"
139
+ type="warning">
140
+ {{ labels.noSchemas }}
141
+ </NcNoteCard>
142
+ </div>
143
+ </div>
144
+
145
+ <!-- Footer -->
146
+ <template v-if="$slots.footer" #footer>
147
+ <slot name="footer" />
148
+ </template>
149
+ </CnSettingsSection>
150
+ </template>
151
+
152
+ <script>
153
+ import { CnSettingsSection } from '../CnSettingsSection/index.js'
154
+ import { NcButton, NcLoadingIcon, NcNoteCard, NcSelect } from '@nextcloud/vue'
155
+ import ContentSave from 'vue-material-design-icons/ContentSave.vue'
156
+ import Refresh from 'vue-material-design-icons/Refresh.vue'
157
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
158
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
159
+ import { buildHeaders } from '../../utils/headers.js'
160
+
161
+ /**
162
+ * CnRegisterMapping - OpenRegister register/schema configuration component.
163
+ *
164
+ * Displays and manages register-to-schema mappings for app object types.
165
+ * Self-fetches available registers and schemas from the OpenRegister API.
166
+ * Supports multiple register groups (stacked sections) with expandable
167
+ * type rows for manual schema override.
168
+ *
169
+ * @example Single register (Pipelinq)
170
+ * <CnRegisterMapping
171
+ * name="Register Configuration"
172
+ * :groups="[{
173
+ * name: 'Core Objects',
174
+ * types: [
175
+ * { slug: 'client', label: 'Client' },
176
+ * { slug: 'contact', label: 'Contact' },
177
+ * ],
178
+ * }]"
179
+ * :configuration="config"
180
+ * :show-reimport-button="true"
181
+ * @save="saveConfig"
182
+ * @reimport="reimport" />
183
+ *
184
+ * @example Multi-register (SoftwareCatalog)
185
+ * <CnRegisterMapping
186
+ * :groups="[
187
+ * { name: 'Voorzieningen', registerConfigKey: 'voorzieningen_register', types: [...] },
188
+ * { name: 'AMEF', registerConfigKey: 'amef_register', types: [...] },
189
+ * ]"
190
+ * :configuration="config"
191
+ * @save="saveConfig" />
192
+ */
193
+ export default {
194
+ name: 'CnRegisterMapping',
195
+
196
+ components: {
197
+ CnSettingsSection,
198
+ NcButton,
199
+ NcLoadingIcon,
200
+ NcNoteCard,
201
+ NcSelect,
202
+ ContentSave,
203
+ Refresh,
204
+ ChevronDown,
205
+ ChevronUp,
206
+ },
207
+
208
+ props: {
209
+ /** Section title */
210
+ name: {
211
+ type: String,
212
+ default: 'Register Configuration',
213
+ },
214
+ /** Section description */
215
+ description: {
216
+ type: String,
217
+ default: 'Configure OpenRegister schema mappings for your object types',
218
+ },
219
+ /** Documentation URL */
220
+ docUrl: {
221
+ type: String,
222
+ default: '',
223
+ },
224
+ /**
225
+ * Groups of object types that share a register.
226
+ * @type {Array<{ name: string, description?: string, registerConfigKey?: string, types: Array<{ slug: string, label: string, description?: string, configKey?: string }> }>}
227
+ */
228
+ groups: {
229
+ type: Array,
230
+ required: true,
231
+ validator: (groups) => groups.length > 0
232
+ && groups.every((g) => g.name && Array.isArray(g.types) && g.types.length > 0),
233
+ },
234
+ /** Current configuration values: { register: '5', client_schema: '28', ... } */
235
+ configuration: {
236
+ type: Object,
237
+ default: () => ({}),
238
+ },
239
+ /** Show save button */
240
+ showSaveButton: {
241
+ type: Boolean,
242
+ default: true,
243
+ },
244
+ /** Whether save is in progress */
245
+ saving: {
246
+ type: Boolean,
247
+ default: false,
248
+ },
249
+ /** Show reimport button */
250
+ showReimportButton: {
251
+ type: Boolean,
252
+ default: false,
253
+ },
254
+ /** Whether reimport is in progress */
255
+ reimporting: {
256
+ type: Boolean,
257
+ default: false,
258
+ },
259
+ /** Save button text */
260
+ saveButtonText: {
261
+ type: String,
262
+ default: 'Save Configuration',
263
+ },
264
+ /** Reimport button text */
265
+ reimportButtonText: {
266
+ type: String,
267
+ default: 'Re-import configuration',
268
+ },
269
+ /** Auto-match schema titles to type slugs on register change */
270
+ autoMatch: {
271
+ type: Boolean,
272
+ default: true,
273
+ },
274
+ /** UI labels (i18n) */
275
+ labels: {
276
+ type: Object,
277
+ default: () => ({
278
+ register: 'Register',
279
+ schema: 'Schema',
280
+ configured: 'Configured',
281
+ notConfigured: 'Not configured',
282
+ noSchemas: 'No schemas available in this register',
283
+ selectRegister: 'Select a register',
284
+ selectSchema: 'Select a schema',
285
+ allConfigured: 'All types configured',
286
+ partiallyConfigured: 'configured',
287
+ }),
288
+ },
289
+ },
290
+
291
+ emits: ['update:configuration', 'save', 'reimport'],
292
+
293
+ data() {
294
+ return {
295
+ // Fetched data
296
+ registers: [],
297
+ schemasByRegister: {},
298
+ registersLoading: false,
299
+ registersError: null,
300
+ // Local state
301
+ localConfig: {},
302
+ expandedRows: {},
303
+ }
304
+ },
305
+
306
+ computed: {
307
+ /** Register options for NcSelect */
308
+ registerSelectOptions() {
309
+ return this.registers.map((r) => ({
310
+ label: r.title || r.slug || `Register ${r.id}`,
311
+ value: String(r.id),
312
+ }))
313
+ },
314
+
315
+ /** Whether local config differs from prop */
316
+ hasChanges() {
317
+ return JSON.stringify(this.localConfig) !== JSON.stringify(this.configuration)
318
+ },
319
+ },
320
+
321
+ watch: {
322
+ configuration: {
323
+ handler(newVal) {
324
+ this.localConfig = { ...newVal }
325
+ },
326
+ immediate: true,
327
+ deep: true,
328
+ },
329
+ },
330
+
331
+ async mounted() {
332
+ await this.loadRegisters()
333
+ },
334
+
335
+ methods: {
336
+ /**
337
+ * Get the config key for a group's register.
338
+ *
339
+ * @param {number} groupIdx Group index
340
+ * @return {string} Config key
341
+ */
342
+ registerConfigKey(groupIdx) {
343
+ const group = this.groups[groupIdx]
344
+ if (group.registerConfigKey) return group.registerConfigKey
345
+ if (this.groups.length === 1) return 'register'
346
+ return group.name.toLowerCase().replace(/[^a-z0-9]+/g, '_') + '_register'
347
+ },
348
+
349
+ /**
350
+ * Get the config key for a type's schema.
351
+ *
352
+ * @param {object} type Type definition
353
+ * @return {string} Config key
354
+ */
355
+ schemaConfigKey(type) {
356
+ return type.configKey || type.slug + '_schema'
357
+ },
358
+
359
+ /**
360
+ * Get the selected register option for a group.
361
+ *
362
+ * @param {number} groupIdx Group index
363
+ * @return {object|null} NcSelect option
364
+ */
365
+ selectedRegister(groupIdx) {
366
+ const key = this.registerConfigKey(groupIdx)
367
+ const value = String(this.localConfig[key] || '')
368
+ if (!value) return null
369
+ return this.registerSelectOptions.find((o) => o.value === value) || null
370
+ },
371
+
372
+ /**
373
+ * Get the selected schema option for a type.
374
+ *
375
+ * @param {number} groupIdx Group index
376
+ * @param {object} type Type definition
377
+ * @return {object|null} NcSelect option
378
+ */
379
+ selectedSchema(groupIdx, type) {
380
+ const key = this.schemaConfigKey(type)
381
+ const value = String(this.localConfig[key] || '')
382
+ if (!value) return null
383
+ const options = this.schemaSelectOptions(groupIdx)
384
+ return options.find((o) => o.value === value) || null
385
+ },
386
+
387
+ /**
388
+ * Get the schema value (ID) for a type.
389
+ *
390
+ * @param {number} groupIdx Group index
391
+ * @param {object} type Type definition
392
+ * @return {string} Schema ID or empty string
393
+ */
394
+ schemaValue(groupIdx, type) {
395
+ const key = this.schemaConfigKey(type)
396
+ return this.localConfig[key] || ''
397
+ },
398
+
399
+ /**
400
+ * Get the display label for a type's current schema.
401
+ *
402
+ * @param {number} groupIdx Group index
403
+ * @param {object} type Type definition
404
+ * @return {string} Schema label or empty string
405
+ */
406
+ schemaLabel(groupIdx, type) {
407
+ const selected = this.selectedSchema(groupIdx, type)
408
+ if (selected) return selected.label
409
+ const value = this.schemaValue(groupIdx, type)
410
+ return value ? `Schema ${value}` : ''
411
+ },
412
+
413
+ /**
414
+ * Get schema options for a group's selected register.
415
+ *
416
+ * @param {number} groupIdx Group index
417
+ * @return {Array<{label: string, value: string}>} NcSelect options
418
+ */
419
+ schemaSelectOptions(groupIdx) {
420
+ const reg = this.selectedRegister(groupIdx)
421
+ if (!reg) return []
422
+ const schemas = this.schemasByRegister[reg.value] || []
423
+ return schemas.map((s) => ({
424
+ label: s.title || s.slug || `Schema ${s.id}`,
425
+ value: String(s.id),
426
+ }))
427
+ },
428
+
429
+ /**
430
+ * Count configured types in a group.
431
+ *
432
+ * @param {number} groupIdx Group index
433
+ * @return {number} Count
434
+ */
435
+ configuredCount(groupIdx) {
436
+ const group = this.groups[groupIdx]
437
+ return group.types.filter((type) => !!this.schemaValue(groupIdx, type)).length
438
+ },
439
+
440
+ /**
441
+ * Check if a type row is expanded.
442
+ *
443
+ * @param {number} groupIdx Group index
444
+ * @param {string} slug Type slug
445
+ * @return {boolean}
446
+ */
447
+ isExpanded(groupIdx, slug) {
448
+ return !!this.expandedRows[groupIdx + '-' + slug]
449
+ },
450
+
451
+ /**
452
+ * Toggle a type row expansion.
453
+ *
454
+ * @param {number} groupIdx Group index
455
+ * @param {string} slug Type slug
456
+ */
457
+ toggleExpand(groupIdx, slug) {
458
+ const key = groupIdx + '-' + slug
459
+ this.expandedRows = {
460
+ ...this.expandedRows,
461
+ [key]: !this.expandedRows[key],
462
+ }
463
+ },
464
+
465
+ /**
466
+ * Handle register selection change for a group.
467
+ *
468
+ * @param {number} groupIdx Group index
469
+ * @param {object|null} option NcSelect option
470
+ */
471
+ async handleRegisterChange(groupIdx, option) {
472
+ const key = this.registerConfigKey(groupIdx)
473
+ const value = option?.value || ''
474
+
475
+ this.localConfig = { ...this.localConfig, [key]: value }
476
+
477
+ // Clear schema selections for this group
478
+ const group = this.groups[groupIdx]
479
+ for (const type of group.types) {
480
+ const schemaKey = this.schemaConfigKey(type)
481
+ this.localConfig = { ...this.localConfig, [schemaKey]: '' }
482
+ }
483
+
484
+ // Fetch schemas for the new register
485
+ if (value) {
486
+ await this.loadSchemasForRegister(value)
487
+
488
+ // Auto-match schemas
489
+ if (this.autoMatch) {
490
+ this.autoMatchSchemas(groupIdx)
491
+ }
492
+ }
493
+
494
+ this.$emit('update:configuration', { ...this.localConfig })
495
+ },
496
+
497
+ /**
498
+ * Handle schema selection change for a type.
499
+ *
500
+ * @param {number} groupIdx Group index
501
+ * @param {object} type Type definition
502
+ * @param {object|null} option NcSelect option
503
+ */
504
+ handleSchemaChange(groupIdx, type, option) {
505
+ const key = this.schemaConfigKey(type)
506
+ const value = option?.value || ''
507
+
508
+ this.localConfig = { ...this.localConfig, [key]: value }
509
+ this.$emit('update:configuration', { ...this.localConfig })
510
+ },
511
+
512
+ /**
513
+ * Auto-match schema titles to type slugs/labels (case-insensitive).
514
+ *
515
+ * @param {number} groupIdx Group index
516
+ */
517
+ autoMatchSchemas(groupIdx) {
518
+ const group = this.groups[groupIdx]
519
+ const options = this.schemaSelectOptions(groupIdx)
520
+
521
+ for (const type of group.types) {
522
+ const schemaKey = this.schemaConfigKey(type)
523
+ // Skip if already has a value
524
+ if (this.localConfig[schemaKey]) continue
525
+
526
+ const slug = type.slug.toLowerCase()
527
+ const label = type.label.toLowerCase()
528
+
529
+ const match = options.find((o) => {
530
+ const optLabel = o.label.toLowerCase()
531
+ return optLabel === slug || optLabel === label
532
+ || optLabel.includes(slug) || slug.includes(optLabel)
533
+ })
534
+
535
+ if (match) {
536
+ this.localConfig = { ...this.localConfig, [schemaKey]: match.value }
537
+ }
538
+ }
539
+ },
540
+
541
+ /** Emit save event with current config */
542
+ handleSave() {
543
+ this.$emit('save', { ...this.localConfig })
544
+ },
545
+
546
+ /**
547
+ * Fetch all registers from OpenRegister API.
548
+ */
549
+ async loadRegisters() {
550
+ this.registersLoading = true
551
+ this.registersError = null
552
+
553
+ try {
554
+ const response = await fetch('/apps/openregister/api/registers?_extend[]=schemas', {
555
+ method: 'GET',
556
+ headers: buildHeaders(),
557
+ })
558
+
559
+ if (!response.ok) {
560
+ this.registersError = `Failed to fetch registers: ${response.statusText}`
561
+ return
562
+ }
563
+
564
+ const data = await response.json()
565
+ const results = data.results || data
566
+ this.registers = Array.isArray(results) ? results : []
567
+
568
+ // Cache expanded schemas
569
+ for (const reg of this.registers) {
570
+ if (Array.isArray(reg.schemas) && reg.schemas.length > 0) {
571
+ const schemas = reg.schemas.filter((s) => s && typeof s === 'object' && s.id)
572
+ if (schemas.length > 0) {
573
+ this.schemasByRegister = {
574
+ ...this.schemasByRegister,
575
+ [String(reg.id)]: schemas,
576
+ }
577
+ }
578
+ }
579
+ }
580
+ } catch (error) {
581
+ this.registersError = error.message || 'Network error fetching registers'
582
+ } finally {
583
+ this.registersLoading = false
584
+ }
585
+ },
586
+
587
+ /**
588
+ * Fetch schemas for a specific register.
589
+ *
590
+ * @param {string} registerId Register ID
591
+ */
592
+ async loadSchemasForRegister(registerId) {
593
+ const id = String(registerId)
594
+
595
+ // Return cached
596
+ if (this.schemasByRegister[id]?.length > 0) return
597
+
598
+ try {
599
+ const response = await fetch(
600
+ `/apps/openregister/api/registers/${id}?_extend[]=schemas`,
601
+ { method: 'GET', headers: buildHeaders() },
602
+ )
603
+ if (!response.ok) return
604
+
605
+ const data = await response.json()
606
+ const schemas = (data.schemas || []).filter((s) => s && typeof s === 'object' && s.id)
607
+ this.schemasByRegister = { ...this.schemasByRegister, [id]: schemas }
608
+ } catch {
609
+ // Silently fail — register already selected, schemas just won't populate
610
+ }
611
+ },
612
+ },
613
+ }
614
+ </script>
615
+
616
+ <style scoped>
617
+ .cn-register-mapping__group {
618
+ margin-bottom: 24px;
619
+ border: 1px solid var(--color-border);
620
+ border-radius: var(--border-radius-large);
621
+ padding: 20px;
622
+ background: var(--color-background-hover);
623
+ }
624
+
625
+ .cn-register-mapping__group:last-child {
626
+ margin-bottom: 0;
627
+ }
628
+
629
+ .cn-register-mapping__group-header {
630
+ display: flex;
631
+ align-items: center;
632
+ justify-content: space-between;
633
+ margin-bottom: 12px;
634
+ }
635
+
636
+ .cn-register-mapping__group-title {
637
+ font-size: 16px;
638
+ font-weight: 600;
639
+ margin: 0;
640
+ color: var(--color-main-text);
641
+ }
642
+
643
+ .cn-register-mapping__group-status {
644
+ font-size: 13px;
645
+ color: var(--color-text-maxcontrast);
646
+ }
647
+
648
+ .cn-register-mapping__group-description {
649
+ color: var(--color-text-maxcontrast);
650
+ font-size: 13px;
651
+ margin: 0 0 12px 0;
652
+ }
653
+
654
+ .cn-register-mapping__label {
655
+ display: block;
656
+ font-weight: 500;
657
+ font-size: 13px;
658
+ color: var(--color-text-maxcontrast);
659
+ margin-bottom: 4px;
660
+ }
661
+
662
+ .cn-register-mapping__register-select {
663
+ margin-bottom: 16px;
664
+ max-width: 400px;
665
+ }
666
+
667
+ .cn-register-mapping__type-list {
668
+ border: 1px solid var(--color-border);
669
+ border-radius: var(--border-radius);
670
+ overflow: hidden;
671
+ background: var(--color-main-background);
672
+ }
673
+
674
+ .cn-register-mapping__type-list-header {
675
+ display: grid;
676
+ grid-template-columns: 1fr 1fr 40px 32px;
677
+ align-items: center;
678
+ padding: 8px 16px;
679
+ font-size: 12px;
680
+ font-weight: 600;
681
+ color: var(--color-text-maxcontrast);
682
+ text-transform: uppercase;
683
+ letter-spacing: 0.5px;
684
+ border-bottom: 1px solid var(--color-border);
685
+ background: var(--color-background-hover);
686
+ }
687
+
688
+ .cn-register-mapping__type-row {
689
+ display: grid;
690
+ grid-template-columns: 1fr 1fr 40px 32px;
691
+ align-items: center;
692
+ padding: 10px 16px;
693
+ border-bottom: 1px solid var(--color-border);
694
+ cursor: pointer;
695
+ transition: background-color 0.15s ease;
696
+ }
697
+
698
+ .cn-register-mapping__type-row:hover {
699
+ background: var(--color-background-hover);
700
+ }
701
+
702
+ .cn-register-mapping__type-row--expanded {
703
+ background: var(--color-background-hover);
704
+ }
705
+
706
+ .cn-register-mapping__type-name {
707
+ font-weight: 500;
708
+ color: var(--color-main-text);
709
+ }
710
+
711
+ .cn-register-mapping__type-schema {
712
+ color: var(--color-text-maxcontrast);
713
+ font-size: 13px;
714
+ }
715
+
716
+ .cn-register-mapping__type-status {
717
+ display: flex;
718
+ justify-content: center;
719
+ }
720
+
721
+ .cn-register-mapping__type-chevron {
722
+ display: flex;
723
+ justify-content: center;
724
+ color: var(--color-text-maxcontrast);
725
+ }
726
+
727
+ .cn-register-mapping__status-dot {
728
+ display: inline-block;
729
+ width: 10px;
730
+ height: 10px;
731
+ border-radius: 50%;
732
+ }
733
+
734
+ .cn-register-mapping__status-dot--configured {
735
+ background-color: var(--color-success);
736
+ }
737
+
738
+ .cn-register-mapping__status-dot--unconfigured {
739
+ background-color: var(--color-warning);
740
+ }
741
+
742
+ .cn-register-mapping__type-detail {
743
+ padding: 12px 16px 16px;
744
+ border-bottom: 1px solid var(--color-border);
745
+ background: var(--color-main-background);
746
+ }
747
+
748
+ .cn-register-mapping__type-description {
749
+ color: var(--color-text-maxcontrast);
750
+ font-size: 13px;
751
+ margin: 0 0 8px 0;
752
+ }
753
+
754
+ /* Slide transition */
755
+ .slide-enter-active,
756
+ .slide-leave-active {
757
+ transition: all 0.2s ease;
758
+ max-height: 200px;
759
+ overflow: hidden;
760
+ }
761
+
762
+ .slide-enter,
763
+ .slide-leave-to {
764
+ max-height: 0;
765
+ padding-top: 0;
766
+ padding-bottom: 0;
767
+ opacity: 0;
768
+ }
769
+
770
+ /* Last row in list should not have bottom border */
771
+ .cn-register-mapping__type-list > :last-child {
772
+ border-bottom: none;
773
+ }
774
+
775
+ @media (max-width: 768px) {
776
+ .cn-register-mapping__type-list-header {
777
+ display: none;
778
+ }
779
+
780
+ .cn-register-mapping__type-row {
781
+ grid-template-columns: 1fr auto 32px;
782
+ }
783
+
784
+ .cn-register-mapping__type-schema {
785
+ display: none;
786
+ }
787
+
788
+ .cn-register-mapping__register-select {
789
+ max-width: none;
790
+ }
791
+ }
792
+ </style>