@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.
Files changed (71) hide show
  1. package/dist/nextcloud-vue.cjs +67614 -0
  2. package/dist/nextcloud-vue.cjs.js +9559 -8983
  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 +1231 -1231
  6. package/dist/nextcloud-vue.esm.js +9559 -8983
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +14 -5
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -235
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -579
  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 +418 -418
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -247
  15. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  16. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  17. package/src/components/CnChartWidget/CnChartWidget.vue +320 -320
  18. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  19. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -250
  20. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -225
  21. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -390
  22. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  23. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -170
  24. package/src/components/CnDetailCard/CnDetailCard.vue +214 -214
  25. package/src/components/CnDetailPage/CnDetailPage.vue +285 -281
  26. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -231
  27. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  28. package/src/components/CnFormDialog/CnFormDialog.vue +302 -11
  29. package/src/components/CnIcon/CnIcon.vue +89 -89
  30. package/src/components/CnIndexPage/CnIndexPage.vue +884 -874
  31. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -503
  32. package/src/components/CnItemCard/CnItemCard.vue +132 -132
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  34. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  35. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  36. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  37. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  38. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  39. package/src/components/CnNoteCard/CnNoteCard.vue +149 -149
  40. package/src/components/CnNotesCard/CnNotesCard.vue +413 -413
  41. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  42. package/src/components/CnObjectCard/eslint-setup.md +235 -0
  43. package/src/components/CnObjectCard/package.json-or.json +132 -0
  44. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -876
  45. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  46. package/src/components/CnPagination/CnPagination.vue +252 -252
  47. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -792
  48. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  49. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -226
  50. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -787
  51. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -305
  52. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -1398
  53. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -236
  54. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  55. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  56. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -420
  57. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  58. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -540
  59. package/src/components/CnTasksCard/CnTasksCard.vue +373 -373
  60. package/src/components/CnTileWidget/CnTileWidget.vue +159 -159
  61. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -292
  62. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -435
  63. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  64. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -180
  65. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -211
  66. package/src/index.js +1 -1
  67. package/src/types/notification.d.ts +13 -13
  68. package/src/types/organisation.d.ts +15 -15
  69. package/src/types/schema.d.ts +13 -13
  70. package/src/types/task.d.ts +6 -6
  71. 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>