@conduction/nextcloud-vue 0.1.0-beta.3 → 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 (152) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs.js +60455 -8755
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +2062 -528
  5. package/dist/nextcloud-vue.esm.js +60411 -8731
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +75 -62
  8. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  9. package/src/components/CnActionsBar/index.js +1 -1
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  15. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  16. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  17. package/src/components/CnCardGrid/index.js +1 -1
  18. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  19. package/src/components/CnCellRenderer/index.js +1 -1
  20. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  21. package/src/components/CnChartWidget/index.js +1 -0
  22. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  25. package/src/components/CnDashboardGrid/index.js +1 -0
  26. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  27. package/src/components/CnDashboardPage/index.js +1 -0
  28. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  29. package/src/components/CnDataTable/index.js +1 -1
  30. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  31. package/src/components/CnDetailCard/index.js +1 -0
  32. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  33. package/src/components/CnDetailPage/index.js +1 -0
  34. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  35. package/src/components/CnFacetSidebar/index.js +1 -1
  36. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  37. package/src/components/CnFilterBar/index.js +1 -1
  38. package/src/components/CnIcon/CnIcon.vue +89 -89
  39. package/src/components/CnIcon/index.js +1 -1
  40. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  41. package/src/components/CnIndexPage/index.js +1 -1
  42. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  43. package/src/components/CnIndexSidebar/index.js +1 -1
  44. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  45. package/src/components/CnItemCard/index.js +1 -0
  46. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  49. package/src/components/CnMassActionBar/index.js +1 -1
  50. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  51. package/src/components/CnMassCopyDialog/index.js +1 -1
  52. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  53. package/src/components/CnMassDeleteDialog/index.js +1 -1
  54. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  55. package/src/components/CnMassExportDialog/index.js +1 -1
  56. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  57. package/src/components/CnMassImportDialog/index.js +1 -1
  58. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  59. package/src/components/CnNoteCard/index.js +1 -0
  60. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  61. package/src/components/CnNotesCard/index.js +1 -0
  62. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  63. package/src/components/CnObjectCard/index.js +1 -1
  64. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  65. package/src/components/CnObjectSidebar/index.js +1 -0
  66. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  67. package/src/components/CnPageHeader/index.js +1 -1
  68. package/src/components/CnPagination/CnPagination.vue +252 -252
  69. package/src/components/CnPagination/index.js +1 -1
  70. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  71. package/src/components/CnRowActions/index.js +1 -1
  72. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  73. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  77. package/src/components/CnSchemaFormDialog/index.js +1 -0
  78. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  79. package/src/components/CnSettingsCard/index.js +1 -1
  80. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  81. package/src/components/CnSettingsSection/index.js +1 -1
  82. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  83. package/src/components/CnStatsBlock/index.js +1 -1
  84. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  85. package/src/components/CnStatusBadge/index.js +1 -1
  86. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  87. package/src/components/CnTabbedFormDialog/index.js +1 -0
  88. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  89. package/src/components/CnTasksCard/index.js +1 -0
  90. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  91. package/src/components/CnTileWidget/index.js +1 -0
  92. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  93. package/src/components/CnTimelineStages/index.js +1 -0
  94. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  95. package/src/components/CnUserActionMenu/index.js +1 -0
  96. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  97. package/src/components/CnVersionInfoCard/index.js +1 -1
  98. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  99. package/src/components/CnWidgetRenderer/index.js +1 -0
  100. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  101. package/src/components/CnWidgetWrapper/index.js +1 -0
  102. package/src/components/index.js +43 -29
  103. package/src/composables/index.js +4 -3
  104. package/src/composables/useDashboardView.js +240 -0
  105. package/src/composables/useDetailView.js +289 -132
  106. package/src/composables/useListView.js +363 -362
  107. package/src/composables/useSubResource.js +142 -142
  108. package/src/constants/metadata.js +30 -30
  109. package/src/css/CnSchemaFormDialog.css +546 -0
  110. package/src/css/__sample_nextcloud_tokens.css +110 -0
  111. package/src/css/actions-bar.css +48 -48
  112. package/src/css/badge.css +51 -51
  113. package/src/css/card.css +128 -128
  114. package/src/css/dashboard.css +70 -0
  115. package/src/css/detail-page.css +168 -0
  116. package/src/css/detail.css +68 -68
  117. package/src/css/index-page.css +44 -32
  118. package/src/css/index-sidebar.css +193 -187
  119. package/src/css/index.css +16 -12
  120. package/src/css/layout.css +90 -90
  121. package/src/css/page-header.css +33 -33
  122. package/src/css/pagination.css +72 -72
  123. package/src/css/table.css +142 -142
  124. package/src/css/timeline-stages.css +218 -0
  125. package/src/css/utilities.css +46 -46
  126. package/src/index.js +72 -53
  127. package/src/store/createSubResourcePlugin.js +135 -135
  128. package/src/store/index.js +3 -3
  129. package/src/store/plugins/auditTrails.js +17 -17
  130. package/src/store/plugins/files.js +250 -186
  131. package/src/store/plugins/index.js +7 -5
  132. package/src/store/plugins/lifecycle.js +180 -180
  133. package/src/store/plugins/relations.js +68 -68
  134. package/src/store/plugins/search.js +372 -0
  135. package/src/store/plugins/selection.js +104 -0
  136. package/src/store/useObjectStore.js +829 -686
  137. package/src/types/auditTrail.d.ts +32 -32
  138. package/src/types/file.d.ts +23 -23
  139. package/src/types/index.d.ts +35 -35
  140. package/src/types/notification.d.ts +36 -36
  141. package/src/types/object.d.ts +40 -40
  142. package/src/types/organisation.d.ts +41 -41
  143. package/src/types/register.d.ts +25 -25
  144. package/src/types/schema.d.ts +39 -39
  145. package/src/types/shared.d.ts +79 -79
  146. package/src/types/source.d.ts +14 -14
  147. package/src/types/task.d.ts +31 -31
  148. package/src/utils/errors.js +96 -96
  149. package/src/utils/headers.js +68 -50
  150. package/src/utils/id.js +13 -0
  151. package/src/utils/index.js +3 -3
  152. package/src/utils/schema.js +422 -419
