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

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 (153) hide show
  1. package/README.md +226 -226
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +60455 -8755
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +2062 -528
  6. package/dist/nextcloud-vue.esm.js +60411 -8731
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +75 -61
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  10. package/src/components/CnActionsBar/index.js +1 -1
  11. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  12. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  13. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  16. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  17. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  18. package/src/components/CnCardGrid/index.js +1 -1
  19. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  24. package/src/components/CnConfigurationCard/index.js +1 -1
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  32. package/src/components/CnDetailCard/index.js +1 -0
  33. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  34. package/src/components/CnDetailPage/index.js +1 -0
  35. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  36. package/src/components/CnFacetSidebar/index.js +1 -1
  37. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnIcon/CnIcon.vue +89 -89
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  48. package/src/components/CnKpiGrid/index.js +1 -1
  49. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  50. package/src/components/CnMassActionBar/index.js +1 -1
  51. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  52. package/src/components/CnMassCopyDialog/index.js +1 -1
  53. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  54. package/src/components/CnMassDeleteDialog/index.js +1 -1
  55. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  56. package/src/components/CnMassExportDialog/index.js +1 -1
  57. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  58. package/src/components/CnMassImportDialog/index.js +1 -1
  59. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  60. package/src/components/CnNoteCard/index.js +1 -0
  61. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  62. package/src/components/CnNotesCard/index.js +1 -0
  63. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  64. package/src/components/CnObjectCard/index.js +1 -1
  65. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  66. package/src/components/CnObjectSidebar/index.js +1 -0
  67. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  68. package/src/components/CnPageHeader/index.js +1 -1
  69. package/src/components/CnPagination/CnPagination.vue +252 -252
  70. package/src/components/CnPagination/index.js +1 -1
  71. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  72. package/src/components/CnRowActions/index.js +1 -1
  73. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  77. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  78. package/src/components/CnSchemaFormDialog/index.js +1 -0
  79. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  80. package/src/components/CnSettingsCard/index.js +1 -1
  81. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  82. package/src/components/CnSettingsSection/index.js +1 -1
  83. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  84. package/src/components/CnStatsBlock/index.js +1 -1
  85. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  86. package/src/components/CnStatusBadge/index.js +1 -1
  87. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  88. package/src/components/CnTabbedFormDialog/index.js +1 -0
  89. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  90. package/src/components/CnTasksCard/index.js +1 -0
  91. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  92. package/src/components/CnTileWidget/index.js +1 -0
  93. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  94. package/src/components/CnTimelineStages/index.js +1 -0
  95. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  96. package/src/components/CnUserActionMenu/index.js +1 -0
  97. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  98. package/src/components/CnVersionInfoCard/index.js +1 -1
  99. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  100. package/src/components/CnWidgetRenderer/index.js +1 -0
  101. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  102. package/src/components/CnWidgetWrapper/index.js +1 -0
  103. package/src/components/index.js +43 -29
  104. package/src/composables/index.js +4 -3
  105. package/src/composables/useDashboardView.js +240 -0
  106. package/src/composables/useDetailView.js +289 -132
  107. package/src/composables/useListView.js +363 -153
  108. package/src/composables/useSubResource.js +142 -142
  109. package/src/constants/metadata.js +30 -30
  110. package/src/css/CnSchemaFormDialog.css +546 -0
  111. package/src/css/__sample_nextcloud_tokens.css +110 -0
  112. package/src/css/actions-bar.css +48 -48
  113. package/src/css/badge.css +51 -51
  114. package/src/css/card.css +128 -128
  115. package/src/css/dashboard.css +70 -0
  116. package/src/css/detail-page.css +168 -0
  117. package/src/css/detail.css +68 -68
  118. package/src/css/index-page.css +44 -32
  119. package/src/css/index-sidebar.css +193 -187
  120. package/src/css/index.css +16 -12
  121. package/src/css/layout.css +90 -90
  122. package/src/css/page-header.css +33 -33
  123. package/src/css/pagination.css +72 -72
  124. package/src/css/table.css +142 -142
  125. package/src/css/timeline-stages.css +218 -0
  126. package/src/css/utilities.css +46 -46
  127. package/src/index.js +72 -53
  128. package/src/store/createSubResourcePlugin.js +135 -135
  129. package/src/store/index.js +3 -3
  130. package/src/store/plugins/auditTrails.js +17 -17
  131. package/src/store/plugins/files.js +250 -186
  132. package/src/store/plugins/index.js +7 -5
  133. package/src/store/plugins/lifecycle.js +180 -180
  134. package/src/store/plugins/relations.js +68 -68
  135. package/src/store/plugins/search.js +372 -0
  136. package/src/store/plugins/selection.js +104 -0
  137. package/src/store/useObjectStore.js +829 -686
  138. package/src/types/auditTrail.d.ts +32 -32
  139. package/src/types/file.d.ts +23 -23
  140. package/src/types/index.d.ts +35 -35
  141. package/src/types/notification.d.ts +36 -36
  142. package/src/types/object.d.ts +40 -40
  143. package/src/types/organisation.d.ts +41 -41
  144. package/src/types/register.d.ts +25 -25
  145. package/src/types/schema.d.ts +39 -39
  146. package/src/types/shared.d.ts +79 -79
  147. package/src/types/source.d.ts +14 -14
  148. package/src/types/task.d.ts +31 -31
  149. package/src/utils/errors.js +96 -96
  150. package/src/utils/headers.js +68 -50
  151. package/src/utils/id.js +13 -0
  152. package/src/utils/index.js +3 -3
  153. package/src/utils/schema.js +422 -419
@@ -0,0 +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>