@conduction/nextcloud-vue 0.1.0-beta.4 → 0.1.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nextcloud-vue.cjs +67614 -0
- package/dist/nextcloud-vue.cjs.js +9559 -8983
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.cjs.map +1 -0
- package/dist/nextcloud-vue.css +1231 -1231
- package/dist/nextcloud-vue.esm.js +9559 -8983
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +14 -5
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -217
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -121
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -418
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
- package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
- package/src/components/CnItemCard/CnItemCard.vue +132 -132
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectCard/eslint-setup.md +235 -0
- package/src/components/CnObjectCard/package.json-or.json +132 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
- package/src/components/CnRowActions/CnRowActions.vue +95 -73
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
- package/src/index.js +1 -1
- package/src/types/notification.d.ts +13 -13
- package/src/types/organisation.d.ts +15 -15
- package/src/types/schema.d.ts +13 -13
- package/src/types/task.d.ts +6 -6
- package/src/utils/headers.js +5 -3
|
@@ -1,579 +1,579 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<NcDialog
|
|
3
|
-
:name="resolvedTitle"
|
|
4
|
-
size="large"
|
|
5
|
-
:can-close="!loading"
|
|
6
|
-
@closing="$emit('close')">
|
|
7
|
-
<!-- Result phase -->
|
|
8
|
-
<div v-if="result !== null" class="cn-advanced-form-dialog__result">
|
|
9
|
-
<NcNoteCard v-if="result.success" type="success">
|
|
10
|
-
{{ resolvedSuccessText }}
|
|
11
|
-
</NcNoteCard>
|
|
12
|
-
<NcNoteCard v-if="result.error" type="error">
|
|
13
|
-
{{ result.error }}
|
|
14
|
-
</NcNoteCard>
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
|
-
<!-- Form phase -->
|
|
18
|
-
<div v-else class="cn-advanced-form-dialog__form">
|
|
19
|
-
<!-- Full form override slot -->
|
|
20
|
-
<slot
|
|
21
|
-
v-if="$scopedSlots.form"
|
|
22
|
-
name="form"
|
|
23
|
-
:form-data="formData"
|
|
24
|
-
:update-field="updateField"
|
|
25
|
-
:object-properties="objectPropertiesForSlot"
|
|
26
|
-
:json-data="jsonData"
|
|
27
|
-
:update-json="updateJsonFromExternal"
|
|
28
|
-
:is-valid-json="isValidJson(jsonData)" />
|
|
29
|
-
|
|
30
|
-
<!-- Default content -->
|
|
31
|
-
<template v-else>
|
|
32
|
-
<!-- Register/schema selection step (optional slot) -->
|
|
33
|
-
<slot
|
|
34
|
-
v-if="$scopedSlots['register-schema-selection']"
|
|
35
|
-
name="register-schema-selection"
|
|
36
|
-
:proceed="proceedFromRegisterSchemaStep" />
|
|
37
|
-
|
|
38
|
-
<!-- Main tabs -->
|
|
39
|
-
<div v-else class="cn-advanced-form-dialog__tabs tabContainer">
|
|
40
|
-
<BTabs v-model="activeTab" content-class="mt-3" justified>
|
|
41
|
-
<!-- Properties tab -->
|
|
42
|
-
<BTab v-if="showPropertiesTable" title="Properties">
|
|
43
|
-
<slot
|
|
44
|
-
name="tab-properties"
|
|
45
|
-
:form-data="formData"
|
|
46
|
-
:update-field="updateField"
|
|
47
|
-
:object-properties="objectPropertiesForSlot"
|
|
48
|
-
:selected-property="selectedProperty"
|
|
49
|
-
:handle-row-click="onRowClick"
|
|
50
|
-
:get-property-display-name="getPropertyDisplayName"
|
|
51
|
-
:get-property-validation-class="getPropertyValidationClass"
|
|
52
|
-
:is-property-editable="isPropertyEditable"
|
|
53
|
-
:validation-display="validationDisplay">
|
|
54
|
-
<CnPropertiesTab
|
|
55
|
-
ref="propertiesTab"
|
|
56
|
-
:schema="schema"
|
|
57
|
-
:item="item"
|
|
58
|
-
:form-data="formData"
|
|
59
|
-
:selected-property="selectedProperty"
|
|
60
|
-
:editable-types="editableTypes"
|
|
61
|
-
:validation-display="validationDisplay"
|
|
62
|
-
:exclude-fields="excludeFields"
|
|
63
|
-
:include-fields="includeFields"
|
|
64
|
-
@update:property-value="onPropertyValueUpdate"
|
|
65
|
-
@update:selected-property="selectedProperty = $event" />
|
|
66
|
-
</slot>
|
|
67
|
-
</BTab>
|
|
68
|
-
|
|
69
|
-
<!-- Metadata tab -->
|
|
70
|
-
<BTab v-if="resolvedShowMetadataTab" title="Metadata">
|
|
71
|
-
<slot name="tab-metadata" :item="item" :form-data="formData">
|
|
72
|
-
<CnMetadataTab :item="item" :form-data="formData" />
|
|
73
|
-
</slot>
|
|
74
|
-
</BTab>
|
|
75
|
-
|
|
76
|
-
<!-- Data (JSON) tab -->
|
|
77
|
-
<BTab v-if="showJsonTab" title="Data">
|
|
78
|
-
<slot
|
|
79
|
-
name="tab-data"
|
|
80
|
-
:json-data="jsonData"
|
|
81
|
-
:update-json="updateJsonFromExternal"
|
|
82
|
-
:is-valid="isValidJson(jsonData)"
|
|
83
|
-
:format-json="formatJSON">
|
|
84
|
-
<CnDataTab
|
|
85
|
-
:value="jsonData"
|
|
86
|
-
:dark="jsonEditorDark"
|
|
87
|
-
@update:value="jsonData = $event"
|
|
88
|
-
@format="onFormatResult" />
|
|
89
|
-
</slot>
|
|
90
|
-
</BTab>
|
|
91
|
-
</BTabs>
|
|
92
|
-
</div>
|
|
93
|
-
</template>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<template #actions>
|
|
97
|
-
<slot name="actions-left" />
|
|
98
|
-
<NcButton @click="$emit('close')">
|
|
99
|
-
{{ result !== null ? closeLabel : cancelLabel }}
|
|
100
|
-
</NcButton>
|
|
101
|
-
<NcButton
|
|
102
|
-
v-if="result === null"
|
|
103
|
-
type="primary"
|
|
104
|
-
:disabled="loading"
|
|
105
|
-
@click="executeConfirm">
|
|
106
|
-
<template #icon>
|
|
107
|
-
<NcLoadingIcon v-if="loading" :size="20" />
|
|
108
|
-
<Plus v-else-if="isCreateMode" :size="20" />
|
|
109
|
-
<ContentSaveOutline v-else :size="20" />
|
|
110
|
-
</template>
|
|
111
|
-
{{ resolvedConfirmLabel }}
|
|
112
|
-
</NcButton>
|
|
113
|
-
<slot name="actions-right" />
|
|
114
|
-
</template>
|
|
115
|
-
</NcDialog>
|
|
116
|
-
</template>
|
|
117
|
-
|
|
118
|
-
<script>
|
|
119
|
-
import {
|
|
120
|
-
NcDialog,
|
|
121
|
-
NcButton,
|
|
122
|
-
NcNoteCard,
|
|
123
|
-
NcLoadingIcon,
|
|
124
|
-
} from '@nextcloud/vue'
|
|
125
|
-
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
126
|
-
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
127
|
-
import { BTabs, BTab } from 'bootstrap-vue'
|
|
128
|
-
import { fieldsFromSchema } from '../../utils/schema.js'
|
|
129
|
-
import CnPropertiesTab from './CnPropertiesTab.vue'
|
|
130
|
-
import CnMetadataTab from './CnMetadataTab.vue'
|
|
131
|
-
import CnDataTab from './CnDataTab.vue'
|
|
132
|
-
|
|
133
|
-
/** Schema types for which we have built-in inline editing support in the properties table. */
|
|
134
|
-
const EDITABLE_SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean']
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* CnAdvancedFormDialog — Create/edit dialog with properties table (click-to-edit), JSON tab, and optional store integration.
|
|
138
|
-
*
|
|
139
|
-
* When `item` is null, operates in create mode. When `item` is provided, operates in edit mode.
|
|
140
|
-
* Provides a richer UX than CnFormDialog: properties table with inline editing, Data (JSON) tab with CodeMirror,
|
|
141
|
-
* optional Metadata tab. Editable property types are determined by coded-in support; optional editablePropertyTypes
|
|
142
|
-
* prop can restrict or extend. Dialog size is fixed to large.
|
|
143
|
-
*
|
|
144
|
-
* @event confirm Emitted when the user confirms. Payload: formData object.
|
|
145
|
-
* @event close Emitted when the dialog should be closed.
|
|
146
|
-
*/
|
|
147
|
-
export default {
|
|
148
|
-
name: 'CnAdvancedFormDialog',
|
|
149
|
-
|
|
150
|
-
components: {
|
|
151
|
-
NcDialog,
|
|
152
|
-
NcButton,
|
|
153
|
-
NcNoteCard,
|
|
154
|
-
NcLoadingIcon,
|
|
155
|
-
Plus,
|
|
156
|
-
ContentSaveOutline,
|
|
157
|
-
BTabs,
|
|
158
|
-
BTab,
|
|
159
|
-
CnPropertiesTab,
|
|
160
|
-
CnMetadataTab,
|
|
161
|
-
CnDataTab,
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
props: {
|
|
165
|
-
schema: { type: Object, default: null },
|
|
166
|
-
item: { type: Object, default: null },
|
|
167
|
-
dialogTitle: { type: String, default: '' },
|
|
168
|
-
nameField: { type: String, default: 'title' },
|
|
169
|
-
successText: { type: String, default: '' },
|
|
170
|
-
cancelLabel: { type: String, default: 'Cancel' },
|
|
171
|
-
closeLabel: { type: String, default: 'Close' },
|
|
172
|
-
confirmLabel: { type: String, default: '' },
|
|
173
|
-
excludeFields: { type: Array, default: () => [] },
|
|
174
|
-
includeFields: { type: Array, default: null },
|
|
175
|
-
fieldOverrides: { type: Object, default: () => ({}) },
|
|
176
|
-
showPropertiesTable: { type: Boolean, default: true },
|
|
177
|
-
showJsonTab: { type: Boolean, default: true },
|
|
178
|
-
showMetadataTab: { type: Boolean, default: null },
|
|
179
|
-
editablePropertyTypes: { type: Array, default: null },
|
|
180
|
-
validationDisplay: { type: String, default: 'indicator', validator: (v) => ['indicator', 'none'].includes(v) },
|
|
181
|
-
jsonEditorDark: { type: Boolean, default: false },
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
data() {
|
|
185
|
-
return {
|
|
186
|
-
formData: {},
|
|
187
|
-
jsonData: '',
|
|
188
|
-
activeTab: 0,
|
|
189
|
-
selectedProperty: null,
|
|
190
|
-
errors: {},
|
|
191
|
-
loading: false,
|
|
192
|
-
result: null,
|
|
193
|
-
closeTimeout: null,
|
|
194
|
-
isInternalUpdate: false,
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
computed: {
|
|
199
|
-
isCreateMode() {
|
|
200
|
-
return !this.item
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
schemaTitle() {
|
|
204
|
-
return (this.schema && this.schema.title) || 'Item'
|
|
205
|
-
},
|
|
206
|
-
|
|
207
|
-
currentSchema() {
|
|
208
|
-
return this.schema
|
|
209
|
-
},
|
|
210
|
-
|
|
211
|
-
resolvedTitle() {
|
|
212
|
-
if (this.dialogTitle) return this.dialogTitle
|
|
213
|
-
return this.isCreateMode
|
|
214
|
-
? `Create ${this.schemaTitle}`
|
|
215
|
-
: `Edit ${this.schemaTitle}`
|
|
216
|
-
},
|
|
217
|
-
|
|
218
|
-
resolvedConfirmLabel() {
|
|
219
|
-
if (this.confirmLabel) return this.confirmLabel
|
|
220
|
-
return this.isCreateMode ? 'Create' : 'Save'
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
resolvedSuccessText() {
|
|
224
|
-
if (this.successText) return this.successText
|
|
225
|
-
return `${this.schemaTitle} saved successfully.`
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
resolvedShowMetadataTab() {
|
|
229
|
-
if (this.showMetadataTab !== null) return this.showMetadataTab
|
|
230
|
-
return !!this.item
|
|
231
|
-
},
|
|
232
|
-
|
|
233
|
-
resolvedFields() {
|
|
234
|
-
return fieldsFromSchema(this.schema, {
|
|
235
|
-
exclude: this.excludeFields,
|
|
236
|
-
include: this.includeFields,
|
|
237
|
-
overrides: this.fieldOverrides,
|
|
238
|
-
includeReadOnly: true,
|
|
239
|
-
})
|
|
240
|
-
},
|
|
241
|
-
|
|
242
|
-
/** objectProperties exposed to the #form and #tab-properties slot consumers */
|
|
243
|
-
objectPropertiesForSlot() {
|
|
244
|
-
const schemaProps = this.schema?.properties || {}
|
|
245
|
-
const obj = this.item || {}
|
|
246
|
-
const exclude = this.excludeFields || []
|
|
247
|
-
const include = this.includeFields
|
|
248
|
-
const filterKey = (k) => {
|
|
249
|
-
if (k === '@self' || k === 'id') return false
|
|
250
|
-
if (exclude.includes(k)) return false
|
|
251
|
-
if (include && !include.includes(k)) return false
|
|
252
|
-
return true
|
|
253
|
-
}
|
|
254
|
-
const existing = Object.entries(obj).filter(([k]) => filterKey(k))
|
|
255
|
-
const missing = []
|
|
256
|
-
for (const [key, prop] of Object.entries(schemaProps)) {
|
|
257
|
-
if (!filterKey(key)) continue
|
|
258
|
-
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
259
|
-
let def
|
|
260
|
-
switch (prop.type) {
|
|
261
|
-
case 'string': def = prop.const ?? ''; break
|
|
262
|
-
case 'number':
|
|
263
|
-
case 'integer': def = 0; break
|
|
264
|
-
case 'boolean': def = false; break
|
|
265
|
-
case 'array': def = []; break
|
|
266
|
-
case 'object': def = {}; break
|
|
267
|
-
default: def = ''
|
|
268
|
-
}
|
|
269
|
-
missing.push([key, def])
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return [...existing, ...missing]
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
dataTabIndex() {
|
|
276
|
-
let index = 0
|
|
277
|
-
if (this.showPropertiesTable) index++
|
|
278
|
-
if (this.resolvedShowMetadataTab) index++
|
|
279
|
-
return index
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
isDataTabActive() {
|
|
283
|
-
return this.showJsonTab && this.activeTab === this.dataTabIndex
|
|
284
|
-
},
|
|
285
|
-
|
|
286
|
-
editableTypes() {
|
|
287
|
-
if (this.editablePropertyTypes && this.editablePropertyTypes.length > 0) {
|
|
288
|
-
return this.editablePropertyTypes
|
|
289
|
-
}
|
|
290
|
-
return EDITABLE_SUPPORTED_TYPES
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
watch: {
|
|
295
|
-
item: {
|
|
296
|
-
immediate: true,
|
|
297
|
-
handler(newItem) {
|
|
298
|
-
this.initFormData(newItem)
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
jsonData(newVal) {
|
|
302
|
-
if (!this.isInternalUpdate && this.isValidJson(newVal)) {
|
|
303
|
-
this.updateFormFromJson()
|
|
304
|
-
}
|
|
305
|
-
},
|
|
306
|
-
formData: {
|
|
307
|
-
handler() {
|
|
308
|
-
if (!this.isInternalUpdate) {
|
|
309
|
-
this.updateJsonFromForm()
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
deep: true,
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
|
|
316
|
-
beforeDestroy() {
|
|
317
|
-
if (this.closeTimeout) {
|
|
318
|
-
clearTimeout(this.closeTimeout)
|
|
319
|
-
this.closeTimeout = null
|
|
320
|
-
}
|
|
321
|
-
},
|
|
322
|
-
|
|
323
|
-
methods: {
|
|
324
|
-
proceedFromRegisterSchemaStep() {
|
|
325
|
-
// Placeholder for slot consumers
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
initFormData(item) {
|
|
329
|
-
if (item) {
|
|
330
|
-
this.formData = JSON.parse(JSON.stringify(item))
|
|
331
|
-
} else {
|
|
332
|
-
const data = {}
|
|
333
|
-
for (const field of this.resolvedFields) {
|
|
334
|
-
if (field.default !== null && field.default !== undefined) {
|
|
335
|
-
data[field.key] = field.default
|
|
336
|
-
} else if (field.widget === 'checkbox') {
|
|
337
|
-
data[field.key] = false
|
|
338
|
-
} else if (field.widget === 'tags' || field.widget === 'multiselect') {
|
|
339
|
-
data[field.key] = []
|
|
340
|
-
} else {
|
|
341
|
-
data[field.key] = null
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
this.formData = data
|
|
345
|
-
}
|
|
346
|
-
this.jsonData = JSON.stringify(this.formData, null, 2)
|
|
347
|
-
this.errors = {}
|
|
348
|
-
this.selectedProperty = null
|
|
349
|
-
},
|
|
350
|
-
|
|
351
|
-
updateField(key, value) {
|
|
352
|
-
this.$set(this.formData, key, value)
|
|
353
|
-
if (this.errors[key]) this.$delete(this.errors, key)
|
|
354
|
-
},
|
|
355
|
-
|
|
356
|
-
onPropertyValueUpdate({ key, value }) {
|
|
357
|
-
this.$set(this.formData, key, value)
|
|
358
|
-
if (this.errors[key]) this.$delete(this.errors, key)
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
onRowClick(key, event) {
|
|
362
|
-
// Forwarded for #tab-properties slot consumers — the sub-component handles it internally
|
|
363
|
-
},
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Proxy for slot consumers: exposes isPropertyEditable from the tab sub-component.
|
|
367
|
-
* @param {string} key - Property key
|
|
368
|
-
* @param {*} value - Current property value
|
|
369
|
-
*/
|
|
370
|
-
isPropertyEditable(key, value) {
|
|
371
|
-
const tab = this.$refs.propertiesTab
|
|
372
|
-
if (tab) return tab.isPropertyEditable(key, value)
|
|
373
|
-
return true
|
|
374
|
-
},
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Proxy for slot consumers.
|
|
378
|
-
* @param {string} key - Property key
|
|
379
|
-
*/
|
|
380
|
-
getPropertyDisplayName(key) {
|
|
381
|
-
const tab = this.$refs.propertiesTab
|
|
382
|
-
if (tab) return tab.getPropertyDisplayName(key)
|
|
383
|
-
return key
|
|
384
|
-
},
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Proxy for slot consumers.
|
|
388
|
-
* @param {string} key - Property key
|
|
389
|
-
* @param {*} value - Current property value
|
|
390
|
-
*/
|
|
391
|
-
getPropertyValidationClass(key, value) {
|
|
392
|
-
const tab = this.$refs.propertiesTab
|
|
393
|
-
if (tab) return tab.getPropertyValidationClass(key, value)
|
|
394
|
-
return ''
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
updateFormFromJson() {
|
|
398
|
-
if (this.isInternalUpdate) return
|
|
399
|
-
try {
|
|
400
|
-
this.isInternalUpdate = true
|
|
401
|
-
this.formData = JSON.parse(this.jsonData)
|
|
402
|
-
} catch {
|
|
403
|
-
// Keep previous formData
|
|
404
|
-
} finally {
|
|
405
|
-
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
406
|
-
}
|
|
407
|
-
},
|
|
408
|
-
|
|
409
|
-
updateJsonFromForm() {
|
|
410
|
-
if (this.isInternalUpdate) return
|
|
411
|
-
try {
|
|
412
|
-
this.isInternalUpdate = true
|
|
413
|
-
this.jsonData = JSON.stringify(this.formData, null, 2)
|
|
414
|
-
} catch {
|
|
415
|
-
// Ignore
|
|
416
|
-
} finally {
|
|
417
|
-
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
|
|
421
|
-
updateJsonFromExternal(newJson) {
|
|
422
|
-
this.jsonData = newJson
|
|
423
|
-
if (this.isValidJson(newJson)) this.updateFormFromJson()
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
isValidJson(str) {
|
|
427
|
-
if (!str || !str.trim()) return false
|
|
428
|
-
try {
|
|
429
|
-
JSON.parse(str)
|
|
430
|
-
return true
|
|
431
|
-
} catch {
|
|
432
|
-
return false
|
|
433
|
-
}
|
|
434
|
-
},
|
|
435
|
-
|
|
436
|
-
formatJSON() {
|
|
437
|
-
try {
|
|
438
|
-
if (this.jsonData) {
|
|
439
|
-
const parsed = JSON.parse(this.jsonData)
|
|
440
|
-
this.jsonData = JSON.stringify(parsed, null, 2)
|
|
441
|
-
if (!this.isInternalUpdate) {
|
|
442
|
-
this.isInternalUpdate = true
|
|
443
|
-
this.formData = parsed
|
|
444
|
-
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
} catch {
|
|
448
|
-
// Keep invalid JSON as-is
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
|
|
452
|
-
onFormatResult(parsed) {
|
|
453
|
-
if (!this.isInternalUpdate) {
|
|
454
|
-
this.isInternalUpdate = true
|
|
455
|
-
this.formData = parsed
|
|
456
|
-
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
|
|
460
|
-
validate() {
|
|
461
|
-
const newErrors = {}
|
|
462
|
-
for (const field of this.resolvedFields) {
|
|
463
|
-
const value = this.formData[field.key]
|
|
464
|
-
if (field.required && (value == null || value === '')) {
|
|
465
|
-
newErrors[field.key] = `${field.label} is required.`
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
this.errors = newErrors
|
|
469
|
-
return Object.keys(newErrors).length === 0
|
|
470
|
-
},
|
|
471
|
-
|
|
472
|
-
executeConfirm() {
|
|
473
|
-
if (!this.validate()) return
|
|
474
|
-
if (this.isDataTabActive && !this.isValidJson(this.jsonData)) return
|
|
475
|
-
this.$emit('confirm', JSON.parse(JSON.stringify(this.formData)))
|
|
476
|
-
},
|
|
477
|
-
|
|
478
|
-
setResult(resultData) {
|
|
479
|
-
this.loading = false
|
|
480
|
-
this.result = resultData
|
|
481
|
-
if (resultData?.success) {
|
|
482
|
-
this.closeTimeout = setTimeout(() => this.$emit('close'), 2000)
|
|
483
|
-
}
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
}
|
|
487
|
-
</script>
|
|
488
|
-
|
|
489
|
-
<style scoped>
|
|
490
|
-
.cn-advanced-form-dialog__form {
|
|
491
|
-
display: flex;
|
|
492
|
-
flex-direction: column;
|
|
493
|
-
gap: 8px;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
.cn-advanced-form-dialog__tabs {
|
|
497
|
-
display: flex;
|
|
498
|
-
flex-direction: column;
|
|
499
|
-
gap: 12px;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/* Bootstrap-Vue tab styling to match ViewObject */
|
|
503
|
-
.tabContainer {
|
|
504
|
-
margin-top: 20px;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
.tabContainer > * ul > li {
|
|
508
|
-
display: flex;
|
|
509
|
-
flex: 1;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
.tabContainer > * ul > li:hover {
|
|
513
|
-
background-color: var(--color-background-hover);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
.tabContainer > * ul > li > a {
|
|
517
|
-
flex: 1;
|
|
518
|
-
text-align: center;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
.tabContainer > * ul > li > .active {
|
|
522
|
-
background: transparent !important;
|
|
523
|
-
color: var(--color-main-text) !important;
|
|
524
|
-
border-bottom: var(--default-grid-baseline) solid var(--color-primary-element) !important;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
.tabContainer > * ul[role="tablist"] {
|
|
528
|
-
display: flex;
|
|
529
|
-
margin: 10px 8px 0 8px;
|
|
530
|
-
justify-content: space-between;
|
|
531
|
-
border-bottom: 1px solid var(--color-border);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
.tabContainer > * ul[role="tablist"] > * a[role="tab"] {
|
|
535
|
-
padding-inline-start: 10px;
|
|
536
|
-
padding-inline-end: 10px;
|
|
537
|
-
padding-block-start: 10px;
|
|
538
|
-
padding-block-end: 10px;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
.tabContainer > * div[role="tabpanel"] {
|
|
542
|
-
margin-block-start: var(--OR-margin-10);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
:deep(.nav-tabs) {
|
|
546
|
-
border-bottom: 1px solid var(--color-border);
|
|
547
|
-
margin-bottom: 15px;
|
|
548
|
-
display: flex;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
:deep(.nav-tabs .nav-item) {
|
|
552
|
-
display: flex;
|
|
553
|
-
flex: 1;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
:deep(.nav-tabs .nav-link) {
|
|
557
|
-
flex: 1;
|
|
558
|
-
text-align: center;
|
|
559
|
-
border: none;
|
|
560
|
-
border-bottom: 2px solid transparent;
|
|
561
|
-
color: var(--color-text-maxcontrast);
|
|
562
|
-
padding: 8px 16px;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
:deep(.nav-tabs .nav-link.active) {
|
|
566
|
-
color: var(--color-main-text);
|
|
567
|
-
border-bottom: 2px solid var(--color-primary);
|
|
568
|
-
background-color: transparent;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
:deep(.nav-tabs .nav-link:hover) {
|
|
572
|
-
border-bottom: 2px solid var(--color-border);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
:deep(.tab-content) {
|
|
576
|
-
padding: 16px;
|
|
577
|
-
background-color: var(--color-main-background);
|
|
578
|
-
}
|
|
579
|
-
</style>
|
|
1
|
+
<template>
|
|
2
|
+
<NcDialog
|
|
3
|
+
:name="resolvedTitle"
|
|
4
|
+
size="large"
|
|
5
|
+
:can-close="!loading"
|
|
6
|
+
@closing="$emit('close')">
|
|
7
|
+
<!-- Result phase -->
|
|
8
|
+
<div v-if="result !== null" class="cn-advanced-form-dialog__result">
|
|
9
|
+
<NcNoteCard v-if="result.success" type="success">
|
|
10
|
+
{{ resolvedSuccessText }}
|
|
11
|
+
</NcNoteCard>
|
|
12
|
+
<NcNoteCard v-if="result.error" type="error">
|
|
13
|
+
{{ result.error }}
|
|
14
|
+
</NcNoteCard>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Form phase -->
|
|
18
|
+
<div v-else class="cn-advanced-form-dialog__form">
|
|
19
|
+
<!-- Full form override slot -->
|
|
20
|
+
<slot
|
|
21
|
+
v-if="$scopedSlots.form"
|
|
22
|
+
name="form"
|
|
23
|
+
:form-data="formData"
|
|
24
|
+
:update-field="updateField"
|
|
25
|
+
:object-properties="objectPropertiesForSlot"
|
|
26
|
+
:json-data="jsonData"
|
|
27
|
+
:update-json="updateJsonFromExternal"
|
|
28
|
+
:is-valid-json="isValidJson(jsonData)" />
|
|
29
|
+
|
|
30
|
+
<!-- Default content -->
|
|
31
|
+
<template v-else>
|
|
32
|
+
<!-- Register/schema selection step (optional slot) -->
|
|
33
|
+
<slot
|
|
34
|
+
v-if="$scopedSlots['register-schema-selection']"
|
|
35
|
+
name="register-schema-selection"
|
|
36
|
+
:proceed="proceedFromRegisterSchemaStep" />
|
|
37
|
+
|
|
38
|
+
<!-- Main tabs -->
|
|
39
|
+
<div v-else class="cn-advanced-form-dialog__tabs tabContainer">
|
|
40
|
+
<BTabs v-model="activeTab" content-class="mt-3" justified>
|
|
41
|
+
<!-- Properties tab -->
|
|
42
|
+
<BTab v-if="showPropertiesTable" title="Properties">
|
|
43
|
+
<slot
|
|
44
|
+
name="tab-properties"
|
|
45
|
+
:form-data="formData"
|
|
46
|
+
:update-field="updateField"
|
|
47
|
+
:object-properties="objectPropertiesForSlot"
|
|
48
|
+
:selected-property="selectedProperty"
|
|
49
|
+
:handle-row-click="onRowClick"
|
|
50
|
+
:get-property-display-name="getPropertyDisplayName"
|
|
51
|
+
:get-property-validation-class="getPropertyValidationClass"
|
|
52
|
+
:is-property-editable="isPropertyEditable"
|
|
53
|
+
:validation-display="validationDisplay">
|
|
54
|
+
<CnPropertiesTab
|
|
55
|
+
ref="propertiesTab"
|
|
56
|
+
:schema="schema"
|
|
57
|
+
:item="item"
|
|
58
|
+
:form-data="formData"
|
|
59
|
+
:selected-property="selectedProperty"
|
|
60
|
+
:editable-types="editableTypes"
|
|
61
|
+
:validation-display="validationDisplay"
|
|
62
|
+
:exclude-fields="excludeFields"
|
|
63
|
+
:include-fields="includeFields"
|
|
64
|
+
@update:property-value="onPropertyValueUpdate"
|
|
65
|
+
@update:selected-property="selectedProperty = $event" />
|
|
66
|
+
</slot>
|
|
67
|
+
</BTab>
|
|
68
|
+
|
|
69
|
+
<!-- Metadata tab -->
|
|
70
|
+
<BTab v-if="resolvedShowMetadataTab" title="Metadata">
|
|
71
|
+
<slot name="tab-metadata" :item="item" :form-data="formData">
|
|
72
|
+
<CnMetadataTab :item="item" :form-data="formData" />
|
|
73
|
+
</slot>
|
|
74
|
+
</BTab>
|
|
75
|
+
|
|
76
|
+
<!-- Data (JSON) tab -->
|
|
77
|
+
<BTab v-if="showJsonTab" title="Data">
|
|
78
|
+
<slot
|
|
79
|
+
name="tab-data"
|
|
80
|
+
:json-data="jsonData"
|
|
81
|
+
:update-json="updateJsonFromExternal"
|
|
82
|
+
:is-valid="isValidJson(jsonData)"
|
|
83
|
+
:format-json="formatJSON">
|
|
84
|
+
<CnDataTab
|
|
85
|
+
:value="jsonData"
|
|
86
|
+
:dark="jsonEditorDark"
|
|
87
|
+
@update:value="jsonData = $event"
|
|
88
|
+
@format="onFormatResult" />
|
|
89
|
+
</slot>
|
|
90
|
+
</BTab>
|
|
91
|
+
</BTabs>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<template #actions>
|
|
97
|
+
<slot name="actions-left" />
|
|
98
|
+
<NcButton @click="$emit('close')">
|
|
99
|
+
{{ result !== null ? closeLabel : cancelLabel }}
|
|
100
|
+
</NcButton>
|
|
101
|
+
<NcButton
|
|
102
|
+
v-if="result === null"
|
|
103
|
+
type="primary"
|
|
104
|
+
:disabled="loading"
|
|
105
|
+
@click="executeConfirm">
|
|
106
|
+
<template #icon>
|
|
107
|
+
<NcLoadingIcon v-if="loading" :size="20" />
|
|
108
|
+
<Plus v-else-if="isCreateMode" :size="20" />
|
|
109
|
+
<ContentSaveOutline v-else :size="20" />
|
|
110
|
+
</template>
|
|
111
|
+
{{ resolvedConfirmLabel }}
|
|
112
|
+
</NcButton>
|
|
113
|
+
<slot name="actions-right" />
|
|
114
|
+
</template>
|
|
115
|
+
</NcDialog>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
import {
|
|
120
|
+
NcDialog,
|
|
121
|
+
NcButton,
|
|
122
|
+
NcNoteCard,
|
|
123
|
+
NcLoadingIcon,
|
|
124
|
+
} from '@nextcloud/vue'
|
|
125
|
+
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
126
|
+
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
127
|
+
import { BTabs, BTab } from 'bootstrap-vue'
|
|
128
|
+
import { fieldsFromSchema } from '../../utils/schema.js'
|
|
129
|
+
import CnPropertiesTab from './CnPropertiesTab.vue'
|
|
130
|
+
import CnMetadataTab from './CnMetadataTab.vue'
|
|
131
|
+
import CnDataTab from './CnDataTab.vue'
|
|
132
|
+
|
|
133
|
+
/** Schema types for which we have built-in inline editing support in the properties table. */
|
|
134
|
+
const EDITABLE_SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean']
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* CnAdvancedFormDialog — Create/edit dialog with properties table (click-to-edit), JSON tab, and optional store integration.
|
|
138
|
+
*
|
|
139
|
+
* When `item` is null, operates in create mode. When `item` is provided, operates in edit mode.
|
|
140
|
+
* Provides a richer UX than CnFormDialog: properties table with inline editing, Data (JSON) tab with CodeMirror,
|
|
141
|
+
* optional Metadata tab. Editable property types are determined by coded-in support; optional editablePropertyTypes
|
|
142
|
+
* prop can restrict or extend. Dialog size is fixed to large.
|
|
143
|
+
*
|
|
144
|
+
* @event confirm Emitted when the user confirms. Payload: formData object.
|
|
145
|
+
* @event close Emitted when the dialog should be closed.
|
|
146
|
+
*/
|
|
147
|
+
export default {
|
|
148
|
+
name: 'CnAdvancedFormDialog',
|
|
149
|
+
|
|
150
|
+
components: {
|
|
151
|
+
NcDialog,
|
|
152
|
+
NcButton,
|
|
153
|
+
NcNoteCard,
|
|
154
|
+
NcLoadingIcon,
|
|
155
|
+
Plus,
|
|
156
|
+
ContentSaveOutline,
|
|
157
|
+
BTabs,
|
|
158
|
+
BTab,
|
|
159
|
+
CnPropertiesTab,
|
|
160
|
+
CnMetadataTab,
|
|
161
|
+
CnDataTab,
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
props: {
|
|
165
|
+
schema: { type: Object, default: null },
|
|
166
|
+
item: { type: Object, default: null },
|
|
167
|
+
dialogTitle: { type: String, default: '' },
|
|
168
|
+
nameField: { type: String, default: 'title' },
|
|
169
|
+
successText: { type: String, default: '' },
|
|
170
|
+
cancelLabel: { type: String, default: 'Cancel' },
|
|
171
|
+
closeLabel: { type: String, default: 'Close' },
|
|
172
|
+
confirmLabel: { type: String, default: '' },
|
|
173
|
+
excludeFields: { type: Array, default: () => [] },
|
|
174
|
+
includeFields: { type: Array, default: null },
|
|
175
|
+
fieldOverrides: { type: Object, default: () => ({}) },
|
|
176
|
+
showPropertiesTable: { type: Boolean, default: true },
|
|
177
|
+
showJsonTab: { type: Boolean, default: true },
|
|
178
|
+
showMetadataTab: { type: Boolean, default: null },
|
|
179
|
+
editablePropertyTypes: { type: Array, default: null },
|
|
180
|
+
validationDisplay: { type: String, default: 'indicator', validator: (v) => ['indicator', 'none'].includes(v) },
|
|
181
|
+
jsonEditorDark: { type: Boolean, default: false },
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
data() {
|
|
185
|
+
return {
|
|
186
|
+
formData: {},
|
|
187
|
+
jsonData: '',
|
|
188
|
+
activeTab: 0,
|
|
189
|
+
selectedProperty: null,
|
|
190
|
+
errors: {},
|
|
191
|
+
loading: false,
|
|
192
|
+
result: null,
|
|
193
|
+
closeTimeout: null,
|
|
194
|
+
isInternalUpdate: false,
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
computed: {
|
|
199
|
+
isCreateMode() {
|
|
200
|
+
return !this.item
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
schemaTitle() {
|
|
204
|
+
return (this.schema && this.schema.title) || 'Item'
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
currentSchema() {
|
|
208
|
+
return this.schema
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
resolvedTitle() {
|
|
212
|
+
if (this.dialogTitle) return this.dialogTitle
|
|
213
|
+
return this.isCreateMode
|
|
214
|
+
? `Create ${this.schemaTitle}`
|
|
215
|
+
: `Edit ${this.schemaTitle}`
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
resolvedConfirmLabel() {
|
|
219
|
+
if (this.confirmLabel) return this.confirmLabel
|
|
220
|
+
return this.isCreateMode ? 'Create' : 'Save'
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
resolvedSuccessText() {
|
|
224
|
+
if (this.successText) return this.successText
|
|
225
|
+
return `${this.schemaTitle} saved successfully.`
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
resolvedShowMetadataTab() {
|
|
229
|
+
if (this.showMetadataTab !== null) return this.showMetadataTab
|
|
230
|
+
return !!this.item
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
resolvedFields() {
|
|
234
|
+
return fieldsFromSchema(this.schema, {
|
|
235
|
+
exclude: this.excludeFields,
|
|
236
|
+
include: this.includeFields,
|
|
237
|
+
overrides: this.fieldOverrides,
|
|
238
|
+
includeReadOnly: true,
|
|
239
|
+
})
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/** objectProperties exposed to the #form and #tab-properties slot consumers */
|
|
243
|
+
objectPropertiesForSlot() {
|
|
244
|
+
const schemaProps = this.schema?.properties || {}
|
|
245
|
+
const obj = this.item || {}
|
|
246
|
+
const exclude = this.excludeFields || []
|
|
247
|
+
const include = this.includeFields
|
|
248
|
+
const filterKey = (k) => {
|
|
249
|
+
if (k === '@self' || k === 'id') return false
|
|
250
|
+
if (exclude.includes(k)) return false
|
|
251
|
+
if (include && !include.includes(k)) return false
|
|
252
|
+
return true
|
|
253
|
+
}
|
|
254
|
+
const existing = Object.entries(obj).filter(([k]) => filterKey(k))
|
|
255
|
+
const missing = []
|
|
256
|
+
for (const [key, prop] of Object.entries(schemaProps)) {
|
|
257
|
+
if (!filterKey(key)) continue
|
|
258
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
259
|
+
let def
|
|
260
|
+
switch (prop.type) {
|
|
261
|
+
case 'string': def = prop.const ?? ''; break
|
|
262
|
+
case 'number':
|
|
263
|
+
case 'integer': def = 0; break
|
|
264
|
+
case 'boolean': def = false; break
|
|
265
|
+
case 'array': def = []; break
|
|
266
|
+
case 'object': def = {}; break
|
|
267
|
+
default: def = ''
|
|
268
|
+
}
|
|
269
|
+
missing.push([key, def])
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [...existing, ...missing]
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
dataTabIndex() {
|
|
276
|
+
let index = 0
|
|
277
|
+
if (this.showPropertiesTable) index++
|
|
278
|
+
if (this.resolvedShowMetadataTab) index++
|
|
279
|
+
return index
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
isDataTabActive() {
|
|
283
|
+
return this.showJsonTab && this.activeTab === this.dataTabIndex
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
editableTypes() {
|
|
287
|
+
if (this.editablePropertyTypes && this.editablePropertyTypes.length > 0) {
|
|
288
|
+
return this.editablePropertyTypes
|
|
289
|
+
}
|
|
290
|
+
return EDITABLE_SUPPORTED_TYPES
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
watch: {
|
|
295
|
+
item: {
|
|
296
|
+
immediate: true,
|
|
297
|
+
handler(newItem) {
|
|
298
|
+
this.initFormData(newItem)
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
jsonData(newVal) {
|
|
302
|
+
if (!this.isInternalUpdate && this.isValidJson(newVal)) {
|
|
303
|
+
this.updateFormFromJson()
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
formData: {
|
|
307
|
+
handler() {
|
|
308
|
+
if (!this.isInternalUpdate) {
|
|
309
|
+
this.updateJsonFromForm()
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
deep: true,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
|
|
316
|
+
beforeDestroy() {
|
|
317
|
+
if (this.closeTimeout) {
|
|
318
|
+
clearTimeout(this.closeTimeout)
|
|
319
|
+
this.closeTimeout = null
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
methods: {
|
|
324
|
+
proceedFromRegisterSchemaStep() {
|
|
325
|
+
// Placeholder for slot consumers
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
initFormData(item) {
|
|
329
|
+
if (item) {
|
|
330
|
+
this.formData = JSON.parse(JSON.stringify(item))
|
|
331
|
+
} else {
|
|
332
|
+
const data = {}
|
|
333
|
+
for (const field of this.resolvedFields) {
|
|
334
|
+
if (field.default !== null && field.default !== undefined) {
|
|
335
|
+
data[field.key] = field.default
|
|
336
|
+
} else if (field.widget === 'checkbox') {
|
|
337
|
+
data[field.key] = false
|
|
338
|
+
} else if (field.widget === 'tags' || field.widget === 'multiselect') {
|
|
339
|
+
data[field.key] = []
|
|
340
|
+
} else {
|
|
341
|
+
data[field.key] = null
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.formData = data
|
|
345
|
+
}
|
|
346
|
+
this.jsonData = JSON.stringify(this.formData, null, 2)
|
|
347
|
+
this.errors = {}
|
|
348
|
+
this.selectedProperty = null
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
updateField(key, value) {
|
|
352
|
+
this.$set(this.formData, key, value)
|
|
353
|
+
if (this.errors[key]) this.$delete(this.errors, key)
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
onPropertyValueUpdate({ key, value }) {
|
|
357
|
+
this.$set(this.formData, key, value)
|
|
358
|
+
if (this.errors[key]) this.$delete(this.errors, key)
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
onRowClick(key, event) {
|
|
362
|
+
// Forwarded for #tab-properties slot consumers — the sub-component handles it internally
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Proxy for slot consumers: exposes isPropertyEditable from the tab sub-component.
|
|
367
|
+
* @param {string} key - Property key
|
|
368
|
+
* @param {*} value - Current property value
|
|
369
|
+
*/
|
|
370
|
+
isPropertyEditable(key, value) {
|
|
371
|
+
const tab = this.$refs.propertiesTab
|
|
372
|
+
if (tab) return tab.isPropertyEditable(key, value)
|
|
373
|
+
return true
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Proxy for slot consumers.
|
|
378
|
+
* @param {string} key - Property key
|
|
379
|
+
*/
|
|
380
|
+
getPropertyDisplayName(key) {
|
|
381
|
+
const tab = this.$refs.propertiesTab
|
|
382
|
+
if (tab) return tab.getPropertyDisplayName(key)
|
|
383
|
+
return key
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Proxy for slot consumers.
|
|
388
|
+
* @param {string} key - Property key
|
|
389
|
+
* @param {*} value - Current property value
|
|
390
|
+
*/
|
|
391
|
+
getPropertyValidationClass(key, value) {
|
|
392
|
+
const tab = this.$refs.propertiesTab
|
|
393
|
+
if (tab) return tab.getPropertyValidationClass(key, value)
|
|
394
|
+
return ''
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
updateFormFromJson() {
|
|
398
|
+
if (this.isInternalUpdate) return
|
|
399
|
+
try {
|
|
400
|
+
this.isInternalUpdate = true
|
|
401
|
+
this.formData = JSON.parse(this.jsonData)
|
|
402
|
+
} catch {
|
|
403
|
+
// Keep previous formData
|
|
404
|
+
} finally {
|
|
405
|
+
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
updateJsonFromForm() {
|
|
410
|
+
if (this.isInternalUpdate) return
|
|
411
|
+
try {
|
|
412
|
+
this.isInternalUpdate = true
|
|
413
|
+
this.jsonData = JSON.stringify(this.formData, null, 2)
|
|
414
|
+
} catch {
|
|
415
|
+
// Ignore
|
|
416
|
+
} finally {
|
|
417
|
+
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
updateJsonFromExternal(newJson) {
|
|
422
|
+
this.jsonData = newJson
|
|
423
|
+
if (this.isValidJson(newJson)) this.updateFormFromJson()
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
isValidJson(str) {
|
|
427
|
+
if (!str || !str.trim()) return false
|
|
428
|
+
try {
|
|
429
|
+
JSON.parse(str)
|
|
430
|
+
return true
|
|
431
|
+
} catch {
|
|
432
|
+
return false
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
formatJSON() {
|
|
437
|
+
try {
|
|
438
|
+
if (this.jsonData) {
|
|
439
|
+
const parsed = JSON.parse(this.jsonData)
|
|
440
|
+
this.jsonData = JSON.stringify(parsed, null, 2)
|
|
441
|
+
if (!this.isInternalUpdate) {
|
|
442
|
+
this.isInternalUpdate = true
|
|
443
|
+
this.formData = parsed
|
|
444
|
+
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch {
|
|
448
|
+
// Keep invalid JSON as-is
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
onFormatResult(parsed) {
|
|
453
|
+
if (!this.isInternalUpdate) {
|
|
454
|
+
this.isInternalUpdate = true
|
|
455
|
+
this.formData = parsed
|
|
456
|
+
this.$nextTick(() => { this.isInternalUpdate = false })
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
validate() {
|
|
461
|
+
const newErrors = {}
|
|
462
|
+
for (const field of this.resolvedFields) {
|
|
463
|
+
const value = this.formData[field.key]
|
|
464
|
+
if (field.required && (value == null || value === '')) {
|
|
465
|
+
newErrors[field.key] = `${field.label} is required.`
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
this.errors = newErrors
|
|
469
|
+
return Object.keys(newErrors).length === 0
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
executeConfirm() {
|
|
473
|
+
if (!this.validate()) return
|
|
474
|
+
if (this.isDataTabActive && !this.isValidJson(this.jsonData)) return
|
|
475
|
+
this.$emit('confirm', JSON.parse(JSON.stringify(this.formData)))
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
setResult(resultData) {
|
|
479
|
+
this.loading = false
|
|
480
|
+
this.result = resultData
|
|
481
|
+
if (resultData?.success) {
|
|
482
|
+
this.closeTimeout = setTimeout(() => this.$emit('close'), 2000)
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
</script>
|
|
488
|
+
|
|
489
|
+
<style scoped>
|
|
490
|
+
.cn-advanced-form-dialog__form {
|
|
491
|
+
display: flex;
|
|
492
|
+
flex-direction: column;
|
|
493
|
+
gap: 8px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.cn-advanced-form-dialog__tabs {
|
|
497
|
+
display: flex;
|
|
498
|
+
flex-direction: column;
|
|
499
|
+
gap: 12px;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/* Bootstrap-Vue tab styling to match ViewObject */
|
|
503
|
+
.tabContainer {
|
|
504
|
+
margin-top: 20px;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.tabContainer > * ul > li {
|
|
508
|
+
display: flex;
|
|
509
|
+
flex: 1;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.tabContainer > * ul > li:hover {
|
|
513
|
+
background-color: var(--color-background-hover);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.tabContainer > * ul > li > a {
|
|
517
|
+
flex: 1;
|
|
518
|
+
text-align: center;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.tabContainer > * ul > li > .active {
|
|
522
|
+
background: transparent !important;
|
|
523
|
+
color: var(--color-main-text) !important;
|
|
524
|
+
border-bottom: var(--default-grid-baseline) solid var(--color-primary-element) !important;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.tabContainer > * ul[role="tablist"] {
|
|
528
|
+
display: flex;
|
|
529
|
+
margin: 10px 8px 0 8px;
|
|
530
|
+
justify-content: space-between;
|
|
531
|
+
border-bottom: 1px solid var(--color-border);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.tabContainer > * ul[role="tablist"] > * a[role="tab"] {
|
|
535
|
+
padding-inline-start: 10px;
|
|
536
|
+
padding-inline-end: 10px;
|
|
537
|
+
padding-block-start: 10px;
|
|
538
|
+
padding-block-end: 10px;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.tabContainer > * div[role="tabpanel"] {
|
|
542
|
+
margin-block-start: var(--OR-margin-10);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
:deep(.nav-tabs) {
|
|
546
|
+
border-bottom: 1px solid var(--color-border);
|
|
547
|
+
margin-bottom: 15px;
|
|
548
|
+
display: flex;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
:deep(.nav-tabs .nav-item) {
|
|
552
|
+
display: flex;
|
|
553
|
+
flex: 1;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
:deep(.nav-tabs .nav-link) {
|
|
557
|
+
flex: 1;
|
|
558
|
+
text-align: center;
|
|
559
|
+
border: none;
|
|
560
|
+
border-bottom: 2px solid transparent;
|
|
561
|
+
color: var(--color-text-maxcontrast);
|
|
562
|
+
padding: 8px 16px;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
:deep(.nav-tabs .nav-link.active) {
|
|
566
|
+
color: var(--color-main-text);
|
|
567
|
+
border-bottom: 2px solid var(--color-primary);
|
|
568
|
+
background-color: transparent;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
:deep(.nav-tabs .nav-link:hover) {
|
|
572
|
+
border-bottom: 2px solid var(--color-border);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
:deep(.tab-content) {
|
|
576
|
+
padding: 16px;
|
|
577
|
+
background-color: var(--color-main-background);
|
|
578
|
+
}
|
|
579
|
+
</style>
|