@conduction/nextcloud-vue 0.1.0-beta.11 → 0.1.0-beta.12

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