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

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