@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,787 +1,787 @@
1
- <template>
2
- <CnTabbedFormDialog
3
- ref="dialog"
4
- :tabs="dialogTabs"
5
- :item="item"
6
- :dialog-title="dialogTitle"
7
- entity-name="Schema"
8
- :size="size"
9
- :disable-save="!schemaItem.title"
10
- :success-text="resolvedSuccessText"
11
- :cancel-label="cancelLabel"
12
- :close-label="closeLabel"
13
- :confirm-label="confirmLabel"
14
- @confirm="handleConfirm"
15
- @close="$emit('close')">
16
- <!-- Metadata Display -->
17
- <template #above-tabs="{ loading: dialogLoading }">
18
- <div class="cn-schema-form__detail-grid">
19
- <div v-if="schemaItem.id"
20
- class="cn-schema-form__detail-item cn-schema-form__id-card">
21
- <div class="cn-schema-form__id-card-header">
22
- <span class="cn-schema-form__detail-label">ID / UUID:</span>
23
- <NcButton class="cn-schema-form__copy-button"
24
- @click="copyToClipboard(schemaItem.uuid || schemaItem.id)">
25
- <template #icon>
26
- <Check v-if="isCopied" :size="20" />
27
- <ContentCopy v-else :size="20" />
28
- </template>
29
- {{ isCopied ? 'Copied' : 'Copy' }}
30
- </NcButton>
31
- </div>
32
- <span class="cn-schema-form__detail-value">{{ schemaItem.id }}</span>
33
- <span v-if="schemaItem.uuid && schemaItem.uuid !== schemaItem.id"
34
- class="cn-schema-form__detail-value cn-schema-form__uuid-value">{{ schemaItem.uuid }}</span>
35
- </div>
36
- <div class="cn-schema-form__detail-item cn-schema-form__title-with-badge">
37
- <NcTextField :disabled="dialogLoading"
38
- label="Title *"
39
- :value.sync="schemaItem.title" />
40
- <span v-if="schemaItem.allOf && schemaItem.allOf.length > 0"
41
- class="cn-schema-form__statusPill cn-schema-form__statusPill--success">
42
- allOf
43
- </span>
44
- <span v-if="schemaItem.oneOf && schemaItem.oneOf.length > 0"
45
- class="cn-schema-form__statusPill cn-schema-form__statusPill--info">
46
- oneOf
47
- </span>
48
- <span v-if="schemaItem.anyOf && schemaItem.anyOf.length > 0"
49
- class="cn-schema-form__statusPill cn-schema-form__statusPill--info">
50
- anyOf
51
- </span>
52
- </div>
53
- <div v-if="schemaItem.created" class="cn-schema-form__detail-item">
54
- <span class="cn-schema-form__detail-label">Created:</span>
55
- <span class="cn-schema-form__detail-value">{{ new Date(schemaItem.created).toLocaleString() }}</span>
56
- </div>
57
- <div v-if="schemaItem.updated" class="cn-schema-form__detail-item">
58
- <span class="cn-schema-form__detail-label">Updated:</span>
59
- <span class="cn-schema-form__detail-value">{{ new Date(schemaItem.updated).toLocaleString() }}</span>
60
- </div>
61
- <div class="cn-schema-form__detail-item">
62
- <span class="cn-schema-form__detail-label">Version:</span>
63
- <span class="cn-schema-form__detail-value">{{ schemaItem.version || 'Not set' }}</span>
64
- </div>
65
- <div class="cn-schema-form__detail-item">
66
- <span class="cn-schema-form__detail-label">Owner:</span>
67
- <span class="cn-schema-form__detail-value">{{ schemaItem.owner || 'Not set' }}</span>
68
- </div>
69
- </div>
70
- </template>
71
-
72
- <!-- Properties Tab -->
73
- <template #tab-properties="{ loading: dialogLoading }">
74
- <CnSchemaPropertiesTab
75
- :schema-item="schemaItem"
76
- :loading="dialogLoading"
77
- :selected-property="selectedProperty"
78
- :properties-modified="propertiesModified"
79
- :original-properties="originalProperties"
80
- :type-options-for-select="typeOptionsForSelect"
81
- :available-schemas="availableSchemas"
82
- :available-registers="availableRegisters"
83
- :available-tags-options="availableTagsOptions"
84
- :user-groups="userGroups"
85
- :sorted-user-groups="sortedUserGroups"
86
- :loading-groups="loadingGroups"
87
- @add-property="addProperty"
88
- @update:selected-property="selectedProperty = $event"
89
- @update:property-key="updatePropertyKey($event.oldKey, $event.newKey)"
90
- @copy-property="copyProperty"
91
- @delete-property="deleteProperty" />
92
- </template>
93
-
94
- <!-- Configuration Tab -->
95
- <template #tab-configuration="{ loading: dialogLoading }">
96
- <CnSchemaConfigurationTab
97
- :schema-item="schemaItem"
98
- :loading="dialogLoading"
99
- :available-schemas="availableSchemas"
100
- :property-options="propertyOptions"
101
- :all-of-schema-names="allOfSchemaNames" />
102
- </template>
103
-
104
- <!-- Security Tab -->
105
- <template #tab-security>
106
- <CnSchemaSecurityTab
107
- :schema-item="schemaItem"
108
- :user-groups="userGroups"
109
- :sorted-user-groups="sortedUserGroups"
110
- :loading-groups="loadingGroups"
111
- :has-any-permissions="hasAnyPermissions"
112
- :is-restrictive-schema="isRestrictiveSchema" />
113
- </template>
114
-
115
- <!-- Optional Action Buttons (edit mode only) -->
116
- <template #actions-right="{ loading: dialogLoading, isCreateMode, result: dialogResult }">
117
- <template v-if="!isCreateMode && dialogResult === null">
118
- <NcButton
119
- v-if="showExtendSchema"
120
- :disabled="dialogLoading"
121
- @click="$emit('extend-schema')">
122
- <template #icon>
123
- <CallSplit :size="20" />
124
- </template>
125
- {{ extendSchemaLabel }}
126
- </NcButton>
127
- <NcButton
128
- v-if="showAnalyzeProperties"
129
- :disabled="dialogLoading"
130
- @click="$emit('analyze-properties')">
131
- <template #icon>
132
- <DatabaseSearch :size="20" />
133
- </template>
134
- {{ analyzePropertiesLabel }}
135
- </NcButton>
136
- <NcButton
137
- v-if="showValidateObjects"
138
- :disabled="dialogLoading"
139
- @click="$emit('validate-objects')">
140
- <template #icon>
141
- <CheckCircle :size="20" />
142
- </template>
143
- {{ validateObjectsLabel }}
144
- </NcButton>
145
- <NcButton
146
- v-if="showDeleteObjects"
147
- v-tooltip="objectCount > 0 ? deleteObjectsTooltip : noDeleteObjectsTooltip"
148
- :disabled="dialogLoading || objectCount === 0"
149
- @click="$emit('delete-objects')">
150
- <template #icon>
151
- <DeleteSweep :size="20" />
152
- </template>
153
- {{ deleteObjectsLabel }}
154
- </NcButton>
155
- <NcButton
156
- v-if="showPublishObjects"
157
- v-tooltip="objectCount > 0 ? publishObjectsTooltip : noPublishObjectsTooltip"
158
- :disabled="dialogLoading || objectCount === 0"
159
- @click="$emit('publish-objects')">
160
- <template #icon>
161
- <Upload :size="20" />
162
- </template>
163
- {{ publishObjectsLabel }}
164
- </NcButton>
165
- <NcButton
166
- v-if="showDelete"
167
- v-tooltip="objectCount > 0 ? cannotDeleteTooltip : ''"
168
- :disabled="dialogLoading || objectCount > 0"
169
- type="error"
170
- @click="$emit('delete-schema')">
171
- <template #icon>
172
- <TrashCanOutline :size="20" />
173
- </template>
174
- {{ deleteLabel }}
175
- </NcButton>
176
- </template>
177
- </template>
178
- </CnTabbedFormDialog>
179
- </template>
180
-
181
- <script>
182
- import {
183
- NcButton,
184
- NcTextField,
185
- } from '@nextcloud/vue'
186
-
187
- import CnTabbedFormDialog from '../CnTabbedFormDialog/CnTabbedFormDialog.vue'
188
- import CnSchemaPropertiesTab from './CnSchemaPropertiesTab.vue'
189
- import CnSchemaConfigurationTab from './CnSchemaConfigurationTab.vue'
190
- import CnSchemaSecurityTab from './CnSchemaSecurityTab.vue'
191
-
192
- import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
193
- import Check from 'vue-material-design-icons/Check.vue'
194
- import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
195
- import CallSplit from 'vue-material-design-icons/CallSplit.vue'
196
- import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
197
- import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
198
- import DeleteSweep from 'vue-material-design-icons/DeleteSweep.vue'
199
- import Upload from 'vue-material-design-icons/Upload.vue'
200
-
201
- /**
202
- * CnSchemaFormDialog — Generic JSON Schema editor dialog.
203
- *
204
- * Provides a full-featured form for creating and editing JSON Schemas with
205
- * properties table, configuration, and security (RBAC) tabs. Uses CnTabbedFormDialog.
206
- *
207
- * The dialog does NOT perform saves — it emits a `confirm` event with the schema data.
208
- * The parent performs the actual API call and calls `setResult()` via a ref.
209
- *
210
- * @event confirm Emitted when the user confirms. Payload: cleaned schema data object.
211
- * @event close Emitted when the dialog should be closed.
212
- * @event extend-schema Emitted when the Extend Schema button is clicked.
213
- * @event analyze-properties Emitted when the Analyze Properties button is clicked.
214
- * @event validate-objects Emitted when the Validate Objects button is clicked.
215
- * @event delete-objects Emitted when the Delete Objects button is clicked.
216
- * @event publish-objects Emitted when the Publish Objects button is clicked.
217
- * @event delete-schema Emitted when the Delete button is clicked.
218
- */
219
- export default {
220
- name: 'CnSchemaFormDialog',
221
- components: {
222
- NcTextField,
223
- NcButton,
224
- CnTabbedFormDialog,
225
- CnSchemaPropertiesTab,
226
- CnSchemaConfigurationTab,
227
- CnSchemaSecurityTab,
228
- // Icons
229
- ContentCopy,
230
- Check,
231
- TrashCanOutline,
232
- CallSplit,
233
- DatabaseSearch,
234
- CheckCircle,
235
- DeleteSweep,
236
- Upload,
237
- },
238
- props: {
239
- /** Existing schema item for edit mode. Pass null for create mode. */
240
- item: { type: Object, default: null },
241
- /** Dialog title. Defaults to "Create Schema" or "Edit Schema". */
242
- dialogTitle: { type: String, default: '' },
243
- /** NcDialog size */
244
- size: { type: String, default: 'large' },
245
- /** Available schemas for references and composition. Array of { id, title, description, reference } */
246
- availableSchemas: { type: Array, default: () => [] },
247
- /** Available registers. Array of { id, label } */
248
- availableRegisters: { type: Array, default: () => [] },
249
- /** User groups for RBAC. Array of { id, displayname } */
250
- userGroups: { type: Array, default: () => [] },
251
- /** Available tags for file property configuration */
252
- availableTags: { type: Array, default: () => [] },
253
- /** Whether user groups are still loading */
254
- loadingGroups: { type: Boolean, default: false },
255
- /** Number of objects attached to this schema (used for action button disable logic) */
256
- objectCount: { type: Number, default: 0 },
257
- // Optional action button visibility
258
- /** Show "Extend Schema" button */
259
- showExtendSchema: { type: Boolean, default: false },
260
- /** Show "Analyze Properties" button */
261
- showAnalyzeProperties: { type: Boolean, default: false },
262
- /** Show "Validate Objects" button */
263
- showValidateObjects: { type: Boolean, default: false },
264
- /** Show "Delete Objects" button */
265
- showDeleteObjects: { type: Boolean, default: false },
266
- /** Show "Publish Objects" button */
267
- showPublishObjects: { type: Boolean, default: false },
268
- /** Show "Delete" button */
269
- showDelete: { type: Boolean, default: false },
270
- // Labels (pre-translated strings with English defaults)
271
- cancelLabel: { type: String, default: 'Cancel' },
272
- closeLabel: { type: String, default: 'Close' },
273
- /** Confirm button label. Defaults to "Create" or "Save". */
274
- confirmLabel: { type: String, default: '' },
275
- /** Success message. Defaults to "Schema saved successfully." */
276
- successText: { type: String, default: '' },
277
- extendSchemaLabel: { type: String, default: 'Extend Schema' },
278
- analyzePropertiesLabel: { type: String, default: 'Analyze Properties' },
279
- validateObjectsLabel: { type: String, default: 'Validate Objects' },
280
- deleteObjectsLabel: { type: String, default: 'Delete Objects' },
281
- publishObjectsLabel: { type: String, default: 'Publish Objects' },
282
- deleteLabel: { type: String, default: 'Delete' },
283
- deleteObjectsTooltip: { type: String, default: 'Delete all objects in this schema' },
284
- publishObjectsTooltip: { type: String, default: 'Publish all objects in this schema' },
285
- /** Tooltip for the Delete Objects button when no objects exist */
286
- noDeleteObjectsTooltip: { type: String, default: 'No objects to delete' },
287
- /** Tooltip for the Publish Objects button when no objects exist */
288
- noPublishObjectsTooltip: { type: String, default: 'No objects to publish' },
289
- cannotDeleteTooltip: { type: String, default: 'Cannot delete: objects are still attached' },
290
- },
291
- data() {
292
- return {
293
- isCopied: false,
294
- selectedProperty: null,
295
- propertiesModified: false,
296
- originalProperties: null,
297
- schemaItem: {
298
- title: '',
299
- version: '0.0.0',
300
- description: '',
301
- summary: '',
302
- slug: '',
303
- properties: {},
304
- configuration: {
305
- objectNameField: '',
306
- objectDescriptionField: '',
307
- objectImageField: '',
308
- objectSummaryField: '',
309
- allowFiles: false,
310
- allowedTags: [],
311
- autoPublish: false,
312
- },
313
- authorization: {},
314
- hardValidation: false,
315
- immutable: false,
316
- searchable: true,
317
- maxDepth: 0,
318
- },
319
- }
320
- },
321
- computed: {
322
- /**
323
- * Tab definitions for CnTabbedFormDialog.
324
- *
325
- * @return {Array} Tab configuration
326
- */
327
- dialogTabs() {
328
- return [
329
- { id: 'properties', title: 'Properties' },
330
- { id: 'configuration', title: 'Configuration' },
331
- { id: 'security', title: 'Security' },
332
- ]
333
- },
334
- sortedUserGroups() {
335
- return this.userGroups
336
- .filter(group => group.id !== 'admin' && group.id !== 'public')
337
- .sort((a, b) => {
338
- const nameA = a.displayname || a.id
339
- const nameB = b.displayname || b.id
340
- return nameA.localeCompare(nameB)
341
- })
342
- },
343
- hasAnyPermissions() {
344
- const auth = this.schemaItem.authorization || {}
345
- return Object.keys(auth).some(action =>
346
- Array.isArray(auth[action]) && auth[action].length > 0,
347
- )
348
- },
349
- isRestrictiveSchema() {
350
- const auth = this.schemaItem.authorization || {}
351
- const actions = ['create', 'read', 'update', 'delete']
352
- return actions.some(action =>
353
- Array.isArray(auth[action]) && auth[action].length > 0
354
- && !auth[action].includes('public'),
355
- )
356
- },
357
- typeOptionsForSelect() {
358
- return [
359
- { id: 'string', label: 'String' },
360
- { id: 'number', label: 'Number' },
361
- { id: 'integer', label: 'Integer' },
362
- { id: 'boolean', label: 'Boolean' },
363
- { id: 'array', label: 'Array' },
364
- { id: 'object', label: 'Object' },
365
- { id: 'dictionary', label: 'Dictionary' },
366
- { id: 'file', label: 'File' },
367
- { id: 'oneOf', label: 'One Of' },
368
- ]
369
- },
370
- propertyOptions() {
371
- const properties = this.schemaItem.properties || {}
372
- return ['', ...Object.keys(properties)]
373
- },
374
- availableTagsOptions() {
375
- return this.availableTags.map(tag => ({
376
- id: tag,
377
- label: tag,
378
- }))
379
- },
380
- /**
381
- * Resolved success text for backwards compatibility (includes trailing period).
382
- *
383
- * @return {string}
384
- */
385
- resolvedSuccessText() {
386
- if (this.successText) return this.successText
387
- return 'Schema saved successfully.'
388
- },
389
- allOfSchemaNames() {
390
- if (!this.schemaItem.allOf || !Array.isArray(this.schemaItem.allOf) || this.schemaItem.allOf.length === 0) {
391
- return []
392
- }
393
-
394
- return this.schemaItem.allOf
395
- .map(ref => {
396
- const schemaId = typeof ref === 'object' ? ref.id : ref
397
- const schema = this.availableSchemas.find(s => s.id === schemaId)
398
- return schema ? (schema.title || `Schema ${schema.id}`) : schemaId
399
- })
400
- .filter(name => name)
401
- },
402
- },
403
- watch: {
404
- item: {
405
- immediate: true,
406
- handler() {
407
- this.initializeSchemaItem()
408
- },
409
- },
410
- 'schemaItem.properties': {
411
- handler(newProperties) {
412
- if (newProperties) {
413
- Object.keys(newProperties).forEach(key => {
414
- const property = newProperties[key]
415
- if (property) {
416
- // Initialize nested objects if they don't exist
417
- if (property.type === 'array' && !property.items) {
418
- this.$set(this.schemaItem.properties[key], 'items', { type: 'string' })
419
- }
420
- if (property.type === 'object' && !property.objectConfiguration) {
421
- this.$set(this.schemaItem.properties[key], 'objectConfiguration', { handling: 'nested-object' })
422
- }
423
- if (property.type === 'array' && property.items && property.items.type === 'object' && !property.items.objectConfiguration) {
424
- this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'nested-object' })
425
- }
426
-
427
- // Convert property type from object to string
428
- if (property.type && typeof property.type === 'object' && property.type.id) {
429
- this.$set(this.schemaItem.properties[key], 'type', property.type.id)
430
- }
431
-
432
- // Convert property format from object to string
433
- if (property.format && typeof property.format === 'object' && property.format.id) {
434
- this.$set(this.schemaItem.properties[key], 'format', property.format.id)
435
- }
436
-
437
- // Convert array item type from object to string
438
- if (property.items && property.items.type && typeof property.items.type === 'object' && property.items.type.id) {
439
- this.$set(this.schemaItem.properties[key].items, 'type', property.items.type.id)
440
- }
441
-
442
- // Convert object handling from object to string
443
- if (property.objectConfiguration && property.objectConfiguration.handling
444
- && typeof property.objectConfiguration.handling === 'object' && property.objectConfiguration.handling.id) {
445
- this.$set(this.schemaItem.properties[key].objectConfiguration, 'handling', property.objectConfiguration.handling.id)
446
- }
447
-
448
- // Convert register from object to ID
449
- if (property.objectConfiguration && property.objectConfiguration.register
450
- && typeof property.objectConfiguration.register === 'object' && property.objectConfiguration.register.id) {
451
- this.$set(this.schemaItem.properties[key].objectConfiguration, 'register', property.objectConfiguration.register.id)
452
- }
453
-
454
- // Convert array item object handling from object to string
455
- if (property.items && property.items.objectConfiguration && property.items.objectConfiguration.handling
456
- && typeof property.items.objectConfiguration.handling === 'object' && property.items.objectConfiguration.handling.id) {
457
- this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'handling', property.items.objectConfiguration.handling.id)
458
- }
459
-
460
- // Convert array item register from object to ID
461
- if (property.items && property.items.objectConfiguration && property.items.objectConfiguration.register
462
- && typeof property.items.objectConfiguration.register === 'object' && property.items.objectConfiguration.register.id) {
463
- this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'register', property.items.objectConfiguration.register.id)
464
- }
465
-
466
- // Ensure $ref is always a string
467
- this.ensureRefIsString(this.schemaItem.properties, key)
468
-
469
- // Ensure inversedBy is always a string for regular properties
470
- if (property.inversedBy && typeof property.inversedBy === 'object' && property.inversedBy.id) {
471
- this.$set(this.schemaItem.properties[key], 'inversedBy', property.inversedBy.id)
472
- }
473
-
474
- // Ensure inversedBy is always a string for array items
475
- if (property.items && property.items.inversedBy && typeof property.items.inversedBy === 'object' && property.items.inversedBy.id) {
476
- this.$set(this.schemaItem.properties[key].items, 'inversedBy', property.items.inversedBy.id)
477
- }
478
- }
479
- })
480
- }
481
- this.checkPropertiesModified()
482
- },
483
- deep: true,
484
- },
485
- },
486
- methods: {
487
- findSchemaBySlug(schemaSlug) {
488
- if (!schemaSlug) return undefined
489
- return this.availableSchemas.find(schema =>
490
- (schema.slug && schema.slug.toLowerCase() === schemaSlug.toLowerCase())
491
- || schema.id === schemaSlug
492
- || schema.title === schemaSlug,
493
- )
494
- },
495
-
496
- ensureRefIsString(obj, key) {
497
- if (!obj || !key) return
498
-
499
- if (obj[key] && typeof obj[key].$ref === 'object' && obj[key].$ref !== null) {
500
- if (obj[key].$ref.id) {
501
- obj[key].$ref = obj[key].$ref.id
502
- } else {
503
- obj[key].$ref = ''
504
- }
505
- }
506
-
507
- if (obj[key] && obj[key].items && typeof obj[key].items.$ref === 'object' && obj[key].items.$ref !== null) {
508
- if (obj[key].items.$ref.id) {
509
- obj[key].items.$ref = obj[key].items.$ref.id
510
- } else {
511
- obj[key].items.$ref = ''
512
- }
513
- }
514
- },
515
-
516
- initializeSchemaItem() {
517
- // Reset CnTabbedFormDialog state if available (not yet mounted on first call)
518
- if (this.$refs.dialog) {
519
- this.$refs.dialog.resetDialog()
520
- }
521
-
522
- if (this.item && this.item.id) {
523
- this.schemaItem = {
524
- ...this.schemaItem,
525
- ...JSON.parse(JSON.stringify(this.item)),
526
- }
527
-
528
- // Ensure configuration object exists
529
- if (!this.schemaItem.configuration) {
530
- this.schemaItem.configuration = {
531
- objectNameField: '',
532
- objectDescriptionField: '',
533
- objectImageField: '',
534
- objectSummaryField: '',
535
- allowFiles: false,
536
- allowedTags: [],
537
- }
538
- } else {
539
- if (!this.schemaItem.configuration.objectNameField) {
540
- this.schemaItem.configuration.objectNameField = ''
541
- }
542
- if (!this.schemaItem.configuration.objectDescriptionField) {
543
- this.schemaItem.configuration.objectDescriptionField = ''
544
- }
545
- if (!this.schemaItem.configuration.objectImageField) {
546
- this.schemaItem.configuration.objectImageField = ''
547
- }
548
- if (!this.schemaItem.configuration.objectSummaryField) {
549
- this.schemaItem.configuration.objectSummaryField = ''
550
- }
551
- if (this.schemaItem.configuration.allowFiles === undefined) {
552
- this.schemaItem.configuration.allowFiles = false
553
- }
554
- if (!this.schemaItem.configuration.allowedTags) {
555
- this.schemaItem.configuration.allowedTags = []
556
- }
557
- if (this.schemaItem.configuration.autoPublish === undefined) {
558
- this.schemaItem.configuration.autoPublish = false
559
- }
560
- }
561
-
562
- // Ensure authorization object exists
563
- if (!this.schemaItem.authorization) {
564
- this.schemaItem.authorization = {}
565
- }
566
-
567
- // Ensure existing properties have facetable set to false by default
568
- Object.keys(this.schemaItem.properties || {}).forEach(key => {
569
- if (this.schemaItem.properties[key].facetable === undefined) {
570
- this.$set(this.schemaItem.properties[key], 'facetable', false)
571
- }
572
-
573
- if (this.schemaItem.properties[key].enum && Array.isArray(this.schemaItem.properties[key].enum)) {
574
- this.$set(this.schemaItem.properties[key], 'enum', [...this.schemaItem.properties[key].enum])
575
- }
576
-
577
- const property = this.schemaItem.properties[key]
578
- if (property.type === 'array' && property.items && property.items.type === 'object' && !property.items.objectConfiguration) {
579
- this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'nested-object' })
580
- }
581
- })
582
-
583
- // Ensure all $ref values are strings and migrate old structure
584
- Object.keys(this.schemaItem.properties || {}).forEach(key => {
585
- this.ensureRefIsString(this.schemaItem.properties, key)
586
- this.migratePropertyToNewStructure(key)
587
- })
588
-
589
- this.originalProperties = JSON.parse(JSON.stringify(this.schemaItem.properties || {}))
590
- } else {
591
- this.schemaItem.configuration = {
592
- objectNameField: '',
593
- objectDescriptionField: '',
594
- objectImageField: '',
595
- objectSummaryField: '',
596
- allowFiles: false,
597
- allowedTags: [],
598
- autoPublish: false,
599
- }
600
- this.originalProperties = {}
601
- }
602
- this.propertiesModified = false
603
- },
604
-
605
- checkPropertiesModified() {
606
- if (!this.originalProperties) return false
607
-
608
- const currentProperties = JSON.stringify(this.schemaItem.properties || {})
609
- const originalProperties = JSON.stringify(this.originalProperties)
610
-
611
- this.propertiesModified = currentProperties !== originalProperties
612
- },
613
-
614
- async copyToClipboard(text) {
615
- try {
616
- await navigator.clipboard.writeText(text)
617
- this.isCopied = true
618
- setTimeout(() => { this.isCopied = false }, 2000)
619
- } catch (err) {
620
- console.error('Failed to copy text:', err)
621
- }
622
- },
623
-
624
- addProperty() {
625
- let newPropertyName = 'new'
626
- let counter = 1
627
-
628
- while (this.schemaItem.properties[newPropertyName]) {
629
- counter++
630
- newPropertyName = `new_${counter}`
631
- }
632
-
633
- this.$set(this.schemaItem.properties, newPropertyName, {
634
- type: 'string',
635
- format: '',
636
- title: newPropertyName,
637
- description: '',
638
- facetable: false,
639
- })
640
-
641
- this.checkPropertiesModified()
642
- this.selectedProperty = newPropertyName
643
- },
644
-
645
- updatePropertyKey(oldKey, newKey) {
646
- if (!newKey || newKey === oldKey) return
647
- if (this.schemaItem.properties[newKey] && newKey !== oldKey) return
648
-
649
- const propertyData = { ...this.schemaItem.properties[oldKey] }
650
-
651
- this.$set(this.schemaItem.properties, newKey, propertyData)
652
- this.$delete(this.schemaItem.properties, oldKey)
653
-
654
- this.selectedProperty = newKey
655
- this.checkPropertiesModified()
656
- },
657
-
658
- deleteProperty(key) {
659
- this.$delete(this.schemaItem.properties, key)
660
-
661
- if (this.selectedProperty === key) {
662
- this.selectedProperty = null
663
- }
664
-
665
- this.checkPropertiesModified()
666
- },
667
-
668
- copyProperty(key) {
669
- if (this.schemaItem.properties[key]) {
670
- const originalProperty = JSON.parse(JSON.stringify(this.schemaItem.properties[key]))
671
-
672
- let newPropertyName = `${key}_copy`
673
- let counter = 1
674
-
675
- while (this.schemaItem.properties[newPropertyName]) {
676
- counter++
677
- newPropertyName = `${key}_copy_${counter}`
678
- }
679
-
680
- const originalTitle = originalProperty.title || key
681
- this.$set(this.schemaItem.properties, newPropertyName, {
682
- ...originalProperty,
683
- title: `${originalTitle} (copy)`,
684
- })
685
-
686
- this.checkPropertiesModified()
687
- this.selectedProperty = newPropertyName
688
- }
689
- },
690
-
691
- /**
692
- * Handle confirm from CnTabbedFormDialog.
693
- * Cleans schema data and emits confirm with the cleaned payload.
694
- */
695
- handleConfirm() {
696
- const cleanedSchemaItem = JSON.parse(JSON.stringify(this.schemaItem))
697
- Object.keys(cleanedSchemaItem.properties || {}).forEach(key => {
698
- this.ensureRefIsString(cleanedSchemaItem.properties, key)
699
-
700
- if (cleanedSchemaItem.properties[key].register
701
- && cleanedSchemaItem.properties[key].objectConfiguration
702
- && cleanedSchemaItem.properties[key].objectConfiguration.register) {
703
- delete cleanedSchemaItem.properties[key].register
704
- }
705
-
706
- if (cleanedSchemaItem.properties[key].items
707
- && cleanedSchemaItem.properties[key].items.register
708
- && cleanedSchemaItem.properties[key].items.objectConfiguration
709
- && cleanedSchemaItem.properties[key].items.objectConfiguration.register) {
710
- delete cleanedSchemaItem.properties[key].items.register
711
- }
712
- })
713
-
714
- this.$emit('confirm', cleanedSchemaItem)
715
- },
716
-
717
- /**
718
- * Set the result of the save operation. Call this from the parent
719
- * after the API call completes.
720
- *
721
- * @param {{ success?: boolean, error?: string }} resultData - result data
722
- * @public
723
- */
724
- setResult(resultData) {
725
- this.$refs.dialog.setResult(resultData)
726
- if (resultData.success) {
727
- this.originalProperties = JSON.parse(JSON.stringify(this.schemaItem.properties || {}))
728
- this.propertiesModified = false
729
- }
730
- },
731
-
732
- migratePropertyToNewStructure(key) {
733
- if (!this.schemaItem.properties[key]) return
734
-
735
- const property = this.schemaItem.properties[key]
736
-
737
- if (property.$ref && property.register && !property.objectConfiguration?.register) {
738
- if (!property.objectConfiguration) {
739
- this.$set(this.schemaItem.properties[key], 'objectConfiguration', { handling: 'related-object' })
740
- }
741
-
742
- const registerId = typeof property.register === 'object' && property.register.id
743
- ? property.register.id
744
- : property.register
745
-
746
- this.$set(this.schemaItem.properties[key].objectConfiguration, 'register', registerId)
747
-
748
- if (property.$ref) {
749
- let schemaSlug = property.$ref
750
- if (schemaSlug.includes('/')) {
751
- schemaSlug = schemaSlug.substring(schemaSlug.lastIndexOf('/') + 1)
752
- }
753
-
754
- const referencedSchema = this.findSchemaBySlug(schemaSlug)
755
- if (referencedSchema) {
756
- this.$set(this.schemaItem.properties[key].objectConfiguration, 'schema', referencedSchema.id)
757
- }
758
- }
759
- }
760
-
761
- if (property.items && property.items.$ref && property.items.register && !property.items.objectConfiguration?.register) {
762
- if (!property.items.objectConfiguration) {
763
- this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'related-object' })
764
- }
765
-
766
- const registerId = typeof property.items.register === 'object' && property.items.register.id
767
- ? property.items.register.id
768
- : property.items.register
769
-
770
- this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'register', registerId)
771
-
772
- if (property.items.$ref) {
773
- let schemaSlug = property.items.$ref
774
- if (schemaSlug.includes('/')) {
775
- schemaSlug = schemaSlug.substring(schemaSlug.lastIndexOf('/') + 1)
776
- }
777
-
778
- const referencedSchema = this.findSchemaBySlug(schemaSlug)
779
- if (referencedSchema) {
780
- this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'schema', referencedSchema.id)
781
- }
782
- }
783
- }
784
- },
785
- },
786
- }
787
- </script>
1
+ <template>
2
+ <CnTabbedFormDialog
3
+ ref="dialog"
4
+ :tabs="dialogTabs"
5
+ :item="item"
6
+ :dialog-title="dialogTitle"
7
+ entity-name="Schema"
8
+ :size="size"
9
+ :disable-save="!schemaItem.title"
10
+ :success-text="resolvedSuccessText"
11
+ :cancel-label="cancelLabel"
12
+ :close-label="closeLabel"
13
+ :confirm-label="confirmLabel"
14
+ @confirm="handleConfirm"
15
+ @close="$emit('close')">
16
+ <!-- Metadata Display -->
17
+ <template #above-tabs="{ loading: dialogLoading }">
18
+ <div class="cn-schema-form__detail-grid">
19
+ <div v-if="schemaItem.id"
20
+ class="cn-schema-form__detail-item cn-schema-form__id-card">
21
+ <div class="cn-schema-form__id-card-header">
22
+ <span class="cn-schema-form__detail-label">ID / UUID:</span>
23
+ <NcButton class="cn-schema-form__copy-button"
24
+ @click="copyToClipboard(schemaItem.uuid || schemaItem.id)">
25
+ <template #icon>
26
+ <Check v-if="isCopied" :size="20" />
27
+ <ContentCopy v-else :size="20" />
28
+ </template>
29
+ {{ isCopied ? 'Copied' : 'Copy' }}
30
+ </NcButton>
31
+ </div>
32
+ <span class="cn-schema-form__detail-value">{{ schemaItem.id }}</span>
33
+ <span v-if="schemaItem.uuid && schemaItem.uuid !== schemaItem.id"
34
+ class="cn-schema-form__detail-value cn-schema-form__uuid-value">{{ schemaItem.uuid }}</span>
35
+ </div>
36
+ <div class="cn-schema-form__detail-item cn-schema-form__title-with-badge">
37
+ <NcTextField :disabled="dialogLoading"
38
+ label="Title *"
39
+ :value.sync="schemaItem.title" />
40
+ <span v-if="schemaItem.allOf && schemaItem.allOf.length > 0"
41
+ class="cn-schema-form__statusPill cn-schema-form__statusPill--success">
42
+ allOf
43
+ </span>
44
+ <span v-if="schemaItem.oneOf && schemaItem.oneOf.length > 0"
45
+ class="cn-schema-form__statusPill cn-schema-form__statusPill--info">
46
+ oneOf
47
+ </span>
48
+ <span v-if="schemaItem.anyOf && schemaItem.anyOf.length > 0"
49
+ class="cn-schema-form__statusPill cn-schema-form__statusPill--info">
50
+ anyOf
51
+ </span>
52
+ </div>
53
+ <div v-if="schemaItem.created" class="cn-schema-form__detail-item">
54
+ <span class="cn-schema-form__detail-label">Created:</span>
55
+ <span class="cn-schema-form__detail-value">{{ new Date(schemaItem.created).toLocaleString() }}</span>
56
+ </div>
57
+ <div v-if="schemaItem.updated" class="cn-schema-form__detail-item">
58
+ <span class="cn-schema-form__detail-label">Updated:</span>
59
+ <span class="cn-schema-form__detail-value">{{ new Date(schemaItem.updated).toLocaleString() }}</span>
60
+ </div>
61
+ <div class="cn-schema-form__detail-item">
62
+ <span class="cn-schema-form__detail-label">Version:</span>
63
+ <span class="cn-schema-form__detail-value">{{ schemaItem.version || 'Not set' }}</span>
64
+ </div>
65
+ <div class="cn-schema-form__detail-item">
66
+ <span class="cn-schema-form__detail-label">Owner:</span>
67
+ <span class="cn-schema-form__detail-value">{{ schemaItem.owner || 'Not set' }}</span>
68
+ </div>
69
+ </div>
70
+ </template>
71
+
72
+ <!-- Properties Tab -->
73
+ <template #tab-properties="{ loading: dialogLoading }">
74
+ <CnSchemaPropertiesTab
75
+ :schema-item="schemaItem"
76
+ :loading="dialogLoading"
77
+ :selected-property="selectedProperty"
78
+ :properties-modified="propertiesModified"
79
+ :original-properties="originalProperties"
80
+ :type-options-for-select="typeOptionsForSelect"
81
+ :available-schemas="availableSchemas"
82
+ :available-registers="availableRegisters"
83
+ :available-tags-options="availableTagsOptions"
84
+ :user-groups="userGroups"
85
+ :sorted-user-groups="sortedUserGroups"
86
+ :loading-groups="loadingGroups"
87
+ @add-property="addProperty"
88
+ @update:selected-property="selectedProperty = $event"
89
+ @update:property-key="updatePropertyKey($event.oldKey, $event.newKey)"
90
+ @copy-property="copyProperty"
91
+ @delete-property="deleteProperty" />
92
+ </template>
93
+
94
+ <!-- Configuration Tab -->
95
+ <template #tab-configuration="{ loading: dialogLoading }">
96
+ <CnSchemaConfigurationTab
97
+ :schema-item="schemaItem"
98
+ :loading="dialogLoading"
99
+ :available-schemas="availableSchemas"
100
+ :property-options="propertyOptions"
101
+ :all-of-schema-names="allOfSchemaNames" />
102
+ </template>
103
+
104
+ <!-- Security Tab -->
105
+ <template #tab-security>
106
+ <CnSchemaSecurityTab
107
+ :schema-item="schemaItem"
108
+ :user-groups="userGroups"
109
+ :sorted-user-groups="sortedUserGroups"
110
+ :loading-groups="loadingGroups"
111
+ :has-any-permissions="hasAnyPermissions"
112
+ :is-restrictive-schema="isRestrictiveSchema" />
113
+ </template>
114
+
115
+ <!-- Optional Action Buttons (edit mode only) -->
116
+ <template #actions-right="{ loading: dialogLoading, isCreateMode, result: dialogResult }">
117
+ <template v-if="!isCreateMode && dialogResult === null">
118
+ <NcButton
119
+ v-if="showExtendSchema"
120
+ :disabled="dialogLoading"
121
+ @click="$emit('extend-schema')">
122
+ <template #icon>
123
+ <CallSplit :size="20" />
124
+ </template>
125
+ {{ extendSchemaLabel }}
126
+ </NcButton>
127
+ <NcButton
128
+ v-if="showAnalyzeProperties"
129
+ :disabled="dialogLoading"
130
+ @click="$emit('analyze-properties')">
131
+ <template #icon>
132
+ <DatabaseSearch :size="20" />
133
+ </template>
134
+ {{ analyzePropertiesLabel }}
135
+ </NcButton>
136
+ <NcButton
137
+ v-if="showValidateObjects"
138
+ :disabled="dialogLoading"
139
+ @click="$emit('validate-objects')">
140
+ <template #icon>
141
+ <CheckCircle :size="20" />
142
+ </template>
143
+ {{ validateObjectsLabel }}
144
+ </NcButton>
145
+ <NcButton
146
+ v-if="showDeleteObjects"
147
+ v-tooltip="objectCount > 0 ? deleteObjectsTooltip : noDeleteObjectsTooltip"
148
+ :disabled="dialogLoading || objectCount === 0"
149
+ @click="$emit('delete-objects')">
150
+ <template #icon>
151
+ <DeleteSweep :size="20" />
152
+ </template>
153
+ {{ deleteObjectsLabel }}
154
+ </NcButton>
155
+ <NcButton
156
+ v-if="showPublishObjects"
157
+ v-tooltip="objectCount > 0 ? publishObjectsTooltip : noPublishObjectsTooltip"
158
+ :disabled="dialogLoading || objectCount === 0"
159
+ @click="$emit('publish-objects')">
160
+ <template #icon>
161
+ <Upload :size="20" />
162
+ </template>
163
+ {{ publishObjectsLabel }}
164
+ </NcButton>
165
+ <NcButton
166
+ v-if="showDelete"
167
+ v-tooltip="objectCount > 0 ? cannotDeleteTooltip : ''"
168
+ :disabled="dialogLoading || objectCount > 0"
169
+ type="error"
170
+ @click="$emit('delete-schema')">
171
+ <template #icon>
172
+ <TrashCanOutline :size="20" />
173
+ </template>
174
+ {{ deleteLabel }}
175
+ </NcButton>
176
+ </template>
177
+ </template>
178
+ </CnTabbedFormDialog>
179
+ </template>
180
+
181
+ <script>
182
+ import {
183
+ NcButton,
184
+ NcTextField,
185
+ } from '@nextcloud/vue'
186
+
187
+ import CnTabbedFormDialog from '../CnTabbedFormDialog/CnTabbedFormDialog.vue'
188
+ import CnSchemaPropertiesTab from './CnSchemaPropertiesTab.vue'
189
+ import CnSchemaConfigurationTab from './CnSchemaConfigurationTab.vue'
190
+ import CnSchemaSecurityTab from './CnSchemaSecurityTab.vue'
191
+
192
+ import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
193
+ import Check from 'vue-material-design-icons/Check.vue'
194
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
195
+ import CallSplit from 'vue-material-design-icons/CallSplit.vue'
196
+ import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
197
+ import CheckCircle from 'vue-material-design-icons/CheckCircle.vue'
198
+ import DeleteSweep from 'vue-material-design-icons/DeleteSweep.vue'
199
+ import Upload from 'vue-material-design-icons/Upload.vue'
200
+
201
+ /**
202
+ * CnSchemaFormDialog — Generic JSON Schema editor dialog.
203
+ *
204
+ * Provides a full-featured form for creating and editing JSON Schemas with
205
+ * properties table, configuration, and security (RBAC) tabs. Uses CnTabbedFormDialog.
206
+ *
207
+ * The dialog does NOT perform saves — it emits a `confirm` event with the schema data.
208
+ * The parent performs the actual API call and calls `setResult()` via a ref.
209
+ *
210
+ * @event confirm Emitted when the user confirms. Payload: cleaned schema data object.
211
+ * @event close Emitted when the dialog should be closed.
212
+ * @event extend-schema Emitted when the Extend Schema button is clicked.
213
+ * @event analyze-properties Emitted when the Analyze Properties button is clicked.
214
+ * @event validate-objects Emitted when the Validate Objects button is clicked.
215
+ * @event delete-objects Emitted when the Delete Objects button is clicked.
216
+ * @event publish-objects Emitted when the Publish Objects button is clicked.
217
+ * @event delete-schema Emitted when the Delete button is clicked.
218
+ */
219
+ export default {
220
+ name: 'CnSchemaFormDialog',
221
+ components: {
222
+ NcTextField,
223
+ NcButton,
224
+ CnTabbedFormDialog,
225
+ CnSchemaPropertiesTab,
226
+ CnSchemaConfigurationTab,
227
+ CnSchemaSecurityTab,
228
+ // Icons
229
+ ContentCopy,
230
+ Check,
231
+ TrashCanOutline,
232
+ CallSplit,
233
+ DatabaseSearch,
234
+ CheckCircle,
235
+ DeleteSweep,
236
+ Upload,
237
+ },
238
+ props: {
239
+ /** Existing schema item for edit mode. Pass null for create mode. */
240
+ item: { type: Object, default: null },
241
+ /** Dialog title. Defaults to "Create Schema" or "Edit Schema". */
242
+ dialogTitle: { type: String, default: '' },
243
+ /** NcDialog size */
244
+ size: { type: String, default: 'large' },
245
+ /** Available schemas for references and composition. Array of { id, title, description, reference } */
246
+ availableSchemas: { type: Array, default: () => [] },
247
+ /** Available registers. Array of { id, label } */
248
+ availableRegisters: { type: Array, default: () => [] },
249
+ /** User groups for RBAC. Array of { id, displayname } */
250
+ userGroups: { type: Array, default: () => [] },
251
+ /** Available tags for file property configuration */
252
+ availableTags: { type: Array, default: () => [] },
253
+ /** Whether user groups are still loading */
254
+ loadingGroups: { type: Boolean, default: false },
255
+ /** Number of objects attached to this schema (used for action button disable logic) */
256
+ objectCount: { type: Number, default: 0 },
257
+ // Optional action button visibility
258
+ /** Show "Extend Schema" button */
259
+ showExtendSchema: { type: Boolean, default: false },
260
+ /** Show "Analyze Properties" button */
261
+ showAnalyzeProperties: { type: Boolean, default: false },
262
+ /** Show "Validate Objects" button */
263
+ showValidateObjects: { type: Boolean, default: false },
264
+ /** Show "Delete Objects" button */
265
+ showDeleteObjects: { type: Boolean, default: false },
266
+ /** Show "Publish Objects" button */
267
+ showPublishObjects: { type: Boolean, default: false },
268
+ /** Show "Delete" button */
269
+ showDelete: { type: Boolean, default: false },
270
+ // Labels (pre-translated strings with English defaults)
271
+ cancelLabel: { type: String, default: 'Cancel' },
272
+ closeLabel: { type: String, default: 'Close' },
273
+ /** Confirm button label. Defaults to "Create" or "Save". */
274
+ confirmLabel: { type: String, default: '' },
275
+ /** Success message. Defaults to "Schema saved successfully." */
276
+ successText: { type: String, default: '' },
277
+ extendSchemaLabel: { type: String, default: 'Extend Schema' },
278
+ analyzePropertiesLabel: { type: String, default: 'Analyze Properties' },
279
+ validateObjectsLabel: { type: String, default: 'Validate Objects' },
280
+ deleteObjectsLabel: { type: String, default: 'Delete Objects' },
281
+ publishObjectsLabel: { type: String, default: 'Publish Objects' },
282
+ deleteLabel: { type: String, default: 'Delete' },
283
+ deleteObjectsTooltip: { type: String, default: 'Delete all objects in this schema' },
284
+ publishObjectsTooltip: { type: String, default: 'Publish all objects in this schema' },
285
+ /** Tooltip for the Delete Objects button when no objects exist */
286
+ noDeleteObjectsTooltip: { type: String, default: 'No objects to delete' },
287
+ /** Tooltip for the Publish Objects button when no objects exist */
288
+ noPublishObjectsTooltip: { type: String, default: 'No objects to publish' },
289
+ cannotDeleteTooltip: { type: String, default: 'Cannot delete: objects are still attached' },
290
+ },
291
+ data() {
292
+ return {
293
+ isCopied: false,
294
+ selectedProperty: null,
295
+ propertiesModified: false,
296
+ originalProperties: null,
297
+ schemaItem: {
298
+ title: '',
299
+ version: '0.0.0',
300
+ description: '',
301
+ summary: '',
302
+ slug: '',
303
+ properties: {},
304
+ configuration: {
305
+ objectNameField: '',
306
+ objectDescriptionField: '',
307
+ objectImageField: '',
308
+ objectSummaryField: '',
309
+ allowFiles: false,
310
+ allowedTags: [],
311
+ autoPublish: false,
312
+ },
313
+ authorization: {},
314
+ hardValidation: false,
315
+ immutable: false,
316
+ searchable: true,
317
+ maxDepth: 0,
318
+ },
319
+ }
320
+ },
321
+ computed: {
322
+ /**
323
+ * Tab definitions for CnTabbedFormDialog.
324
+ *
325
+ * @return {Array} Tab configuration
326
+ */
327
+ dialogTabs() {
328
+ return [
329
+ { id: 'properties', title: 'Properties' },
330
+ { id: 'configuration', title: 'Configuration' },
331
+ { id: 'security', title: 'Security' },
332
+ ]
333
+ },
334
+ sortedUserGroups() {
335
+ return this.userGroups
336
+ .filter(group => group.id !== 'admin' && group.id !== 'public')
337
+ .sort((a, b) => {
338
+ const nameA = a.displayname || a.id
339
+ const nameB = b.displayname || b.id
340
+ return nameA.localeCompare(nameB)
341
+ })
342
+ },
343
+ hasAnyPermissions() {
344
+ const auth = this.schemaItem.authorization || {}
345
+ return Object.keys(auth).some(action =>
346
+ Array.isArray(auth[action]) && auth[action].length > 0,
347
+ )
348
+ },
349
+ isRestrictiveSchema() {
350
+ const auth = this.schemaItem.authorization || {}
351
+ const actions = ['create', 'read', 'update', 'delete']
352
+ return actions.some(action =>
353
+ Array.isArray(auth[action]) && auth[action].length > 0
354
+ && !auth[action].includes('public'),
355
+ )
356
+ },
357
+ typeOptionsForSelect() {
358
+ return [
359
+ { id: 'string', label: 'String' },
360
+ { id: 'number', label: 'Number' },
361
+ { id: 'integer', label: 'Integer' },
362
+ { id: 'boolean', label: 'Boolean' },
363
+ { id: 'array', label: 'Array' },
364
+ { id: 'object', label: 'Object' },
365
+ { id: 'dictionary', label: 'Dictionary' },
366
+ { id: 'file', label: 'File' },
367
+ { id: 'oneOf', label: 'One Of' },
368
+ ]
369
+ },
370
+ propertyOptions() {
371
+ const properties = this.schemaItem.properties || {}
372
+ return ['', ...Object.keys(properties)]
373
+ },
374
+ availableTagsOptions() {
375
+ return this.availableTags.map(tag => ({
376
+ id: tag,
377
+ label: tag,
378
+ }))
379
+ },
380
+ /**
381
+ * Resolved success text for backwards compatibility (includes trailing period).
382
+ *
383
+ * @return {string}
384
+ */
385
+ resolvedSuccessText() {
386
+ if (this.successText) return this.successText
387
+ return 'Schema saved successfully.'
388
+ },
389
+ allOfSchemaNames() {
390
+ if (!this.schemaItem.allOf || !Array.isArray(this.schemaItem.allOf) || this.schemaItem.allOf.length === 0) {
391
+ return []
392
+ }
393
+
394
+ return this.schemaItem.allOf
395
+ .map(ref => {
396
+ const schemaId = typeof ref === 'object' ? ref.id : ref
397
+ const schema = this.availableSchemas.find(s => s.id === schemaId)
398
+ return schema ? (schema.title || `Schema ${schema.id}`) : schemaId
399
+ })
400
+ .filter(name => name)
401
+ },
402
+ },
403
+ watch: {
404
+ item: {
405
+ immediate: true,
406
+ handler() {
407
+ this.initializeSchemaItem()
408
+ },
409
+ },
410
+ 'schemaItem.properties': {
411
+ handler(newProperties) {
412
+ if (newProperties) {
413
+ Object.keys(newProperties).forEach(key => {
414
+ const property = newProperties[key]
415
+ if (property) {
416
+ // Initialize nested objects if they don't exist
417
+ if (property.type === 'array' && !property.items) {
418
+ this.$set(this.schemaItem.properties[key], 'items', { type: 'string' })
419
+ }
420
+ if (property.type === 'object' && !property.objectConfiguration) {
421
+ this.$set(this.schemaItem.properties[key], 'objectConfiguration', { handling: 'nested-object' })
422
+ }
423
+ if (property.type === 'array' && property.items && property.items.type === 'object' && !property.items.objectConfiguration) {
424
+ this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'nested-object' })
425
+ }
426
+
427
+ // Convert property type from object to string
428
+ if (property.type && typeof property.type === 'object' && property.type.id) {
429
+ this.$set(this.schemaItem.properties[key], 'type', property.type.id)
430
+ }
431
+
432
+ // Convert property format from object to string
433
+ if (property.format && typeof property.format === 'object' && property.format.id) {
434
+ this.$set(this.schemaItem.properties[key], 'format', property.format.id)
435
+ }
436
+
437
+ // Convert array item type from object to string
438
+ if (property.items && property.items.type && typeof property.items.type === 'object' && property.items.type.id) {
439
+ this.$set(this.schemaItem.properties[key].items, 'type', property.items.type.id)
440
+ }
441
+
442
+ // Convert object handling from object to string
443
+ if (property.objectConfiguration && property.objectConfiguration.handling
444
+ && typeof property.objectConfiguration.handling === 'object' && property.objectConfiguration.handling.id) {
445
+ this.$set(this.schemaItem.properties[key].objectConfiguration, 'handling', property.objectConfiguration.handling.id)
446
+ }
447
+
448
+ // Convert register from object to ID
449
+ if (property.objectConfiguration && property.objectConfiguration.register
450
+ && typeof property.objectConfiguration.register === 'object' && property.objectConfiguration.register.id) {
451
+ this.$set(this.schemaItem.properties[key].objectConfiguration, 'register', property.objectConfiguration.register.id)
452
+ }
453
+
454
+ // Convert array item object handling from object to string
455
+ if (property.items && property.items.objectConfiguration && property.items.objectConfiguration.handling
456
+ && typeof property.items.objectConfiguration.handling === 'object' && property.items.objectConfiguration.handling.id) {
457
+ this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'handling', property.items.objectConfiguration.handling.id)
458
+ }
459
+
460
+ // Convert array item register from object to ID
461
+ if (property.items && property.items.objectConfiguration && property.items.objectConfiguration.register
462
+ && typeof property.items.objectConfiguration.register === 'object' && property.items.objectConfiguration.register.id) {
463
+ this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'register', property.items.objectConfiguration.register.id)
464
+ }
465
+
466
+ // Ensure $ref is always a string
467
+ this.ensureRefIsString(this.schemaItem.properties, key)
468
+
469
+ // Ensure inversedBy is always a string for regular properties
470
+ if (property.inversedBy && typeof property.inversedBy === 'object' && property.inversedBy.id) {
471
+ this.$set(this.schemaItem.properties[key], 'inversedBy', property.inversedBy.id)
472
+ }
473
+
474
+ // Ensure inversedBy is always a string for array items
475
+ if (property.items && property.items.inversedBy && typeof property.items.inversedBy === 'object' && property.items.inversedBy.id) {
476
+ this.$set(this.schemaItem.properties[key].items, 'inversedBy', property.items.inversedBy.id)
477
+ }
478
+ }
479
+ })
480
+ }
481
+ this.checkPropertiesModified()
482
+ },
483
+ deep: true,
484
+ },
485
+ },
486
+ methods: {
487
+ findSchemaBySlug(schemaSlug) {
488
+ if (!schemaSlug) return undefined
489
+ return this.availableSchemas.find(schema =>
490
+ (schema.slug && schema.slug.toLowerCase() === schemaSlug.toLowerCase())
491
+ || schema.id === schemaSlug
492
+ || schema.title === schemaSlug,
493
+ )
494
+ },
495
+
496
+ ensureRefIsString(obj, key) {
497
+ if (!obj || !key) return
498
+
499
+ if (obj[key] && typeof obj[key].$ref === 'object' && obj[key].$ref !== null) {
500
+ if (obj[key].$ref.id) {
501
+ obj[key].$ref = obj[key].$ref.id
502
+ } else {
503
+ obj[key].$ref = ''
504
+ }
505
+ }
506
+
507
+ if (obj[key] && obj[key].items && typeof obj[key].items.$ref === 'object' && obj[key].items.$ref !== null) {
508
+ if (obj[key].items.$ref.id) {
509
+ obj[key].items.$ref = obj[key].items.$ref.id
510
+ } else {
511
+ obj[key].items.$ref = ''
512
+ }
513
+ }
514
+ },
515
+
516
+ initializeSchemaItem() {
517
+ // Reset CnTabbedFormDialog state if available (not yet mounted on first call)
518
+ if (this.$refs.dialog) {
519
+ this.$refs.dialog.resetDialog()
520
+ }
521
+
522
+ if (this.item && this.item.id) {
523
+ this.schemaItem = {
524
+ ...this.schemaItem,
525
+ ...JSON.parse(JSON.stringify(this.item)),
526
+ }
527
+
528
+ // Ensure configuration object exists
529
+ if (!this.schemaItem.configuration) {
530
+ this.schemaItem.configuration = {
531
+ objectNameField: '',
532
+ objectDescriptionField: '',
533
+ objectImageField: '',
534
+ objectSummaryField: '',
535
+ allowFiles: false,
536
+ allowedTags: [],
537
+ }
538
+ } else {
539
+ if (!this.schemaItem.configuration.objectNameField) {
540
+ this.schemaItem.configuration.objectNameField = ''
541
+ }
542
+ if (!this.schemaItem.configuration.objectDescriptionField) {
543
+ this.schemaItem.configuration.objectDescriptionField = ''
544
+ }
545
+ if (!this.schemaItem.configuration.objectImageField) {
546
+ this.schemaItem.configuration.objectImageField = ''
547
+ }
548
+ if (!this.schemaItem.configuration.objectSummaryField) {
549
+ this.schemaItem.configuration.objectSummaryField = ''
550
+ }
551
+ if (this.schemaItem.configuration.allowFiles === undefined) {
552
+ this.schemaItem.configuration.allowFiles = false
553
+ }
554
+ if (!this.schemaItem.configuration.allowedTags) {
555
+ this.schemaItem.configuration.allowedTags = []
556
+ }
557
+ if (this.schemaItem.configuration.autoPublish === undefined) {
558
+ this.schemaItem.configuration.autoPublish = false
559
+ }
560
+ }
561
+
562
+ // Ensure authorization object exists
563
+ if (!this.schemaItem.authorization) {
564
+ this.schemaItem.authorization = {}
565
+ }
566
+
567
+ // Ensure existing properties have facetable set to false by default
568
+ Object.keys(this.schemaItem.properties || {}).forEach(key => {
569
+ if (this.schemaItem.properties[key].facetable === undefined) {
570
+ this.$set(this.schemaItem.properties[key], 'facetable', false)
571
+ }
572
+
573
+ if (this.schemaItem.properties[key].enum && Array.isArray(this.schemaItem.properties[key].enum)) {
574
+ this.$set(this.schemaItem.properties[key], 'enum', [...this.schemaItem.properties[key].enum])
575
+ }
576
+
577
+ const property = this.schemaItem.properties[key]
578
+ if (property.type === 'array' && property.items && property.items.type === 'object' && !property.items.objectConfiguration) {
579
+ this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'nested-object' })
580
+ }
581
+ })
582
+
583
+ // Ensure all $ref values are strings and migrate old structure
584
+ Object.keys(this.schemaItem.properties || {}).forEach(key => {
585
+ this.ensureRefIsString(this.schemaItem.properties, key)
586
+ this.migratePropertyToNewStructure(key)
587
+ })
588
+
589
+ this.originalProperties = JSON.parse(JSON.stringify(this.schemaItem.properties || {}))
590
+ } else {
591
+ this.schemaItem.configuration = {
592
+ objectNameField: '',
593
+ objectDescriptionField: '',
594
+ objectImageField: '',
595
+ objectSummaryField: '',
596
+ allowFiles: false,
597
+ allowedTags: [],
598
+ autoPublish: false,
599
+ }
600
+ this.originalProperties = {}
601
+ }
602
+ this.propertiesModified = false
603
+ },
604
+
605
+ checkPropertiesModified() {
606
+ if (!this.originalProperties) return false
607
+
608
+ const currentProperties = JSON.stringify(this.schemaItem.properties || {})
609
+ const originalProperties = JSON.stringify(this.originalProperties)
610
+
611
+ this.propertiesModified = currentProperties !== originalProperties
612
+ },
613
+
614
+ async copyToClipboard(text) {
615
+ try {
616
+ await navigator.clipboard.writeText(text)
617
+ this.isCopied = true
618
+ setTimeout(() => { this.isCopied = false }, 2000)
619
+ } catch (err) {
620
+ console.error('Failed to copy text:', err)
621
+ }
622
+ },
623
+
624
+ addProperty() {
625
+ let newPropertyName = 'new'
626
+ let counter = 1
627
+
628
+ while (this.schemaItem.properties[newPropertyName]) {
629
+ counter++
630
+ newPropertyName = `new_${counter}`
631
+ }
632
+
633
+ this.$set(this.schemaItem.properties, newPropertyName, {
634
+ type: 'string',
635
+ format: '',
636
+ title: newPropertyName,
637
+ description: '',
638
+ facetable: false,
639
+ })
640
+
641
+ this.checkPropertiesModified()
642
+ this.selectedProperty = newPropertyName
643
+ },
644
+
645
+ updatePropertyKey(oldKey, newKey) {
646
+ if (!newKey || newKey === oldKey) return
647
+ if (this.schemaItem.properties[newKey] && newKey !== oldKey) return
648
+
649
+ const propertyData = { ...this.schemaItem.properties[oldKey] }
650
+
651
+ this.$set(this.schemaItem.properties, newKey, propertyData)
652
+ this.$delete(this.schemaItem.properties, oldKey)
653
+
654
+ this.selectedProperty = newKey
655
+ this.checkPropertiesModified()
656
+ },
657
+
658
+ deleteProperty(key) {
659
+ this.$delete(this.schemaItem.properties, key)
660
+
661
+ if (this.selectedProperty === key) {
662
+ this.selectedProperty = null
663
+ }
664
+
665
+ this.checkPropertiesModified()
666
+ },
667
+
668
+ copyProperty(key) {
669
+ if (this.schemaItem.properties[key]) {
670
+ const originalProperty = JSON.parse(JSON.stringify(this.schemaItem.properties[key]))
671
+
672
+ let newPropertyName = `${key}_copy`
673
+ let counter = 1
674
+
675
+ while (this.schemaItem.properties[newPropertyName]) {
676
+ counter++
677
+ newPropertyName = `${key}_copy_${counter}`
678
+ }
679
+
680
+ const originalTitle = originalProperty.title || key
681
+ this.$set(this.schemaItem.properties, newPropertyName, {
682
+ ...originalProperty,
683
+ title: `${originalTitle} (copy)`,
684
+ })
685
+
686
+ this.checkPropertiesModified()
687
+ this.selectedProperty = newPropertyName
688
+ }
689
+ },
690
+
691
+ /**
692
+ * Handle confirm from CnTabbedFormDialog.
693
+ * Cleans schema data and emits confirm with the cleaned payload.
694
+ */
695
+ handleConfirm() {
696
+ const cleanedSchemaItem = JSON.parse(JSON.stringify(this.schemaItem))
697
+ Object.keys(cleanedSchemaItem.properties || {}).forEach(key => {
698
+ this.ensureRefIsString(cleanedSchemaItem.properties, key)
699
+
700
+ if (cleanedSchemaItem.properties[key].register
701
+ && cleanedSchemaItem.properties[key].objectConfiguration
702
+ && cleanedSchemaItem.properties[key].objectConfiguration.register) {
703
+ delete cleanedSchemaItem.properties[key].register
704
+ }
705
+
706
+ if (cleanedSchemaItem.properties[key].items
707
+ && cleanedSchemaItem.properties[key].items.register
708
+ && cleanedSchemaItem.properties[key].items.objectConfiguration
709
+ && cleanedSchemaItem.properties[key].items.objectConfiguration.register) {
710
+ delete cleanedSchemaItem.properties[key].items.register
711
+ }
712
+ })
713
+
714
+ this.$emit('confirm', cleanedSchemaItem)
715
+ },
716
+
717
+ /**
718
+ * Set the result of the save operation. Call this from the parent
719
+ * after the API call completes.
720
+ *
721
+ * @param {{ success?: boolean, error?: string }} resultData - result data
722
+ * @public
723
+ */
724
+ setResult(resultData) {
725
+ this.$refs.dialog.setResult(resultData)
726
+ if (resultData.success) {
727
+ this.originalProperties = JSON.parse(JSON.stringify(this.schemaItem.properties || {}))
728
+ this.propertiesModified = false
729
+ }
730
+ },
731
+
732
+ migratePropertyToNewStructure(key) {
733
+ if (!this.schemaItem.properties[key]) return
734
+
735
+ const property = this.schemaItem.properties[key]
736
+
737
+ if (property.$ref && property.register && !property.objectConfiguration?.register) {
738
+ if (!property.objectConfiguration) {
739
+ this.$set(this.schemaItem.properties[key], 'objectConfiguration', { handling: 'related-object' })
740
+ }
741
+
742
+ const registerId = typeof property.register === 'object' && property.register.id
743
+ ? property.register.id
744
+ : property.register
745
+
746
+ this.$set(this.schemaItem.properties[key].objectConfiguration, 'register', registerId)
747
+
748
+ if (property.$ref) {
749
+ let schemaSlug = property.$ref
750
+ if (schemaSlug.includes('/')) {
751
+ schemaSlug = schemaSlug.substring(schemaSlug.lastIndexOf('/') + 1)
752
+ }
753
+
754
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
755
+ if (referencedSchema) {
756
+ this.$set(this.schemaItem.properties[key].objectConfiguration, 'schema', referencedSchema.id)
757
+ }
758
+ }
759
+ }
760
+
761
+ if (property.items && property.items.$ref && property.items.register && !property.items.objectConfiguration?.register) {
762
+ if (!property.items.objectConfiguration) {
763
+ this.$set(this.schemaItem.properties[key].items, 'objectConfiguration', { handling: 'related-object' })
764
+ }
765
+
766
+ const registerId = typeof property.items.register === 'object' && property.items.register.id
767
+ ? property.items.register.id
768
+ : property.items.register
769
+
770
+ this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'register', registerId)
771
+
772
+ if (property.items.$ref) {
773
+ let schemaSlug = property.items.$ref
774
+ if (schemaSlug.includes('/')) {
775
+ schemaSlug = schemaSlug.substring(schemaSlug.lastIndexOf('/') + 1)
776
+ }
777
+
778
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
779
+ if (referencedSchema) {
780
+ this.$set(this.schemaItem.properties[key].items.objectConfiguration, 'schema', referencedSchema.id)
781
+ }
782
+ }
783
+ }
784
+ },
785
+ },
786
+ }
787
+ </script>