@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -0
- package/dist/nextcloud-vue.cjs.js +7039 -2409
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +237 -52
- package/dist/nextcloud-vue.esm.js +7012 -2386
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +2 -4
- package/src/components/CnActionsBar/CnActionsBar.vue +225 -0
- package/src/components/CnActionsBar/index.js +1 -0
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -0
- package/src/components/CnCopyDialog/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +0 -5
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -0
- package/src/components/CnDeleteDialog/index.js +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +629 -0
- package/src/components/CnFormDialog/index.js +1 -0
- package/src/components/CnIcon/CnIcon.vue +89 -0
- package/src/components/CnIcon/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +434 -300
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +484 -0
- package/src/components/CnIndexSidebar/index.js +1 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -0
- package/src/components/CnPageHeader/index.js +1 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
- package/src/components/CnRegisterMapping/index.js +1 -0
- package/src/components/index.js +8 -4
- package/src/constants/metadata.js +30 -0
- package/src/css/actions-bar.css +48 -0
- package/src/css/badge.css +4 -4
- package/src/css/card.css +23 -23
- package/src/css/detail.css +13 -13
- package/src/css/index-page.css +32 -0
- package/src/css/index-sidebar.css +187 -0
- package/src/css/index.css +4 -0
- package/src/css/layout.css +14 -14
- package/src/css/page-header.css +33 -0
- package/src/css/pagination.css +12 -12
- package/src/css/table.css +21 -22
- package/src/css/utilities.css +2 -2
- package/src/index.js +11 -8
- package/src/store/plugins/index.js +1 -0
- package/src/store/plugins/registerMapping.js +185 -0
- package/src/store/useObjectStore.js +122 -61
- package/src/utils/headers.js +7 -1
- package/src/utils/index.js +1 -1
- package/src/utils/schema.js +133 -1
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
- package/src/components/CnDetailViewLayout/index.js +0 -1
- package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
- package/src/components/CnEmptyState/index.js +0 -1
- package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
- package/src/components/CnListViewLayout/index.js +0 -1
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
- package/src/components/CnViewModeToggle/index.js +0 -1
|
@@ -0,0 +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>
|