@@ -0,0 +1,1398 @@
1
+ <template>
2
+ <NcActions>
3
+ <NcActionCaption name="Actions" />
4
+ <NcActionButton :aria-label="'Copy ' + propertyKey" @click="$emit('copy-property', propertyKey)">
5
+ <template #icon>
6
+ <ContentCopy :size="16" />
7
+ </template>
8
+ Copy Property
9
+ </NcActionButton>
10
+ <NcActionButton :aria-label="'Delete ' + propertyKey" @click="$emit('delete-property', propertyKey)">
11
+ <template #icon>
12
+ <TrashCanOutline :size="16" />
13
+ </template>
14
+ Delete Property
15
+ </NcActionButton>
16
+
17
+ <NcActionSeparator />
18
+ <NcActionCaption name="General" />
19
+ <NcActionCheckbox
20
+ :checked="isPropertyRequired(schema, propertyKey)"
21
+ @update:checked="updatePropertyRequired(propertyKey, $event)">
22
+ Required
23
+ </NcActionCheckbox>
24
+ <NcActionCheckbox
25
+ :checked="property.immutable || false"
26
+ @update:checked="updatePropertySetting(propertyKey, 'immutable', $event)">
27
+ Immutable
28
+ </NcActionCheckbox>
29
+ <NcActionCheckbox
30
+ :checked="property.deprecated || false"
31
+ @update:checked="updatePropertySetting(propertyKey, 'deprecated', $event)">
32
+ Deprecated
33
+ </NcActionCheckbox>
34
+ <NcActionCheckbox
35
+ :checked="property.visible !== false"
36
+ @update:checked="updatePropertySetting(propertyKey, 'visible', $event)">
37
+ Visible to end users
38
+ </NcActionCheckbox>
39
+ <NcActionCheckbox
40
+ :checked="property.hideOnCollection || false"
41
+ @update:checked="updatePropertySetting(propertyKey, 'hideOnCollection', $event)">
42
+ Hide in collection view
43
+ </NcActionCheckbox>
44
+ <NcActionCheckbox
45
+ :checked="property.hideOnForm || false"
46
+ @update:checked="updatePropertySetting(propertyKey, 'hideOnForm', $event)">
47
+ Hide in form view
48
+ </NcActionCheckbox>
49
+ <NcActionCheckbox
50
+ :checked="isFacetableEnabled(property)"
51
+ @update:checked="toggleFacetable(propertyKey, $event)">
52
+ Facetable
53
+ </NcActionCheckbox>
54
+ <NcActionCheckbox
55
+ v-if="isFacetableEnabled(property)"
56
+ :checked="getFacetConfig(property).aggregated !== false"
57
+ @update:checked="updateFacetConfigField(propertyKey, property, 'aggregated', $event)">
58
+ Aggregated across schemas
59
+ </NcActionCheckbox>
60
+ <NcActionInput
61
+ v-if="isFacetableEnabled(property)"
62
+ :value="getFacetConfig(property).title || ''"
63
+ label="Facet Title"
64
+ @update:value="updateFacetConfigField(propertyKey, property, 'title', $event)" />
65
+ <NcActionInput
66
+ v-if="isFacetableEnabled(property)"
67
+ :value="getFacetConfig(property).description || ''"
68
+ label="Facet Description"
69
+ @update:value="updateFacetConfigField(propertyKey, property, 'description', $event)" />
70
+ <NcActionInput
71
+ v-if="isFacetableEnabled(property)"
72
+ :value="getFacetConfig(property).order != null ? String(getFacetConfig(property).order) : ''"
73
+ type="number"
74
+ label="Facet Order"
75
+ @update:value="updateFacetConfigField(propertyKey, property, 'order', $event)" />
76
+
77
+ <NcActionSeparator />
78
+ <NcActionCaption name="Properties" />
79
+ <NcActionInput
80
+ :value="property.title || ''"
81
+ label="Title"
82
+ @update:value="updatePropertySetting(propertyKey, 'title', $event)" />
83
+ <NcActionInput
84
+ v-if="getFormatOptionsForType(property.type).length > 0"
85
+ v-model="schema.properties[propertyKey].format"
86
+ type="multiselect"
87
+ :options="getFormatOptionsForType(property.type)"
88
+ input-label="Format"
89
+ label="Format" />
90
+ <NcActionInput
91
+ :value="property.description || ''"
92
+ label="Description"
93
+ @update:value="updatePropertySetting(propertyKey, 'description', $event)" />
94
+ <NcActionInput
95
+ :value="property.example || ''"
96
+ label="Example"
97
+ @update:value="updatePropertySetting(propertyKey, 'example', $event)" />
98
+ <NcActionInput
99
+ :value="property.order || 0"
100
+ type="number"
101
+ label="Order"
102
+ @update:value="updatePropertySetting(propertyKey, 'order', Number($event))" />
103
+
104
+ <!-- Const and Enum Configuration -->
105
+ <NcActionSeparator />
106
+ <NcActionCaption name="Value Constraints" />
107
+ <NcActionInput
108
+ :value="property.const || ''"
109
+ label="Constant"
110
+ @update:value="updatePropertySetting(propertyKey, 'const', $event === '' ? undefined : $event)" />
111
+ <template v-if="property.enum && property.enum.length > 0">
112
+ <NcActionCaption :name="'Current Enum Values (' + property.enum.length + ')'" />
113
+ <NcActionButton
114
+ v-for="(enumValue, index) in property.enum"
115
+ :key="`enum-chip-${index}-${enumValue}`"
116
+ :aria-label="'Remove ' + enumValue"
117
+ class="cn-schema-form__enum-action-chip"
118
+ @click="removeEnumValue(propertyKey, index)">
119
+ <template #icon>
120
+ <Close :size="16" />
121
+ </template>
122
+ {{ String(enumValue) }}
123
+ </NcActionButton>
124
+ </template>
125
+ <NcActionInput
126
+ :value="enumInputValue"
127
+ label="Add Enum Value"
128
+ placeholder="Type value and press Enter"
129
+ @update:value="enumInputValue = $event"
130
+ @keydown.enter.prevent="addEnumValueAndClear(propertyKey)" />
131
+
132
+ <!-- Default Value Configuration -->
133
+ <NcActionSeparator />
134
+ <NcActionCaption name="Default Value Configuration" />
135
+ <template v-if="property.type === 'string'">
136
+ <NcActionInput
137
+ :value="property.default || ''"
138
+ label="Default Value"
139
+ @update:value="updatePropertySetting(propertyKey, 'default', $event === '' ? undefined : $event)" />
140
+ </template>
141
+ <template v-else-if="property.type === 'number' || property.type === 'integer'">
142
+ <NcActionInput
143
+ :value="property.default || 0"
144
+ type="number"
145
+ label="Default Value"
146
+ @update:value="updatePropertySetting(propertyKey, 'default', Number($event))" />
147
+ </template>
148
+ <template v-else-if="property.type === 'boolean'">
149
+ <NcActionCheckbox
150
+ :checked="property.default === true"
151
+ @update:checked="updatePropertySetting(propertyKey, 'default', $event)">
152
+ Default Value
153
+ </NcActionCheckbox>
154
+ </template>
155
+ <template v-else-if="property.type === 'array' && property.items && property.items.type === 'string'">
156
+ <NcActionInput
157
+ :value="getArrayDefaultAsString(property.default)"
158
+ label="Default Values (comma separated)"
159
+ placeholder="value1, value2, value3"
160
+ @update:value="updateArrayDefault(propertyKey, $event)" />
161
+ </template>
162
+ <template v-else-if="property.type === 'object'">
163
+ <NcActionInput
164
+ :value="typeof property.default === 'object' ? JSON.stringify(property.default, null, 2) : (property.default || '{}')"
165
+ label="Default Value (JSON)"
166
+ @update:value="updateObjectDefault(propertyKey, $event)" />
167
+ </template>
168
+
169
+ <!-- Default Behavior Toggle -->
170
+ <template v-if="property.default !== undefined && property.default !== null && property.default !== ''">
171
+ <NcActionCheckbox
172
+ :checked="property.defaultBehavior === 'falsy'"
173
+ @update:checked="updatePropertySetting(propertyKey, 'defaultBehavior', $event ? 'falsy' : 'false')">
174
+ Apply default for empty values
175
+ </NcActionCheckbox>
176
+ <NcActionCaption
177
+ v-if="property.defaultBehavior === 'falsy'"
178
+ name="ℹ️ Default will be applied when value is missing, null, or empty string"
179
+ style="color: var(--color-text-lighter); font-size: 11px;" />
180
+ <NcActionCaption
181
+ v-else
182
+ name="ℹ️ Default will only be applied when value is missing or null"
183
+ style="color: var(--color-text-lighter); font-size: 11px;" />
184
+ </template>
185
+
186
+ <!-- Type-specific configurations -->
187
+ <template v-if="property.type === 'string'">
188
+ <NcActionSeparator />
189
+ <NcActionCaption name="String Configuration" />
190
+ <NcActionInput
191
+ :value="property.minLength || 0"
192
+ type="number"
193
+ label="Minimum Length"
194
+ @update:value="updatePropertySetting(propertyKey, 'minLength', Number($event))" />
195
+ <NcActionInput
196
+ :value="property.maxLength || 0"
197
+ type="number"
198
+ label="Maximum Length"
199
+ @update:value="updatePropertySetting(propertyKey, 'maxLength', Number($event))" />
200
+ <NcActionInput
201
+ :value="property.pattern || ''"
202
+ label="Pattern (regex)"
203
+ @update:value="updatePropertySetting(propertyKey, 'pattern', $event)" />
204
+ </template>
205
+
206
+ <template v-if="property.type === 'number' || property.type === 'integer'">
207
+ <NcActionSeparator />
208
+ <NcActionCaption name="Number Configuration" />
209
+ <NcActionInput
210
+ :value="property.minimum || 0"
211
+ type="number"
212
+ label="Minimum Value"
213
+ @update:value="updatePropertySetting(propertyKey, 'minimum', Number($event))" />
214
+ <NcActionInput
215
+ :value="property.maximum || 0"
216
+ type="number"
217
+ label="Maximum Value"
218
+ @update:value="updatePropertySetting(propertyKey, 'maximum', Number($event))" />
219
+ <NcActionInput
220
+ :value="property.multipleOf || 0"
221
+ type="number"
222
+ label="Multiple Of"
223
+ @update:value="updatePropertySetting(propertyKey, 'multipleOf', Number($event))" />
224
+ <NcActionCheckbox
225
+ :checked="property.exclusiveMin || false"
226
+ @update:checked="updatePropertySetting(propertyKey, 'exclusiveMin', $event)">
227
+ Exclusive Minimum
228
+ </NcActionCheckbox>
229
+ <NcActionCheckbox
230
+ :checked="property.exclusiveMax || false"
231
+ @update:checked="updatePropertySetting(propertyKey, 'exclusiveMax', $event)">
232
+ Exclusive Maximum
233
+ </NcActionCheckbox>
234
+ </template>
235
+
236
+ <template v-if="property.type === 'array'">
237
+ <NcActionSeparator />
238
+ <NcActionCaption name="Array Configuration" />
239
+ <NcActionInput
240
+ v-model="schema.properties[propertyKey].items.type"
241
+ type="multiselect"
242
+ :options="[
243
+ { id: 'string', label: 'String' },
244
+ { id: 'number', label: 'Number' },
245
+ { id: 'integer', label: 'Integer' },
246
+ { id: 'object', label: 'Object' },
247
+ { id: 'boolean', label: 'Boolean' },
248
+ { id: 'file', label: 'File' }
249
+ ]"
250
+ input-label="Array Item Type"
251
+ label="Array Item Type" />
252
+ <NcActionInput
253
+ :value="property.minItems || 0"
254
+ type="number"
255
+ label="Minimum Items"
256
+ @update:value="updatePropertySetting(propertyKey, 'minItems', Number($event))" />
257
+ <NcActionInput
258
+ :value="property.maxItems || 0"
259
+ type="number"
260
+ label="Maximum Items"
261
+ @update:value="updatePropertySetting(propertyKey, 'maxItems', Number($event))" />
262
+
263
+ <!-- Show object configuration for array items when item type is object -->
264
+ <template v-if="property.items && property.items.type === 'object'">
265
+ <NcActionSeparator />
266
+ <NcActionCaption name="Array Item Object Configuration" />
267
+ <NcActionInput
268
+ v-model="schema.properties[propertyKey].items.objectConfiguration.handling"
269
+ type="multiselect"
270
+ :options="objectHandlingOptions"
271
+ input-label="Object Handling"
272
+ label="Object Handling" />
273
+ <NcActionInput
274
+ :value="schema.properties[propertyKey].items.$ref"
275
+ type="multiselect"
276
+ :options="availableSchemas"
277
+ input-label="Schema Reference"
278
+ label="Schema Reference"
279
+ @update:value="updateArrayItemSchemaReference(propertyKey, $event)" />
280
+ <NcActionCaption
281
+ v-if="isArrayItemRefInvalid(propertyKey)"
282
+ :name="`⚠️ Invalid Schema Reference: Expected string, got number (${schema.properties[propertyKey].items.$ref}). This will be sent to backend as-is.`"
283
+ style="color: var(--color-error); font-weight: bold;" />
284
+ <NcActionInput
285
+ :value="getArrayItemRegisterValue(propertyKey)"
286
+ type="multiselect"
287
+ :options="availableRegisters"
288
+ input-label="Register"
289
+ label="Register (Required when schema is selected)"
290
+ :required="!!schema.properties[propertyKey].items.$ref"
291
+ :disabled="!schema.properties[propertyKey].items.$ref"
292
+ @update:value="updateArrayItemRegisterReference(propertyKey, $event)" />
293
+ <NcActionInput
294
+ v-model="schema.properties[propertyKey].items.inversedBy"
295
+ type="multiselect"
296
+ :options="getInversedByOptionsForArrayItems(propertyKey)"
297
+ input-label="Inversed By Property"
298
+ label="Inversed By"
299
+ :disabled="!schema.properties[propertyKey].items.$ref"
300
+ @update:value="updateInversedByForArrayItems(propertyKey, $event)" />
301
+ <NcActionInput
302
+ :value="getArrayItemQueryParams(propertyKey)"
303
+ label="Query Parameters"
304
+ placeholder="e.g. gemmaType=referentiecomponent&_extend=aanbevolenStandaarden"
305
+ @update:value="updateArrayItemQueryParams(propertyKey, $event)" />
306
+ <NcActionCheckbox
307
+ :checked="property.items.writeBack || false"
308
+ @update:checked="updateArrayItemObjectConfigurationSetting(propertyKey, 'writeBack', $event)">
309
+ Write Back
310
+ </NcActionCheckbox>
311
+ <NcActionCheckbox
312
+ :checked="property.items.removeAfterWriteBack || false"
313
+ @update:checked="updateArrayItemObjectConfigurationSetting(propertyKey, 'removeAfterWriteBack', $event)">
314
+ Remove After Write Back
315
+ </NcActionCheckbox>
316
+ <NcActionCheckbox
317
+ :checked="property.items.cascadeDelete || false"
318
+ @update:checked="updateArrayItemObjectConfigurationSetting(propertyKey, 'cascadeDelete', $event)">
319
+ Cascade Delete
320
+ </NcActionCheckbox>
321
+ </template>
322
+ </template>
323
+
324
+ <template v-if="property.type === 'object'">
325
+ <NcActionSeparator />
326
+ <NcActionCaption name="Object Configuration" />
327
+ <NcActionInput
328
+ v-model="schema.properties[propertyKey].objectConfiguration.handling"
329
+ type="multiselect"
330
+ :options="objectHandlingOptions"
331
+ input-label="Object Handling"
332
+ label="Object Handling" />
333
+ <NcActionInput
334
+ :value="schema.properties[propertyKey].$ref"
335
+ type="multiselect"
336
+ :options="availableSchemas"
337
+ input-label="Schema Reference"
338
+ label="Schema Reference"
339
+ @update:value="updateSchemaReference(propertyKey, $event)" />
340
+ <NcActionCaption
341
+ v-if="isRefInvalid(propertyKey)"
342
+ :name="`⚠️ Invalid Schema Reference: Expected string, got number (${schema.properties[propertyKey].$ref}). This will be sent to backend as-is.`"
343
+ style="color: var(--color-error); font-weight: bold;" />
344
+ <NcActionInput
345
+ :value="getRegisterValue(propertyKey)"
346
+ type="multiselect"
347
+ :options="availableRegisters"
348
+ input-label="Register"
349
+ label="Register (Required when schema is selected)"
350
+ :required="!!schema.properties[propertyKey].$ref"
351
+ :disabled="!schema.properties[propertyKey].$ref"
352
+ @update:value="updateRegisterReference(propertyKey, $event)" />
353
+ <NcActionInput
354
+ v-model="schema.properties[propertyKey].inversedBy"
355
+ type="multiselect"
356
+ :options="getInversedByOptions(propertyKey)"
357
+ input-label="Inversed By Property"
358
+ label="Inversed By"
359
+ :disabled="!schema.properties[propertyKey].$ref"
360
+ @update:value="updateInversedBy(propertyKey, $event)" />
361
+ <NcActionInput
362
+ :value="getObjectQueryParams(propertyKey)"
363
+ label="Query Parameters"
364
+ placeholder="e.g. gemmaType=referentiecomponent&_extend=aanbevolenStandaarden"
365
+ @update:value="updateObjectQueryParams(propertyKey, $event)" />
366
+ <NcActionCheckbox
367
+ :checked="property.writeBack || false"
368
+ @update:checked="updatePropertySetting(propertyKey, 'writeBack', $event)">
369
+ Write Back
370
+ </NcActionCheckbox>
371
+ <NcActionCheckbox
372
+ :checked="property.removeAfterWriteBack || false"
373
+ @update:checked="updatePropertySetting(propertyKey, 'removeAfterWriteBack', $event)">
374
+ Remove After Write Back
375
+ </NcActionCheckbox>
376
+ <NcActionCheckbox
377
+ :checked="property.cascadeDelete || false"
378
+ @update:checked="updatePropertySetting(propertyKey, 'cascadeDelete', $event)">
379
+ Cascade Delete
380
+ </NcActionCheckbox>
381
+ </template>
382
+
383
+ <!-- File Configuration -->
384
+ <template v-if="property.type === 'file' || (property.type === 'array' && property.items && property.items.type === 'file')">
385
+ <NcActionSeparator />
386
+ <NcActionCaption name="File Configuration" />
387
+ <NcActionCheckbox
388
+ :checked="getFilePropertySetting(propertyKey, 'autoPublish')"
389
+ @update:checked="updateFilePropertySetting(propertyKey, 'autoPublish', $event)">
390
+ Auto-Publish Files
391
+ </NcActionCheckbox>
392
+ <NcActionCaption
393
+ v-if="getFilePropertySetting(propertyKey, 'autoPublish')"
394
+ name="ℹ️ Files uploaded to this property will be automatically publicly shared"
395
+ style="color: var(--color-text-lighter); font-size: 11px;" />
396
+ <NcActionInput
397
+ :value="(property.allowedTypes || []).join(', ')"
398
+ label="Allowed MIME Types (comma separated)"
399
+ placeholder="image/png, image/jpeg, application/pdf"
400
+ @update:value="updateFileProperty(propertyKey, 'allowedTypes', $event)" />
401
+ <NcActionInput
402
+ :value="property.maxSize || ''"
403
+ type="number"
404
+ label="Maximum File Size (bytes)"
405
+ placeholder="5242880"
406
+ @update:value="updateFileProperty(propertyKey, 'maxSize', $event)" />
407
+ <NcActionInput
408
+ :value="getFilePropertyTags(propertyKey, 'allowedTags')"
409
+ type="multiselect"
410
+ :options="availableTagsOptions"
411
+ input-label="Allowed Tags"
412
+ label="Allowed Tags (select from available tags)"
413
+ multiple
414
+ @update:value="updateFilePropertyTags(propertyKey, 'allowedTags', $event)" />
415
+ <NcActionInput
416
+ :value="getFilePropertyTags(propertyKey, 'autoTags')"
417
+ type="multiselect"
418
+ :options="availableTagsOptions"
419
+ input-label="Auto Tags"
420
+ label="Auto Tags (automatically applied to uploaded files)"
421
+ multiple
422
+ @update:value="updateFilePropertyTags(propertyKey, 'autoTags', $event)" />
423
+ </template>
424
+
425
+ <!-- Property-level Table Configuration -->
426
+ <NcActionSeparator />
427
+ <NcActionCaption name="Table" />
428
+ <NcActionCheckbox
429
+ :checked="getPropertyTableSetting(propertyKey, 'default')"
430
+ @update:checked="updatePropertyTableSetting(propertyKey, 'default', $event)">
431
+ Default
432
+ </NcActionCheckbox>
433
+
434
+ <!-- Property-level Security Configuration -->
435
+ <NcActionSeparator />
436
+ <NcActionCaption name="Property Security" />
437
+
438
+ <template v-if="!loadingGroups">
439
+ <!-- Current Property Permissions List -->
440
+ <div v-for="permission in getPropertyPermissionsList(propertyKey)" :key="`${propertyKey}-perm-text-${permission.group}`">
441
+ <NcActionText
442
+ class="cn-schema-form__property-permission-text">
443
+ {{ permission.group }} ({{ permission.rights }})
444
+ </NcActionText>
445
+ <NcActionButton
446
+ v-if="permission.groupId !== 'admin'"
447
+ :key="`${propertyKey}-perm-remove-${permission.group}`"
448
+ :aria-label="`Remove ${permission.group} permissions`"
449
+ class="cn-schema-form__property-permission-remove-btn"
450
+ @click="removePropertyGroupPermissions(propertyKey, permission.group)">
451
+ <template #icon>
452
+ <Close :size="16" />
453
+ </template>
454
+ Remove {{ permission.group }}
455
+ </NcActionButton>
456
+ </div>
457
+
458
+ <!-- Show inheritance status if no specific permissions -->
459
+ <NcActionCaption
460
+ v-if="!hasPropertyAnyPermissions(propertyKey)"
461
+ name="📄 Inherits schema permissions"
462
+ style="color: var(--color-success); font-size: 11px;" />
463
+
464
+ <!-- Add Permission Interface -->
465
+ <NcActionSeparator />
466
+ <NcActionInput
467
+ v-model="propertyNewPermissionGroup"
468
+ type="multiselect"
469
+ :options="getAvailableGroupsForProperty()"
470
+ input-label="Group"
471
+ label="Add Group Permission"
472
+ placeholder="Select group..." />
473
+
474
+ <template v-if="propertyNewPermissionGroup">
475
+ <NcActionCaption name="Select Permissions:" />
476
+ <NcActionCheckbox
477
+ :checked="propertyNewPermissionCreate"
478
+ @update:checked="propertyNewPermissionCreate = $event">
479
+ Create (C)
480
+ </NcActionCheckbox>
481
+ <NcActionCheckbox
482
+ :checked="propertyNewPermissionRead"
483
+ @update:checked="propertyNewPermissionRead = $event">
484
+ Read (R)
485
+ </NcActionCheckbox>
486
+ <NcActionCheckbox
487
+ :checked="propertyNewPermissionUpdate"
488
+ @update:checked="propertyNewPermissionUpdate = $event">
489
+ Update (U)
490
+ </NcActionCheckbox>
491
+ <NcActionCheckbox
492
+ :checked="propertyNewPermissionDelete"
493
+ @update:checked="propertyNewPermissionDelete = $event">
494
+ Delete (D)
495
+ </NcActionCheckbox>
496
+
497
+ <NcActionButton
498
+ v-if="hasAnyPropertyNewPermissionSelected()"
499
+ @click="addPropertyGroupPermissions(propertyKey)">
500
+ <template #icon>
501
+ <Plus :size="16" />
502
+ </template>
503
+ Add Permission
504
+ </NcActionButton>
505
+ </template>
506
+ </template>
507
+ <template v-else>
508
+ <NcActionCaption name="Loading groups..." />
509
+ </template>
510
+ </NcActions>
511
+ </template>
512
+
513
+ <script>
514
+ import {
515
+ NcActions,
516
+ NcActionButton,
517
+ NcActionCheckbox,
518
+ NcActionInput,
519
+ NcActionCaption,
520
+ NcActionSeparator,
521
+ NcActionText,
522
+ } from '@nextcloud/vue'
523
+
524
+ import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
525
+ import TrashCanOutline from 'vue-material-design-icons/TrashCanOutline.vue'
526
+ import Close from 'vue-material-design-icons/Close.vue'
527
+ import Plus from 'vue-material-design-icons/Plus.vue'
528
+
529
+ /**
530
+ * CnSchemaPropertyActions — NcActions dropdown for a single schema property.
531
+ *
532
+ * Renders all configuration sections (General, Properties, Value Constraints,
533
+ * Default Value, type-specific configs, File, Table, Security) inside an
534
+ * NcActions popover menu.
535
+ *
536
+ * Mutates schemaItem directly (passed by reference from parent).
537
+ *
538
+ * @event copy-property Emitted when copy is clicked. Payload: property key.
539
+ * @event delete-property Emitted when delete is clicked. Payload: property key.
540
+ */
541
+ export default {
542
+ name: 'CnSchemaPropertyActions',
543
+ components: {
544
+ NcActions,
545
+ NcActionButton,
546
+ NcActionCheckbox,
547
+ NcActionInput,
548
+ NcActionCaption,
549
+ NcActionSeparator,
550
+ NcActionText,
551
+ ContentCopy,
552
+ TrashCanOutline,
553
+ Close,
554
+ Plus,
555
+ },
556
+ props: {
557
+ /** The property name/key in the schema */
558
+ propertyKey: { type: String, required: true },
559
+ /** The property definition object */
560
+ property: { type: Object, required: true },
561
+ /** Full schemaItem (for direct mutations and required array checks) */
562
+ schemaItem: { type: Object, required: true },
563
+ /** Original properties snapshot for table setting comparison */
564
+ originalProperties: { type: Object, default: () => ({}) },
565
+ /** Available schemas for $ref selects */
566
+ availableSchemas: { type: Array, default: () => [] },
567
+ /** Available registers for register selects */
568
+ availableRegisters: { type: Array, default: () => [] },
569
+ /** Available tags for file property configuration, pre-mapped to [{id, label}] */
570
+ availableTagsOptions: { type: Array, default: () => [] },
571
+ /** User groups for property-level RBAC */
572
+ userGroups: { type: Array, default: () => [] },
573
+ /** Filtered/sorted user groups (excludes admin/public) */
574
+ sortedUserGroups: { type: Array, default: () => [] },
575
+ /** Whether user groups are still loading */
576
+ loadingGroups: { type: Boolean, default: false },
577
+ },
578
+ data() {
579
+ return {
580
+ enumInputValue: '',
581
+ propertyNewPermissionGroup: null,
582
+ propertyNewPermissionCreate: false,
583
+ propertyNewPermissionRead: false,
584
+ propertyNewPermissionUpdate: false,
585
+ propertyNewPermissionDelete: false,
586
+ objectHandlingOptions: [
587
+ { id: 'nested-object', label: 'Nested Object' },
588
+ { id: 'related-object', label: 'Related Object' },
589
+ { id: 'nested-schema', label: 'Nested Schema' },
590
+ { id: 'related-schema', label: 'Related Schema' },
591
+ { id: 'uri', label: 'URI' },
592
+ ],
593
+ }
594
+ },
595
+ computed: {
596
+ /** Local alias to avoid vue/no-mutating-props on template bindings */
597
+ schema() {
598
+ return this.schemaItem
599
+ },
600
+ },
601
+ methods: {
602
+ // --- General helpers ---
603
+
604
+ isPropertyRequired(schema, key) {
605
+ const isInSchemaRequired = schema.required && schema.required.includes(key)
606
+ const hasPropertyRequired = schema.properties && schema.properties[key] && schema.properties[key].required === true
607
+ return isInSchemaRequired || hasPropertyRequired
608
+ },
609
+
610
+ findSchemaBySlug(schemaSlug) {
611
+ if (!schemaSlug) return undefined
612
+ return this.availableSchemas.find(schema =>
613
+ (schema.slug && schema.slug.toLowerCase() === schemaSlug.toLowerCase())
614
+ || schema.id === schemaSlug
615
+ || schema.title === schemaSlug,
616
+ )
617
+ },
618
+
619
+ checkPropertiesModified() {
620
+ // Bubble up to parent — the parent's deep watcher handles this
621
+ // This is a no-op placeholder; modifications are detected by the parent's watcher
622
+ },
623
+
624
+ // --- Property setting updates ---
625
+
626
+ updatePropertySetting(key, setting, value) {
627
+ if (this.schema.properties[key]) {
628
+ const settingValue = typeof value === 'object' && value?.id ? value.id : value
629
+ this.$set(this.schema.properties[key], setting, settingValue)
630
+ this.ensureRefIsString(this.schema.properties, key)
631
+ }
632
+ },
633
+
634
+ updatePropertyRequired(key, isRequired) {
635
+ if (this.schema.properties[key]) {
636
+ if (isRequired) {
637
+ this.$set(this.schema.properties[key], 'required', true)
638
+ } else {
639
+ this.$delete(this.schema.properties[key], 'required')
640
+ }
641
+ }
642
+
643
+ if (!this.schema.required) {
644
+ this.$set(this.schema, 'required', [])
645
+ }
646
+
647
+ const currentRequired = [...this.schema.required]
648
+ if (isRequired && !currentRequired.includes(key)) {
649
+ currentRequired.push(key)
650
+ } else if (!isRequired && currentRequired.includes(key)) {
651
+ const index = currentRequired.indexOf(key)
652
+ currentRequired.splice(index, 1)
653
+ }
654
+
655
+ this.schema.required = currentRequired
656
+ },
657
+
658
+ // --- Facet ---
659
+
660
+ isFacetableEnabled(property) {
661
+ return property.facetable === true
662
+ || (typeof property.facetable === 'object' && property.facetable !== null)
663
+ },
664
+
665
+ getFacetConfig(property) {
666
+ if (typeof property.facetable === 'object' && property.facetable !== null) {
667
+ return {
668
+ aggregated: property.facetable.aggregated ?? true,
669
+ title: property.facetable.title ?? '',
670
+ description: property.facetable.description ?? '',
671
+ order: property.facetable.order ?? null,
672
+ }
673
+ }
674
+ return { aggregated: true, title: '', description: '', order: null }
675
+ },
676
+
677
+ toggleFacetable(key, enabled) {
678
+ if (enabled) {
679
+ this.$set(this.schema.properties[key], 'facetable', true)
680
+ } else {
681
+ this.$set(this.schema.properties[key], 'facetable', false)
682
+ }
683
+ },
684
+
685
+ updateFacetConfigField(key, property, field, value) {
686
+ const config = this.getFacetConfig(property)
687
+
688
+ if (field === 'aggregated') {
689
+ config.aggregated = value
690
+ } else if (field === 'order') {
691
+ config.order = value !== '' && value != null ? Number(value) : null
692
+ } else {
693
+ config[field] = value
694
+ }
695
+
696
+ const hasCustomConfig = !config.aggregated
697
+ || (config.title && config.title.trim())
698
+ || (config.description && config.description.trim())
699
+ || (config.order != null)
700
+
701
+ if (hasCustomConfig) {
702
+ this.$set(this.schema.properties[key], 'facetable', {
703
+ aggregated: config.aggregated,
704
+ title: config.title?.trim() || null,
705
+ description: config.description?.trim() || null,
706
+ order: config.order,
707
+ })
708
+ } else {
709
+ this.$set(this.schema.properties[key], 'facetable', true)
710
+ }
711
+ },
712
+
713
+ // --- Format options ---
714
+
715
+ getFormatOptionsForType(type) {
716
+ const formatMap = {
717
+ string: [
718
+ { id: 'text', label: 'Text' },
719
+ { id: 'markdown', label: 'Markdown' },
720
+ { id: 'html', label: 'HTML' },
721
+ { id: 'date-time', label: 'Date Time' },
722
+ { id: 'date', label: 'Date' },
723
+ { id: 'time', label: 'Time' },
724
+ { id: 'duration', label: 'Duration' },
725
+ { id: 'email', label: 'Email' },
726
+ { id: 'idn-email', label: 'IDN Email' },
727
+ { id: 'hostname', label: 'Hostname' },
728
+ { id: 'idn-hostname', label: 'IDN Hostname' },
729
+ { id: 'ipv4', label: 'IPv4' },
730
+ { id: 'ipv6', label: 'IPv6' },
731
+ { id: 'uri', label: 'URI' },
732
+ { id: 'uri-reference', label: 'URI Reference' },
733
+ { id: 'iri', label: 'IRI' },
734
+ { id: 'iri-reference', label: 'IRI Reference' },
735
+ { id: 'uuid', label: 'UUID' },
736
+ { id: 'uri-template', label: 'URI Template' },
737
+ { id: 'json-pointer', label: 'JSON Pointer' },
738
+ { id: 'relative-json-pointer', label: 'Relative JSON Pointer' },
739
+ { id: 'regex', label: 'Regex' },
740
+ { id: 'binary', label: 'Binary' },
741
+ { id: 'byte', label: 'Byte' },
742
+ { id: 'password', label: 'Password' },
743
+ { id: 'rsin', label: 'RSIN' },
744
+ { id: 'kvk', label: 'KVK' },
745
+ { id: 'bsn', label: 'BSN' },
746
+ { id: 'oidn', label: 'OIDN' },
747
+ { id: 'telephone', label: 'Telephone' },
748
+ { id: 'accessUrl', label: 'Access URL' },
749
+ { id: 'shareUrl', label: 'Share URL' },
750
+ { id: 'downloadUrl', label: 'Download URL' },
751
+ { id: 'extension', label: 'Extension' },
752
+ { id: 'filename', label: 'Filename' },
753
+ { id: 'semver', label: 'Semantic Version' },
754
+ { id: 'url', label: 'URL' },
755
+ { id: 'color', label: 'Color' },
756
+ { id: 'color-hex', label: 'Color Hex' },
757
+ { id: 'color-hex-alpha', label: 'Color Hex Alpha' },
758
+ { id: 'color-rgb', label: 'Color RGB' },
759
+ { id: 'color-rgba', label: 'Color RGBA' },
760
+ { id: 'color-hsl', label: 'Color HSL' },
761
+ { id: 'color-hsla', label: 'Color HSLA' },
762
+ ],
763
+ number: [],
764
+ integer: [],
765
+ boolean: [],
766
+ array: [],
767
+ object: [],
768
+ }
769
+ return formatMap[type] || []
770
+ },
771
+
772
+ // --- Enum ---
773
+
774
+ addEnumValue(key, value) {
775
+ if (!value || !value.trim()) return
776
+
777
+ const trimmedValue = value.trim()
778
+
779
+ if (this.schema.properties[key]) {
780
+ if (!this.schema.properties[key].enum) {
781
+ this.$set(this.schema.properties[key], 'enum', [])
782
+ }
783
+
784
+ if (!this.schema.properties[key].enum.includes(trimmedValue)) {
785
+ const newEnum = [...this.schema.properties[key].enum, trimmedValue]
786
+ this.$set(this.schema.properties[key], 'enum', newEnum)
787
+ }
788
+ }
789
+ },
790
+
791
+ addEnumValueAndClear(key) {
792
+ if (this.enumInputValue && this.enumInputValue.trim()) {
793
+ this.addEnumValue(key, this.enumInputValue)
794
+ this.enumInputValue = ''
795
+ }
796
+ },
797
+
798
+ removeEnumValue(key, index) {
799
+ if (this.schema.properties[key] && this.schema.properties[key].enum) {
800
+ const newEnum = this.schema.properties[key].enum.filter((_, i) => i !== index)
801
+
802
+ if (newEnum.length === 0) {
803
+ this.$delete(this.schema.properties[key], 'enum')
804
+ } else {
805
+ this.$set(this.schema.properties[key], 'enum', newEnum)
806
+ }
807
+ }
808
+ },
809
+
810
+ // --- Default values ---
811
+
812
+ getArrayDefaultAsString(defaultValue) {
813
+ if (!defaultValue || !Array.isArray(defaultValue)) {
814
+ return ''
815
+ }
816
+ return defaultValue.join(', ')
817
+ },
818
+
819
+ updateArrayDefault(key, value) {
820
+ if (!this.schema.properties[key]) return
821
+
822
+ if (!value || value.trim() === '') {
823
+ this.$set(this.schema.properties[key], 'default', undefined)
824
+ } else {
825
+ const arrayValues = value.split(',').map(item => item.trim()).filter(item => item !== '')
826
+ this.$set(this.schema.properties[key], 'default', arrayValues)
827
+ }
828
+ },
829
+
830
+ updateObjectDefault(key, value) {
831
+ if (!this.schema.properties[key]) return
832
+
833
+ if (!value || value.trim() === '' || value.trim() === '{}') {
834
+ this.$set(this.schema.properties[key], 'default', undefined)
835
+ return
836
+ }
837
+
838
+ try {
839
+ const parsedValue = JSON.parse(value)
840
+ this.$set(this.schema.properties[key], 'default', parsedValue)
841
+ } catch (e) {
842
+ console.warn('Invalid JSON for default value:', e.message)
843
+ }
844
+ },
845
+
846
+ // --- Schema references ---
847
+
848
+ ensureRefIsString(obj, key) {
849
+ if (!obj || !key) return
850
+
851
+ if (obj[key] && typeof obj[key].$ref === 'object' && obj[key].$ref !== null) {
852
+ if (obj[key].$ref.id) {
853
+ obj[key].$ref = obj[key].$ref.id
854
+ } else {
855
+ obj[key].$ref = ''
856
+ }
857
+ }
858
+
859
+ if (obj[key] && obj[key].items && typeof obj[key].items.$ref === 'object' && obj[key].items.$ref !== null) {
860
+ if (obj[key].items.$ref.id) {
861
+ obj[key].items.$ref = obj[key].items.$ref.id
862
+ } else {
863
+ obj[key].items.$ref = ''
864
+ }
865
+ }
866
+ },
867
+
868
+ isRefInvalid(key) {
869
+ const property = this.schema.properties[key]
870
+ if (!property || !property.$ref) return false
871
+ const rawRef = typeof property.$ref === 'object' ? property.$ref.id : property.$ref
872
+ return typeof rawRef === 'number'
873
+ },
874
+
875
+ isArrayItemRefInvalid(key) {
876
+ const property = this.schema.properties[key]
877
+ if (!property || !property.items || !property.items.$ref) return false
878
+ const rawRef = typeof property.items.$ref === 'object' ? property.items.$ref.id : property.items.$ref
879
+ return typeof rawRef === 'number'
880
+ },
881
+
882
+ updateSchemaReference(key, value) {
883
+ if (!this.schema.properties[key]) return
884
+
885
+ const schemaRef = typeof value === 'object' && value?.id ? value.id : value
886
+ this.$set(this.schema.properties[key], '$ref', schemaRef)
887
+
888
+ if (!this.schema.properties[key].objectConfiguration) {
889
+ this.$set(this.schema.properties[key], 'objectConfiguration', { handling: 'related-object' })
890
+ }
891
+
892
+ if (schemaRef) {
893
+ let schemaSlug = schemaRef
894
+ if (schemaRef.includes('/')) {
895
+ schemaSlug = schemaRef.substring(schemaRef.lastIndexOf('/') + 1)
896
+ }
897
+
898
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
899
+ if (referencedSchema) {
900
+ this.$set(this.schema.properties[key].objectConfiguration, 'schema', referencedSchema.id)
901
+ }
902
+
903
+ if (this.schema.properties[key].register && !this.schema.properties[key].objectConfiguration.register) {
904
+ const oldRegister = this.schema.properties[key].register
905
+ const registerId = typeof oldRegister === 'object' && oldRegister.id ? oldRegister.id : oldRegister
906
+ this.$set(this.schema.properties[key].objectConfiguration, 'register', registerId)
907
+ }
908
+ } else {
909
+ this.$delete(this.schema.properties[key].objectConfiguration, 'schema')
910
+ this.$delete(this.schema.properties[key].objectConfiguration, 'register')
911
+ }
912
+ },
913
+
914
+ updateArrayItemSchemaReference(key, value) {
915
+ if (!this.schema.properties[key] || !this.schema.properties[key].items) return
916
+
917
+ const schemaRef = typeof value === 'object' && value?.id ? value.id : value
918
+ this.$set(this.schema.properties[key].items, '$ref', schemaRef)
919
+
920
+ if (!this.schema.properties[key].items.objectConfiguration) {
921
+ this.$set(this.schema.properties[key].items, 'objectConfiguration', { handling: 'related-object' })
922
+ }
923
+
924
+ if (schemaRef) {
925
+ let schemaSlug = schemaRef
926
+ if (schemaRef.includes('/')) {
927
+ schemaSlug = schemaRef.substring(schemaRef.lastIndexOf('/') + 1)
928
+ }
929
+
930
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
931
+ if (referencedSchema) {
932
+ this.$set(this.schema.properties[key].items.objectConfiguration, 'schema', referencedSchema.id)
933
+ }
934
+ } else {
935
+ this.$delete(this.schema.properties[key].items.objectConfiguration, 'schema')
936
+ this.$delete(this.schema.properties[key].items.objectConfiguration, 'register')
937
+ }
938
+ },
939
+
940
+ // --- Register references ---
941
+
942
+ updateRegisterReference(key, value) {
943
+ if (!this.schema.properties[key]) return
944
+
945
+ if (!this.schema.properties[key].objectConfiguration) {
946
+ this.$set(this.schema.properties[key], 'objectConfiguration', { handling: 'related-object' })
947
+ }
948
+
949
+ const registerId = typeof value === 'object' && value?.id ? value.id : value
950
+
951
+ if (registerId) {
952
+ this.$set(this.schema.properties[key].objectConfiguration, 'register', registerId)
953
+ if (this.schema.properties[key].register) {
954
+ this.$delete(this.schema.properties[key], 'register')
955
+ }
956
+ } else {
957
+ this.$delete(this.schema.properties[key].objectConfiguration, 'register')
958
+ }
959
+ },
960
+
961
+ updateArrayItemRegisterReference(key, value) {
962
+ if (!this.schema.properties[key] || !this.schema.properties[key].items) return
963
+
964
+ if (!this.schema.properties[key].items.objectConfiguration) {
965
+ this.$set(this.schema.properties[key].items, 'objectConfiguration', { handling: 'related-object' })
966
+ }
967
+
968
+ const registerId = typeof value === 'object' && value?.id ? value.id : value
969
+
970
+ if (registerId) {
971
+ this.$set(this.schema.properties[key].items.objectConfiguration, 'register', registerId)
972
+ } else {
973
+ this.$delete(this.schema.properties[key].items.objectConfiguration, 'register')
974
+ }
975
+ },
976
+
977
+ getRegisterValue(key) {
978
+ if (!this.schema.properties[key]) return null
979
+
980
+ const property = this.schema.properties[key]
981
+ if (property.objectConfiguration && property.objectConfiguration.register !== undefined) {
982
+ return property.objectConfiguration.register
983
+ }
984
+ if (property.register !== undefined) {
985
+ return property.register
986
+ }
987
+ return null
988
+ },
989
+
990
+ getArrayItemRegisterValue(key) {
991
+ if (!this.schema.properties[key] || !this.schema.properties[key].items) return null
992
+
993
+ const items = this.schema.properties[key].items
994
+ if (items.objectConfiguration && items.objectConfiguration.register !== undefined) {
995
+ return items.objectConfiguration.register
996
+ }
997
+ if (items.register !== undefined) {
998
+ return items.register
999
+ }
1000
+ return null
1001
+ },
1002
+
1003
+ // --- InversedBy ---
1004
+
1005
+ getInversedByOptions(key) {
1006
+ const property = this.schema.properties[key]
1007
+ if (!property || !property.$ref) return []
1008
+
1009
+ const rawRef = typeof property.$ref === 'object' ? property.$ref.id : property.$ref
1010
+ if (typeof rawRef === 'number') return []
1011
+
1012
+ const schemaRef = String(rawRef)
1013
+ let schemaSlug = schemaRef
1014
+ if (schemaRef.includes('/')) {
1015
+ schemaSlug = schemaRef.substring(schemaRef.lastIndexOf('/') + 1)
1016
+ }
1017
+
1018
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
1019
+ if (!referencedSchema || !referencedSchema.properties) return []
1020
+
1021
+ return Object.keys(referencedSchema.properties).map(propKey => ({
1022
+ id: propKey,
1023
+ label: referencedSchema.properties[propKey].title || propKey,
1024
+ }))
1025
+ },
1026
+
1027
+ getInversedByOptionsForArrayItems(key) {
1028
+ const property = this.schema.properties[key]
1029
+ if (!property || !property.items || !property.items.$ref) return []
1030
+
1031
+ const rawRef = typeof property.items.$ref === 'object' ? property.items.$ref.id : property.items.$ref
1032
+ if (typeof rawRef === 'number') return []
1033
+
1034
+ const schemaRef = String(rawRef)
1035
+ let schemaSlug = schemaRef
1036
+ if (schemaRef.includes('/')) {
1037
+ schemaSlug = schemaRef.substring(schemaRef.lastIndexOf('/') + 1)
1038
+ }
1039
+
1040
+ const referencedSchema = this.findSchemaBySlug(schemaSlug)
1041
+ if (!referencedSchema || !referencedSchema.properties) return []
1042
+
1043
+ return Object.keys(referencedSchema.properties).map(propKey => ({
1044
+ id: propKey,
1045
+ label: referencedSchema.properties[propKey].title || propKey,
1046
+ }))
1047
+ },
1048
+
1049
+ updateInversedBy(key, value) {
1050
+ if (this.schema.properties[key]) {
1051
+ const inversedByValue = typeof value === 'object' && value?.id ? value.id : value
1052
+ this.$set(this.schema.properties[key], 'inversedBy', inversedByValue)
1053
+ }
1054
+ },
1055
+
1056
+ updateInversedByForArrayItems(key, value) {
1057
+ if (this.schema.properties[key] && this.schema.properties[key].items) {
1058
+ const inversedByValue = typeof value === 'object' && value?.id ? value.id : value
1059
+ this.$set(this.schema.properties[key].items, 'inversedBy', inversedByValue)
1060
+ }
1061
+ },
1062
+
1063
+ // --- Array item object config ---
1064
+
1065
+ updateArrayItemObjectConfigurationSetting(key, setting, value) {
1066
+ if (this.schema.properties[key]) {
1067
+ if (!this.schema.properties[key].items) {
1068
+ this.$set(this.schema.properties[key], 'items', {})
1069
+ }
1070
+ if (!this.schema.properties[key].items.objectConfiguration) {
1071
+ this.$set(this.schema.properties[key].items, 'objectConfiguration', {})
1072
+ }
1073
+ const settingValue = typeof value === 'object' && value?.id ? value.id : value
1074
+ this.$set(this.schema.properties[key].items, setting, settingValue)
1075
+ }
1076
+ },
1077
+
1078
+ // --- Query params ---
1079
+
1080
+ getObjectQueryParams(key) {
1081
+ if (!this.schema.properties[key] || !this.schema.properties[key].objectConfiguration) return ''
1082
+ return this.schema.properties[key].objectConfiguration.queryParams || ''
1083
+ },
1084
+
1085
+ updateObjectQueryParams(key, value) {
1086
+ if (!this.schema.properties[key]) return
1087
+
1088
+ if (!this.schema.properties[key].objectConfiguration) {
1089
+ this.$set(this.schema.properties[key], 'objectConfiguration', { handling: 'related-object' })
1090
+ }
1091
+
1092
+ if (value && value.trim()) {
1093
+ this.$set(this.schema.properties[key].objectConfiguration, 'queryParams', value.trim())
1094
+ } else {
1095
+ this.$delete(this.schema.properties[key].objectConfiguration, 'queryParams')
1096
+ }
1097
+ },
1098
+
1099
+ getArrayItemQueryParams(key) {
1100
+ if (!this.schema.properties[key] || !this.schema.properties[key].items || !this.schema.properties[key].items.objectConfiguration) return ''
1101
+ return this.schema.properties[key].items.objectConfiguration.queryParams || ''
1102
+ },
1103
+
1104
+ updateArrayItemQueryParams(key, value) {
1105
+ if (!this.schema.properties[key] || !this.schema.properties[key].items) return
1106
+
1107
+ if (!this.schema.properties[key].items.objectConfiguration) {
1108
+ this.$set(this.schema.properties[key].items, 'objectConfiguration', { handling: 'related-object' })
1109
+ }
1110
+
1111
+ if (value && value.trim()) {
1112
+ this.$set(this.schema.properties[key].items.objectConfiguration, 'queryParams', value.trim())
1113
+ } else {
1114
+ this.$delete(this.schema.properties[key].items.objectConfiguration, 'queryParams')
1115
+ }
1116
+ },
1117
+
1118
+ // --- File properties ---
1119
+
1120
+ getFilePropertySetting(key, setting) {
1121
+ const property = this.schema.properties[key]
1122
+ if (!property) return false
1123
+
1124
+ if (property.type === 'file') return property[setting] || false
1125
+ if (property.type === 'array' && property.items) return property.items[setting] || false
1126
+ return false
1127
+ },
1128
+
1129
+ updateFilePropertySetting(key, setting, value) {
1130
+ if (this.schema.properties[key]) {
1131
+ if (this.schema.properties[key].type === 'file') {
1132
+ this.$set(this.schema.properties[key], setting, value)
1133
+ } else if (this.schema.properties[key].type === 'array' && this.schema.properties[key].items) {
1134
+ this.$set(this.schema.properties[key].items, setting, value)
1135
+ }
1136
+ }
1137
+ },
1138
+
1139
+ updateFileProperty(key, setting, value) {
1140
+ if (this.schema.properties[key]) {
1141
+ if (['allowedTypes', 'allowedTags', 'autoTags'].includes(setting)) {
1142
+ const arrayValue = value ? value.split(',').map(item => item.trim()).filter(item => item !== '') : []
1143
+ if (this.schema.properties[key].type === 'file') {
1144
+ this.$set(this.schema.properties[key], setting, arrayValue)
1145
+ } else if (this.schema.properties[key].type === 'array' && this.schema.properties[key].items) {
1146
+ this.$set(this.schema.properties[key].items, setting, arrayValue)
1147
+ }
1148
+ } else if (setting === 'maxSize') {
1149
+ const numValue = value ? Number(value) : undefined
1150
+ if (this.schema.properties[key].type === 'file') {
1151
+ this.$set(this.schema.properties[key], setting, numValue)
1152
+ } else if (this.schema.properties[key].type === 'array' && this.schema.properties[key].items) {
1153
+ this.$set(this.schema.properties[key].items, setting, numValue)
1154
+ }
1155
+ }
1156
+ }
1157
+ },
1158
+
1159
+ getFilePropertyTags(key, setting) {
1160
+ const property = this.schema.properties[key]
1161
+ if (!property) return []
1162
+
1163
+ let tags = []
1164
+ if (property.type === 'file') {
1165
+ tags = property[setting] || []
1166
+ } else if (property.type === 'array' && property.items) {
1167
+ tags = property.items[setting] || []
1168
+ }
1169
+
1170
+ return tags.map(tag => ({
1171
+ id: tag,
1172
+ label: tag,
1173
+ }))
1174
+ },
1175
+
1176
+ updateFilePropertyTags(key, setting, selectedOptions) {
1177
+ if (this.schema.properties[key]) {
1178
+ const tags = selectedOptions ? selectedOptions.map(option => option.id || option) : []
1179
+
1180
+ if (this.schema.properties[key].type === 'file') {
1181
+ this.$set(this.schema.properties[key], setting, tags)
1182
+ } else if (this.schema.properties[key].type === 'array' && this.schema.properties[key].items) {
1183
+ this.$set(this.schema.properties[key].items, setting, tags)
1184
+ }
1185
+ }
1186
+ },
1187
+
1188
+ // --- Table config ---
1189
+
1190
+ getPropertyTableSetting(key, setting) {
1191
+ if (!this.schema.properties[key] || !this.schema.properties[key].table) return false
1192
+ return this.schema.properties[key].table[setting] === true
1193
+ },
1194
+
1195
+ getOriginalPropertyTableSetting(key, setting) {
1196
+ return this.originalProperties?.[key]?.table?.[setting]
1197
+ },
1198
+
1199
+ updatePropertyTableSetting(key, setting, value) {
1200
+ if (!this.schema.properties[key]) return
1201
+
1202
+ if (!this.schema.properties[key].table) {
1203
+ this.$set(this.schema.properties[key], 'table', {})
1204
+ }
1205
+
1206
+ this.$set(this.schema.properties[key].table, setting, value)
1207
+
1208
+ const wasTrueOriginally = this.getOriginalPropertyTableSetting(key, setting) === true
1209
+ const becameExplicitFalse = value === false
1210
+ const shouldKeepExplicitFalse = setting === 'default' && becameExplicitFalse && wasTrueOriginally
1211
+
1212
+ if (this.isTableConfigDefault(key) && !shouldKeepExplicitFalse) {
1213
+ this.$delete(this.schema.properties[key], 'table')
1214
+ }
1215
+ },
1216
+
1217
+ isTableConfigDefault(key) {
1218
+ const table = this.schema.properties[key]?.table
1219
+ if (!table) return true
1220
+
1221
+ const defaults = { default: false }
1222
+ return Object.keys(table).every(setting =>
1223
+ table[setting] === defaults[setting],
1224
+ )
1225
+ },
1226
+
1227
+ hasCustomTableSettings(key) {
1228
+ return !this.isTableConfigDefault(key)
1229
+ },
1230
+
1231
+ // --- Property-level RBAC ---
1232
+
1233
+ hasPropertyAnyPermissions(key) {
1234
+ if (!this.schema.properties[key] || !this.schema.properties[key].authorization) return false
1235
+ const auth = this.schema.properties[key].authorization
1236
+ return Object.keys(auth).some(action =>
1237
+ Array.isArray(auth[action]) && auth[action].length > 0,
1238
+ )
1239
+ },
1240
+
1241
+ getDisplayGroupName(groupId) {
1242
+ if (groupId === 'public') return 'Public'
1243
+ if (groupId === 'user') return 'User'
1244
+ if (groupId === 'admin') return 'Admin'
1245
+
1246
+ const group = this.userGroups.find(g => g.id === groupId)
1247
+ return group ? (group.displayname || group.id) : groupId
1248
+ },
1249
+
1250
+ getPropertyPermissionsList(key) {
1251
+ if (!this.schema.properties[key] || !this.schema.properties[key].authorization) return []
1252
+
1253
+ const auth = this.schema.properties[key].authorization
1254
+ const permissionsList = []
1255
+ const processedGroups = new Set()
1256
+
1257
+ Object.keys(auth).forEach(action => {
1258
+ if (Array.isArray(auth[action])) {
1259
+ auth[action].forEach(groupId => {
1260
+ if (!processedGroups.has(groupId)) {
1261
+ const rights = []
1262
+ if (auth.create && auth.create.includes(groupId)) rights.push('C')
1263
+ if (auth.read && auth.read.includes(groupId)) rights.push('R')
1264
+ if (auth.update && auth.update.includes(groupId)) rights.push('U')
1265
+ if (auth.delete && auth.delete.includes(groupId)) rights.push('D')
1266
+
1267
+ permissionsList.push({
1268
+ group: this.getDisplayGroupName(groupId),
1269
+ groupId,
1270
+ rights: rights.length > 0 ? rights.join(',') : 'none',
1271
+ })
1272
+ processedGroups.add(groupId)
1273
+ }
1274
+ })
1275
+ }
1276
+ })
1277
+
1278
+ permissionsList.push({
1279
+ group: 'Admin',
1280
+ groupId: 'admin',
1281
+ rights: 'C,R,U,D',
1282
+ })
1283
+
1284
+ return permissionsList.sort((a, b) => {
1285
+ if (a.groupId === 'public') return -1
1286
+ if (b.groupId === 'public') return 1
1287
+ if (a.groupId === 'user') return -1
1288
+ if (b.groupId === 'user') return 1
1289
+ if (a.groupId === 'admin') return 1
1290
+ if (b.groupId === 'admin') return -1
1291
+ return a.group.localeCompare(b.group)
1292
+ })
1293
+ },
1294
+
1295
+ getAvailableGroupsForProperty() {
1296
+ return [
1297
+ { id: 'public', label: 'Public (Unauthenticated)' },
1298
+ { id: 'user', label: 'User (Authenticated)' },
1299
+ ...this.sortedUserGroups.map(group => ({
1300
+ id: group.id,
1301
+ label: group.displayname || group.id,
1302
+ })),
1303
+ ]
1304
+ },
1305
+
1306
+ hasAnyPropertyNewPermissionSelected() {
1307
+ return this.propertyNewPermissionCreate
1308
+ || this.propertyNewPermissionRead
1309
+ || this.propertyNewPermissionUpdate
1310
+ || this.propertyNewPermissionDelete
1311
+ },
1312
+
1313
+ hasPropertyGroupPermission(key, groupId, action) {
1314
+ if (!this.schema.properties[key] || !this.schema.properties[key].authorization) return false
1315
+ const auth = this.schema.properties[key].authorization
1316
+ if (!auth[action] || !Array.isArray(auth[action])) return false
1317
+ return auth[action].includes(groupId)
1318
+ },
1319
+
1320
+ updatePropertyGroupPermission(key, groupId, action, hasPermission) {
1321
+ if (!this.schema.properties[key]) return
1322
+
1323
+ if (!this.schema.properties[key].authorization) {
1324
+ this.$set(this.schema.properties[key], 'authorization', {})
1325
+ }
1326
+
1327
+ if (!this.schema.properties[key].authorization[action]) {
1328
+ this.$set(this.schema.properties[key].authorization, action, [])
1329
+ }
1330
+
1331
+ const currentPermissions = this.schema.properties[key].authorization[action]
1332
+ const groupIndex = currentPermissions.indexOf(groupId)
1333
+
1334
+ if (hasPermission && groupIndex === -1) {
1335
+ currentPermissions.push(groupId)
1336
+ } else if (!hasPermission && groupIndex !== -1) {
1337
+ currentPermissions.splice(groupIndex, 1)
1338
+ }
1339
+
1340
+ if (currentPermissions.length === 0) {
1341
+ this.$delete(this.schema.properties[key].authorization, action)
1342
+ }
1343
+
1344
+ if (Object.keys(this.schema.properties[key].authorization).length === 0) {
1345
+ this.$delete(this.schema.properties[key], 'authorization')
1346
+ }
1347
+ },
1348
+
1349
+ addPropertyGroupPermissions(key) {
1350
+ if (!this.propertyNewPermissionGroup) return
1351
+
1352
+ const groupId = typeof this.propertyNewPermissionGroup === 'object'
1353
+ ? this.propertyNewPermissionGroup.id
1354
+ : this.propertyNewPermissionGroup
1355
+
1356
+ if (!this.schema.properties[key].authorization) {
1357
+ this.$set(this.schema.properties[key], 'authorization', {})
1358
+ }
1359
+
1360
+ if (this.propertyNewPermissionCreate) {
1361
+ this.updatePropertyGroupPermission(key, groupId, 'create', true)
1362
+ }
1363
+ if (this.propertyNewPermissionRead) {
1364
+ this.updatePropertyGroupPermission(key, groupId, 'read', true)
1365
+ }
1366
+ if (this.propertyNewPermissionUpdate) {
1367
+ this.updatePropertyGroupPermission(key, groupId, 'update', true)
1368
+ }
1369
+ if (this.propertyNewPermissionDelete) {
1370
+ this.updatePropertyGroupPermission(key, groupId, 'delete', true)
1371
+ }
1372
+
1373
+ this.resetPropertyPermissionForm()
1374
+ },
1375
+
1376
+ removePropertyGroupPermissions(key, displayName) {
1377
+ const permission = this.getPropertyPermissionsList(key).find(p => p.group === displayName)
1378
+ if (!permission || permission.groupId === 'admin') return
1379
+
1380
+ const groupId = permission.groupId
1381
+
1382
+ if (!this.schema.properties[key] || !this.schema.properties[key].authorization) return
1383
+
1384
+ ;['create', 'read', 'update', 'delete'].forEach(action => {
1385
+ this.updatePropertyGroupPermission(key, groupId, action, false)
1386
+ })
1387
+ },
1388
+
1389
+ resetPropertyPermissionForm() {
1390
+ this.propertyNewPermissionGroup = null
1391
+ this.propertyNewPermissionCreate = false
1392
+ this.propertyNewPermissionRead = false
1393
+ this.propertyNewPermissionUpdate = false
1394
+ this.propertyNewPermissionDelete = false
1395
+ },
1396
+ },
1397
+ }
1398
+ </script>