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

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