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

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 (208) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +79416 -7715
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +3583 -504
  6. package/dist/nextcloud-vue.esm.js +79343 -7692
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/l10n/en.json +164 -0
  9. package/l10n/nl.json +164 -0
  10. package/package.json +104 -63
  11. package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
  12. package/src/components/CnActionsBar/index.js +1 -0
  13. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -0
  14. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  15. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
  17. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  18. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  19. package/src/components/CnCard/CnCard.vue +415 -0
  20. package/src/components/CnCard/index.js +1 -0
  21. package/src/components/CnCardGrid/CnCardGrid.vue +156 -152
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  24. package/src/components/CnCellRenderer/index.js +1 -1
  25. package/src/components/CnChartWidget/CnChartWidget.vue +346 -0
  26. package/src/components/CnChartWidget/index.js +1 -0
  27. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  28. package/src/components/CnConfigurationCard/index.js +1 -1
  29. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  30. package/src/components/CnContextMenu/index.js +1 -0
  31. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -0
  32. package/src/components/CnCopyDialog/index.js +1 -0
  33. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  34. package/src/components/CnDashboardGrid/index.js +1 -0
  35. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -0
  36. package/src/components/CnDashboardPage/index.js +1 -0
  37. package/src/components/CnDataTable/CnDataTable.vue +362 -354
  38. package/src/components/CnDataTable/index.js +1 -1
  39. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  40. package/src/components/CnDeleteDialog/index.js +1 -0
  41. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  42. package/src/components/CnDetailCard/index.js +1 -0
  43. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -0
  44. package/src/components/CnDetailGrid/index.js +1 -0
  45. package/src/components/CnDetailPage/CnDetailPage.vue +432 -0
  46. package/src/components/CnDetailPage/index.js +1 -0
  47. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -223
  48. package/src/components/CnFacetSidebar/index.js +1 -1
  49. package/src/components/CnFilterBar/CnFilterBar.vue +153 -152
  50. package/src/components/CnFilterBar/index.js +1 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -0
  52. package/src/components/CnFormDialog/index.js +1 -0
  53. package/src/components/CnIcon/CnIcon.vue +89 -0
  54. package/src/components/CnIcon/index.js +1 -0
  55. package/src/components/CnIndexPage/CnIndexPage.vue +980 -682
  56. package/src/components/CnIndexPage/index.js +1 -1
  57. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -0
  58. package/src/components/CnIndexSidebar/index.js +1 -0
  59. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  60. package/src/components/CnInfoWidget/index.js +1 -0
  61. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  62. package/src/components/CnItemCard/index.js +1 -0
  63. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -0
  64. package/src/components/CnJsonViewer/index.js +1 -0
  65. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -89
  66. package/src/components/CnKpiGrid/index.js +1 -1
  67. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -160
  68. package/src/components/CnMassActionBar/index.js +1 -1
  69. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -320
  70. package/src/components/CnMassCopyDialog/index.js +1 -1
  71. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -238
  72. package/src/components/CnMassDeleteDialog/index.js +1 -1
  73. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -190
  74. package/src/components/CnMassExportDialog/index.js +1 -1
  75. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -491
  76. package/src/components/CnMassImportDialog/index.js +1 -1
  77. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  78. package/src/components/CnNoteCard/index.js +1 -0
  79. package/src/components/CnNotesCard/CnNotesCard.vue +416 -0
  80. package/src/components/CnNotesCard/index.js +1 -0
  81. package/src/components/CnObjectCard/CnObjectCard.vue +294 -292
  82. package/src/components/CnObjectCard/index.js +1 -1
  83. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -0
  84. package/src/components/CnObjectDataWidget/index.js +1 -0
  85. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -0
  86. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  87. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -0
  88. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -0
  89. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -0
  90. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -0
  91. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -0
  92. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -0
  93. package/src/components/CnObjectSidebar/index.js +6 -0
  94. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  95. package/src/components/CnPageHeader/index.js +1 -0
  96. package/src/components/CnPagination/CnPagination.vue +253 -252
  97. package/src/components/CnPagination/index.js +1 -1
  98. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  99. package/src/components/CnProgressBar/index.js +1 -0
  100. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -0
  101. package/src/components/CnRegisterMapping/index.js +1 -0
  102. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  103. package/src/components/CnRowActions/index.js +1 -1
  104. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -0
  106. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  107. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  108. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  109. package/src/components/CnSchemaFormDialog/index.js +1 -0
  110. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  111. package/src/components/CnSettingsCard/index.js +1 -1
  112. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -266
  113. package/src/components/CnSettingsSection/index.js +1 -1
  114. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -366
  115. package/src/components/CnStatsBlock/index.js +1 -1
  116. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -0
  117. package/src/components/CnStatsPanel/index.js +1 -0
  118. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -77
  119. package/src/components/CnStatusBadge/index.js +1 -1
  120. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -0
  121. package/src/components/CnTabbedFormDialog/index.js +1 -0
  122. package/src/components/CnTableWidget/CnTableWidget.vue +333 -0
  123. package/src/components/CnTableWidget/index.js +1 -0
  124. package/src/components/CnTasksCard/CnTasksCard.vue +374 -0
  125. package/src/components/CnTasksCard/index.js +1 -0
  126. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  127. package/src/components/CnTileWidget/index.js +1 -0
  128. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -0
  129. package/src/components/CnTimelineStages/index.js +1 -0
  130. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -0
  131. package/src/components/CnUserActionMenu/index.js +1 -0
  132. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -312
  133. package/src/components/CnVersionInfoCard/index.js +1 -1
  134. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  135. package/src/components/CnWidgetRenderer/index.js +1 -0
  136. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -0
  137. package/src/components/CnWidgetWrapper/index.js +1 -0
  138. package/src/components/index.js +57 -25
  139. package/src/composables/index.js +5 -3
  140. package/src/composables/useContextMenu.js +126 -0
  141. package/src/composables/useDashboardView.js +286 -0
  142. package/src/composables/useDetailView.js +290 -132
  143. package/src/composables/useListView.js +364 -153
  144. package/src/composables/useSubResource.js +142 -142
  145. package/src/constants/metadata.js +30 -0
  146. package/src/css/CnSchemaFormDialog.css +546 -0
  147. package/src/css/__sample_nextcloud_tokens.css +110 -0
  148. package/src/css/actions-bar.css +54 -0
  149. package/src/css/badge.css +83 -51
  150. package/src/css/card.css +129 -128
  151. package/src/css/context-menu.css +20 -0
  152. package/src/css/dashboard.css +70 -0
  153. package/src/css/detail-page.css +235 -0
  154. package/src/css/detail.css +68 -68
  155. package/src/css/index-page.css +44 -0
  156. package/src/css/index-sidebar.css +193 -0
  157. package/src/css/index.css +17 -8
  158. package/src/css/layout.css +90 -90
  159. package/src/css/page-header.css +35 -0
  160. package/src/css/pagination.css +72 -72
  161. package/src/css/table.css +142 -143
  162. package/src/css/timeline-stages.css +220 -0
  163. package/src/css/utilities.css +46 -46
  164. package/src/index.js +95 -50
  165. package/src/l10n/index.js +12 -0
  166. package/src/mixins/gridLayout.js +118 -0
  167. package/src/store/createCrudStore.d.ts +350 -0
  168. package/src/store/createCrudStore.js +413 -0
  169. package/src/store/createSubResourcePlugin.js +125 -135
  170. package/src/store/index.js +4 -3
  171. package/src/store/pluginMerge.js +55 -0
  172. package/src/store/plugins/auditTrails.js +357 -17
  173. package/src/store/plugins/files.js +250 -186
  174. package/src/store/plugins/index.js +8 -4
  175. package/src/store/plugins/lifecycle.js +180 -180
  176. package/src/store/plugins/logs.d.ts +22 -0
  177. package/src/store/plugins/logs.js +172 -0
  178. package/src/store/plugins/registerMapping.js +195 -0
  179. package/src/store/plugins/relations.js +68 -68
  180. package/src/store/plugins/search.js +385 -0
  181. package/src/store/plugins/selection.js +104 -0
  182. package/src/store/useObjectStore.js +793 -625
  183. package/src/types/auditTrail.d.ts +32 -32
  184. package/src/types/file.d.ts +23 -23
  185. package/src/types/index.d.ts +67 -35
  186. package/src/types/notification.d.ts +36 -36
  187. package/src/types/object.d.ts +40 -40
  188. package/src/types/organisation.d.ts +41 -41
  189. package/src/types/register.d.ts +25 -25
  190. package/src/types/schema.d.ts +39 -39
  191. package/src/types/shared.d.ts +79 -79
  192. package/src/types/source.d.ts +14 -14
  193. package/src/types/task.d.ts +31 -31
  194. package/src/utils/errors.js +96 -96
  195. package/src/utils/getTheme.js +9 -0
  196. package/src/utils/headers.js +80 -44
  197. package/src/utils/id.js +13 -0
  198. package/src/utils/index.js +4 -3
  199. package/src/utils/schema.js +423 -287
  200. package/src/utils/widgetVisibility.js +162 -0
  201. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  202. package/src/components/CnDetailViewLayout/index.js +0 -1
  203. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  204. package/src/components/CnEmptyState/index.js +0 -1
  205. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  206. package/src/components/CnListViewLayout/index.js +0 -1
  207. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  208. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -0,0 +1,854 @@
1
+ <!--
2
+ CnObjectDataWidget — Schema-driven editable data grid widget.
3
+
4
+ Displays object properties in a CSS grid layout, auto-generated from a JSON Schema.
5
+ Each cell shows a formatted value. Clicking an editable cell opens an inline editor
6
+ matching the property type (text, select, date, checkbox, textarea, etc.).
7
+ When any value is changed, a Save button appears in the widget header.
8
+
9
+ Supports per-property overrides for order, grid span, visibility, editability, and widget type.
10
+ -->
11
+ <template>
12
+ <CnDetailCard :title="title" :icon="iconComponent">
13
+ <template #actions>
14
+ <NcButton
15
+ v-if="isDirty"
16
+ type="primary"
17
+ :disabled="saving"
18
+ @click="save">
19
+ <template #icon>
20
+ <NcLoadingIcon v-if="saving" :size="20" />
21
+ <ContentSaveOutline v-else :size="20" />
22
+ </template>
23
+ {{ saveLabel }}
24
+ </NcButton>
25
+ <NcButton
26
+ v-if="isDirty"
27
+ @click="discard">
28
+ {{ discardLabel }}
29
+ </NcButton>
30
+ <slot name="actions" />
31
+ </template>
32
+
33
+ <!-- Empty state -->
34
+ <div v-if="!resolvedFields.length" class="cn-object-data-widget__empty">
35
+ {{ emptyLabel }}
36
+ </div>
37
+
38
+ <!-- Grid -->
39
+ <div
40
+ v-else
41
+ class="cn-object-data-widget__grid"
42
+ :style="gridStyle">
43
+ <div
44
+ v-for="field in resolvedFields"
45
+ :key="field.key"
46
+ class="cn-object-data-widget__cell"
47
+ :style="cellStyle(field)">
48
+ <!-- Label -->
49
+ <div class="cn-object-data-widget__label">
50
+ {{ field.label }}
51
+ </div>
52
+
53
+ <!-- Editing mode for this field -->
54
+ <div
55
+ v-if="editingField === field.key"
56
+ class="cn-object-data-widget__editor">
57
+ <!-- Per-field slot override -->
58
+ <slot
59
+ v-if="$scopedSlots['field-' + field.key]"
60
+ :name="'field-' + field.key"
61
+ :field="field"
62
+ :value="editData[field.key]"
63
+ :update="(val) => updateField(field.key, val)"
64
+ :cancel="cancelEdit" />
65
+
66
+ <!-- Auto-generated editor -->
67
+ <template v-else>
68
+ <!-- Text / Email / URL -->
69
+ <NcTextField
70
+ v-if="field.widget === 'text' || field.widget === 'email' || field.widget === 'url'"
71
+ ref="activeEditor"
72
+ :value="editData[field.key] != null ? String(editData[field.key]) : ''"
73
+ :type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
74
+ :placeholder="field.description"
75
+ @update:value="val => updateField(field.key, val)"
76
+ @keydown.native.enter="commitEdit"
77
+ @keydown.native.escape="cancelEdit" />
78
+
79
+ <!-- Number -->
80
+ <NcTextField
81
+ v-else-if="field.widget === 'number'"
82
+ ref="activeEditor"
83
+ :value="editData[field.key] != null ? String(editData[field.key]) : ''"
84
+ type="number"
85
+ :placeholder="field.description"
86
+ @update:value="val => updateField(field.key, val !== '' ? Number(val) : null)"
87
+ @keydown.native.enter="commitEdit"
88
+ @keydown.native.escape="cancelEdit" />
89
+
90
+ <!-- Textarea -->
91
+ <textarea
92
+ v-else-if="field.widget === 'textarea'"
93
+ ref="activeEditor"
94
+ class="cn-object-data-widget__textarea"
95
+ :value="editData[field.key] || ''"
96
+ :placeholder="field.description"
97
+ rows="4"
98
+ @input="updateField(field.key, $event.target.value)"
99
+ @keydown.escape="cancelEdit" />
100
+
101
+ <!-- Select -->
102
+ <NcSelect
103
+ v-else-if="field.widget === 'select'"
104
+ ref="activeEditor"
105
+ :options="getSelectOptions(field)"
106
+ :value="getSelectedOption(field)"
107
+ :clearable="!field.required"
108
+ @input="onSelectChange(field, $event)"
109
+ @close="commitEdit" />
110
+
111
+ <!-- Multiselect -->
112
+ <NcSelect
113
+ v-else-if="field.widget === 'multiselect'"
114
+ ref="activeEditor"
115
+ :options="getMultiselectOptions(field)"
116
+ :value="getSelectedMultiselectOptions(field)"
117
+ :multiple="true"
118
+ :clearable="true"
119
+ @input="onMultiselectChange(field, $event)" />
120
+
121
+ <!-- Tags -->
122
+ <NcSelect
123
+ v-else-if="field.widget === 'tags'"
124
+ ref="activeEditor"
125
+ :value="editData[field.key] || []"
126
+ :multiple="true"
127
+ :taggable="true"
128
+ :clearable="true"
129
+ @input="val => updateField(field.key, val)" />
130
+
131
+ <!-- Checkbox / Switch -->
132
+ <NcCheckboxRadioSwitch
133
+ v-else-if="field.widget === 'checkbox'"
134
+ :checked="!!editData[field.key]"
135
+ type="switch"
136
+ @update:checked="val => { updateField(field.key, val); commitEdit() }">
137
+ {{ editData[field.key] ? 'Yes' : 'No' }}
138
+ </NcCheckboxRadioSwitch>
139
+
140
+ <!-- Date -->
141
+ <NcTextField
142
+ v-else-if="field.widget === 'date'"
143
+ ref="activeEditor"
144
+ :value="editData[field.key] || ''"
145
+ type="date"
146
+ @update:value="val => updateField(field.key, val)"
147
+ @keydown.native.enter="commitEdit"
148
+ @keydown.native.escape="cancelEdit" />
149
+
150
+ <!-- Datetime -->
151
+ <NcTextField
152
+ v-else-if="field.widget === 'datetime'"
153
+ ref="activeEditor"
154
+ :value="editData[field.key] || ''"
155
+ type="datetime-local"
156
+ @update:value="val => updateField(field.key, val)"
157
+ @keydown.native.enter="commitEdit"
158
+ @keydown.native.escape="cancelEdit" />
159
+
160
+ <!-- Fallback: text -->
161
+ <NcTextField
162
+ v-else
163
+ ref="activeEditor"
164
+ :value="editData[field.key] != null ? String(editData[field.key]) : ''"
165
+ :placeholder="field.description"
166
+ @update:value="val => updateField(field.key, val)"
167
+ @keydown.native.enter="commitEdit"
168
+ @keydown.native.escape="cancelEdit" />
169
+ </template>
170
+
171
+ <!-- Confirm/Cancel for non-auto-committing editors -->
172
+ <div
173
+ v-if="field.widget !== 'checkbox'"
174
+ class="cn-object-data-widget__editor-actions">
175
+ <NcButton type="tertiary-no-background" @click="commitEdit">
176
+ <template #icon>
177
+ <Check :size="20" />
178
+ </template>
179
+ </NcButton>
180
+ <NcButton type="tertiary-no-background" @click="cancelEdit">
181
+ <template #icon>
182
+ <Close :size="20" />
183
+ </template>
184
+ </NcButton>
185
+ </div>
186
+ </div>
187
+
188
+ <!-- Display mode -->
189
+ <div
190
+ v-else
191
+ class="cn-object-data-widget__value"
192
+ :class="{
193
+ 'cn-object-data-widget__value--editable': isEditable(field),
194
+ 'cn-object-data-widget__value--empty': isValueEmpty(field.key),
195
+ }"
196
+ :tabindex="isEditable(field) ? 0 : -1"
197
+ :role="isEditable(field) ? 'button' : undefined"
198
+ :aria-label="isEditable(field) ? 'Click to edit ' + field.label : undefined"
199
+ @click="isEditable(field) && startEdit(field)"
200
+ @keydown.enter="isEditable(field) && startEdit(field)">
201
+ <!-- Per-field display slot override -->
202
+ <slot
203
+ v-if="$scopedSlots['display-' + field.key]"
204
+ :name="'display-' + field.key"
205
+ :field="field"
206
+ :value="displayValues[field.key]"
207
+ :raw="objectData[field.key]" />
208
+ <template v-else>
209
+ {{ displayValues[field.key] }}
210
+ </template>
211
+ <Pencil
212
+ v-if="isEditable(field)"
213
+ class="cn-object-data-widget__edit-icon"
214
+ :size="14" />
215
+ </div>
216
+ </div>
217
+ </div>
218
+ </CnDetailCard>
219
+ </template>
220
+
221
+ <script>
222
+ import { translate as t } from '@nextcloud/l10n'
223
+ import { NcButton, NcLoadingIcon, NcTextField, NcSelect, NcCheckboxRadioSwitch } from '@nextcloud/vue'
224
+ import { CnDetailCard } from '../CnDetailCard/index.js'
225
+ import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
226
+ import Pencil from 'vue-material-design-icons/Pencil.vue'
227
+ import Check from 'vue-material-design-icons/Check.vue'
228
+ import Close from 'vue-material-design-icons/Close.vue'
229
+ import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
230
+
231
+ /**
232
+ * CnObjectDataWidget — Schema-driven editable data grid widget.
233
+ *
234
+ * Renders object properties in a configurable CSS grid. Each property is displayed
235
+ * as a label-value cell. Editable cells can be clicked to switch to inline editing.
236
+ * Uses the objectStore to persist changes.
237
+ *
238
+ * @example Basic usage
239
+ * <CnObjectDataWidget
240
+ * title="Character Info"
241
+ * :schema="schema"
242
+ * :object-data="character"
243
+ * object-type="characters"
244
+ * :overrides="{
245
+ * name: { order: 1, gridColumn: 2 },
246
+ * description: { order: 2, gridColumn: 3, gridRow: 2 },
247
+ * status: { order: 3 },
248
+ * internalId: { hidden: true },
249
+ * }" />
250
+ *
251
+ * @example Read-only mode
252
+ * <CnObjectDataWidget
253
+ * title="Summary"
254
+ * :schema="schema"
255
+ * :object-data="item"
256
+ * :editable="false" />
257
+ */
258
+ export default {
259
+ name: 'CnObjectDataWidget',
260
+
261
+ components: {
262
+ NcButton,
263
+ NcLoadingIcon,
264
+ NcTextField,
265
+ NcSelect,
266
+ NcCheckboxRadioSwitch,
267
+ CnDetailCard,
268
+ ContentSaveOutline,
269
+ Pencil,
270
+ Check,
271
+ Close,
272
+ },
273
+
274
+ props: {
275
+ /** Widget title shown in the card header */
276
+ title: {
277
+ type: String,
278
+ default: () => t('nextcloud-vue', 'Data'),
279
+ },
280
+ /** Optional MDI icon component for the header */
281
+ icon: {
282
+ type: [Object, Function],
283
+ default: null,
284
+ },
285
+ /**
286
+ * The JSON Schema describing the object's properties.
287
+ * Must have a `properties` field.
288
+ */
289
+ schema: {
290
+ type: Object,
291
+ required: true,
292
+ },
293
+ /**
294
+ * The object data to display and edit.
295
+ * Keys should match the schema property keys.
296
+ */
297
+ objectData: {
298
+ type: Object,
299
+ required: true,
300
+ },
301
+ /**
302
+ * The registered object type slug in the objectStore.
303
+ * Required for saving via objectStore.saveObject().
304
+ */
305
+ objectType: {
306
+ type: String,
307
+ default: '',
308
+ },
309
+ /**
310
+ * Optional objectStore instance. When provided, used directly for saving.
311
+ * When not provided, falls back to auto-detecting the store via Pinia.
312
+ */
313
+ store: {
314
+ type: Object,
315
+ default: null,
316
+ },
317
+ /**
318
+ * Per-property configuration overrides.
319
+ * Keys are property names, values are override objects.
320
+ *
321
+ * Supported overrides:
322
+ * - `order` (number) — Display order (lower = first)
323
+ * - `gridColumn` (number) — Number of grid columns to span (default 1)
324
+ * - `gridRow` (number) — Number of grid rows to span (default 1)
325
+ * - `hidden` (boolean) — Hide this property
326
+ * - `editable` (boolean) — Override editability (default: based on schema readOnly)
327
+ * - `label` (string) — Override the display label
328
+ * - `widget` (string) — Override the widget type for editing
329
+ *
330
+ * @type {{ [key: string]: { order?: number, gridColumn?: number, gridRow?: number, hidden?: boolean, editable?: boolean, label?: string, widget?: string } }}
331
+ */
332
+ overrides: {
333
+ type: Object,
334
+ default: () => ({}),
335
+ },
336
+ /**
337
+ * Number of grid columns.
338
+ */
339
+ columns: {
340
+ type: Number,
341
+ default: 3,
342
+ },
343
+ /**
344
+ * Whether editing is enabled globally.
345
+ * When false, no fields are editable regardless of per-field settings.
346
+ */
347
+ editable: {
348
+ type: Boolean,
349
+ default: true,
350
+ },
351
+ /**
352
+ * Properties to exclude from display.
353
+ * @type {string[]}
354
+ */
355
+ exclude: {
356
+ type: Array,
357
+ default: () => [],
358
+ },
359
+ /**
360
+ * Properties to include (whitelist mode). If provided, only these are shown.
361
+ * @type {string[]}
362
+ */
363
+ include: {
364
+ type: Array,
365
+ default: () => null,
366
+ },
367
+ /** Label for the save button */
368
+ saveLabel: {
369
+ type: String,
370
+ default: () => t('nextcloud-vue', 'Save'),
371
+ },
372
+ /** Label for the discard button */
373
+ discardLabel: {
374
+ type: String,
375
+ default: () => t('nextcloud-vue', 'Discard'),
376
+ },
377
+ /** Label shown when no properties to display */
378
+ emptyLabel: {
379
+ type: String,
380
+ default: () => t('nextcloud-vue', 'No data available'),
381
+ },
382
+ },
383
+
384
+ data() {
385
+ return {
386
+ /** Currently editing field key, or null */
387
+ editingField: null,
388
+ /** Working copy of changed field values */
389
+ editData: {},
390
+ /** Set of field keys that have been modified */
391
+ dirtyFields: {},
392
+ /** Whether a save is in progress */
393
+ saving: false,
394
+ }
395
+ },
396
+
397
+ computed: {
398
+ iconComponent() {
399
+ return this.icon
400
+ },
401
+
402
+ /**
403
+ * Resolved field definitions from schema + overrides.
404
+ * Sorted by order, filtered by hidden/exclude/include.
405
+ */
406
+ resolvedFields() {
407
+ // Build override map for fieldsFromSchema
408
+ const fieldOverrides = {}
409
+ for (const [key, cfg] of Object.entries(this.overrides)) {
410
+ const override = {}
411
+ if (cfg.label) override.label = cfg.label
412
+ if (cfg.widget) override.widget = cfg.widget
413
+ if (typeof cfg.order === 'number') override.order = cfg.order
414
+ if (cfg.enum) override.enum = cfg.enum
415
+ if (Object.keys(override).length > 0) {
416
+ fieldOverrides[key] = override
417
+ }
418
+ }
419
+
420
+ // Build exclude list: merge prop exclude + hidden overrides
421
+ const excludeList = [...this.exclude]
422
+ for (const [key, cfg] of Object.entries(this.overrides)) {
423
+ if (cfg.hidden === true && !excludeList.includes(key)) {
424
+ excludeList.push(key)
425
+ }
426
+ }
427
+
428
+ const fields = fieldsFromSchema(this.schema, {
429
+ exclude: excludeList,
430
+ include: this.include,
431
+ overrides: fieldOverrides,
432
+ includeReadOnly: true,
433
+ })
434
+
435
+ // Attach grid span info from overrides
436
+ const result = fields.map(field => ({
437
+ ...field,
438
+ gridColumn: (this.overrides[field.key] && this.overrides[field.key].gridColumn) || 1,
439
+ gridRow: (this.overrides[field.key] && this.overrides[field.key].gridRow) || 1,
440
+ }))
441
+
442
+ // Re-sort by order after overrides are applied
443
+ // (fieldsFromSchema sorts before applying overrides, so order may have changed)
444
+ result.sort(function(a, b) {
445
+ const orderA = typeof a.order === 'number' ? a.order : Infinity
446
+ const orderB = typeof b.order === 'number' ? b.order : Infinity
447
+ if (orderA !== orderB) return orderA - orderB
448
+ return a.key.localeCompare(b.key)
449
+ })
450
+
451
+ return result
452
+ },
453
+
454
+ /**
455
+ * Formatted display values for each field.
456
+ */
457
+ displayValues() {
458
+ const values = {}
459
+ for (const field of this.resolvedFields) {
460
+ // Show pending edit value if dirty
461
+ const raw = field.key in this.dirtyFields
462
+ ? this.dirtyFields[field.key]
463
+ : this.objectData[field.key]
464
+ const prop = this.schema.properties && this.schema.properties[field.key]
465
+ values[field.key] = formatValue(raw, prop || {})
466
+ }
467
+ return values
468
+ },
469
+
470
+ /**
471
+ * Whether any fields have been modified.
472
+ */
473
+ isDirty() {
474
+ return Object.keys(this.dirtyFields).length > 0
475
+ },
476
+
477
+ /**
478
+ * CSS grid template for the container.
479
+ */
480
+ gridStyle() {
481
+ return {
482
+ 'grid-template-columns': `repeat(${this.columns}, 1fr)`,
483
+ }
484
+ },
485
+ },
486
+
487
+ watch: {
488
+ objectData: {
489
+ deep: true,
490
+ handler() {
491
+ // If external data changes (e.g. after save), clear dirty state
492
+ // for fields that now match the new data
493
+ for (const key of Object.keys(this.dirtyFields)) {
494
+ if (this.dirtyFields[key] === this.objectData[key]) {
495
+ const { [key]: _, ...rest } = this.dirtyFields
496
+ this.dirtyFields = rest
497
+ }
498
+ }
499
+ },
500
+ },
501
+ },
502
+
503
+ methods: {
504
+ /**
505
+ * Check if a field is editable.
506
+ * @param {object} field - Resolved field definition from resolvedFields
507
+ */
508
+ isEditable(field) {
509
+ if (!this.editable) return false
510
+ // Per-field override takes priority
511
+ const override = this.overrides[field.key]
512
+ if (override && typeof override.editable === 'boolean') {
513
+ return override.editable
514
+ }
515
+ // Schema readOnly
516
+ return !field.readOnly
517
+ },
518
+
519
+ /**
520
+ * Check if a field's current value is empty.
521
+ * @param {string} key - Field key to check
522
+ */
523
+ isValueEmpty(key) {
524
+ const val = key in this.dirtyFields
525
+ ? this.dirtyFields[key]
526
+ : this.objectData[key]
527
+ return val === null || val === undefined || val === '' || (Array.isArray(val) && val.length === 0)
528
+ },
529
+
530
+ /**
531
+ * Start inline editing for a field.
532
+ * @param {object} field - Resolved field definition from resolvedFields
533
+ */
534
+ startEdit(field) {
535
+ // Set working value: dirty value > current object value
536
+ const currentValue = field.key in this.dirtyFields
537
+ ? this.dirtyFields[field.key]
538
+ : this.objectData[field.key]
539
+ this.editData = { ...this.editData, [field.key]: currentValue }
540
+ this.editingField = field.key
541
+
542
+ this.$nextTick(() => {
543
+ // Focus the editor
544
+ const editor = this.$refs.activeEditor
545
+ if (editor) {
546
+ const el = Array.isArray(editor) ? editor[0] : editor
547
+ if (el && el.$el) {
548
+ const input = el.$el.querySelector('input, textarea, select')
549
+ if (input) input.focus()
550
+ } else if (el && el.focus) {
551
+ el.focus()
552
+ }
553
+ }
554
+ })
555
+ },
556
+
557
+ /**
558
+ * Update the working edit value for a field.
559
+ * @param {string} key - Field key to update
560
+ * @param {*} value - New value for the field
561
+ */
562
+ updateField(key, value) {
563
+ this.editData = { ...this.editData, [key]: value }
564
+ },
565
+
566
+ /**
567
+ * Commit the current inline edit — mark the field as dirty.
568
+ */
569
+ commitEdit() {
570
+ if (!this.editingField) return
571
+
572
+ const key = this.editingField
573
+ const newValue = this.editData[key]
574
+ const originalValue = this.objectData[key]
575
+
576
+ // Only mark dirty if actually changed
577
+ if (newValue !== originalValue) {
578
+ this.dirtyFields = { ...this.dirtyFields, [key]: newValue }
579
+ } else {
580
+ // Remove from dirty if reverted to original
581
+ const { [key]: _, ...rest } = this.dirtyFields
582
+ this.dirtyFields = rest
583
+ }
584
+
585
+ this.editingField = null
586
+ },
587
+
588
+ /**
589
+ * Cancel the current inline edit without saving.
590
+ */
591
+ cancelEdit() {
592
+ this.editingField = null
593
+ },
594
+
595
+ /**
596
+ * Discard all pending changes.
597
+ */
598
+ discard() {
599
+ this.dirtyFields = {}
600
+ this.editData = {}
601
+ this.editingField = null
602
+ this.$emit('discard')
603
+ },
604
+
605
+ /**
606
+ * Save all dirty fields via the objectStore or emit event.
607
+ */
608
+ async save() {
609
+ if (!this.isDirty) return
610
+
611
+ const mergedData = {
612
+ ...this.objectData,
613
+ ...this.dirtyFields,
614
+ }
615
+
616
+ this.saving = true
617
+
618
+ try {
619
+ // Try objectStore first if objectType is registered
620
+ if (this.objectType) {
621
+ const store = this._getObjectStore()
622
+ if (store) {
623
+ const result = await store.saveObject(this.objectType, mergedData)
624
+ if (result) {
625
+ this.dirtyFields = {}
626
+ this.editData = {}
627
+ this.$emit('saved', result)
628
+ } else {
629
+ const error = store.getError(this.objectType)
630
+ this.$emit('save-error', error)
631
+ }
632
+ return
633
+ }
634
+ }
635
+
636
+ // Fallback: emit for parent to handle
637
+ this.$emit('save', mergedData)
638
+ } finally {
639
+ this.saving = false
640
+ }
641
+ },
642
+
643
+ /**
644
+ * Get the objectStore instance.
645
+ * Uses the `store` prop if provided, otherwise tries Pinia auto-detection.
646
+ * @return {object|null}
647
+ */
648
+ _getObjectStore() {
649
+ // Prefer explicit store prop
650
+ if (this.store) return this.store
651
+
652
+ try {
653
+ // Dynamic import to avoid hard dependency
654
+ const pinia = this.$pinia
655
+ if (!pinia) return null
656
+ // Try to access the store — it must already be created by the consuming app
657
+ const { useObjectStore } = require('../../store/index.js')
658
+ return useObjectStore()
659
+ } catch {
660
+ return null
661
+ }
662
+ },
663
+
664
+ /**
665
+ * Compute CSS grid placement for a field cell.
666
+ * @param field
667
+ */
668
+ cellStyle(field) {
669
+ const style = {}
670
+ if (field.gridColumn > 1) {
671
+ style.gridColumn = `span ${field.gridColumn}`
672
+ }
673
+ if (field.gridRow > 1) {
674
+ style.gridRow = `span ${field.gridRow}`
675
+ }
676
+ return style
677
+ },
678
+
679
+ // ── Select helpers ──
680
+
681
+ /**
682
+ * Normalize an option to { id, label } format.
683
+ * Accepts plain strings or objects with id/label properties.
684
+ * @param val
685
+ */
686
+ _normalizeOption(val) {
687
+ if (val && typeof val === 'object' && val.id !== undefined) {
688
+ return { id: val.id, label: val.label || val.id }
689
+ }
690
+ return { id: val, label: String(val) }
691
+ },
692
+
693
+ getSelectOptions(field) {
694
+ if (field.enum) {
695
+ return field.enum.map(val => this._normalizeOption(val))
696
+ }
697
+ return []
698
+ },
699
+
700
+ getSelectedOption(field) {
701
+ const val = this.editData[field.key]
702
+ if (val === null || val === undefined) return null
703
+ // Find matching option from enum for proper label display
704
+ const options = this.getSelectOptions(field)
705
+ return options.find(opt => opt.id === val) || { id: val, label: String(val) }
706
+ },
707
+
708
+ onSelectChange(field, option) {
709
+ this.updateField(field.key, option ? option.id : null)
710
+ },
711
+
712
+ getMultiselectOptions(field) {
713
+ // Check override enum first, then schema items.enum
714
+ if (field.enum) {
715
+ return field.enum.map(val => this._normalizeOption(val))
716
+ }
717
+ const itemsEnum = field.items && field.items.enum
718
+ if (itemsEnum) {
719
+ return itemsEnum.map(val => this._normalizeOption(val))
720
+ }
721
+ return []
722
+ },
723
+
724
+ getSelectedMultiselectOptions(field) {
725
+ const val = this.editData[field.key]
726
+ if (!Array.isArray(val)) return []
727
+ // Map selected IDs to option objects with labels
728
+ const options = this.getMultiselectOptions(field)
729
+ return val.map(v => options.find(opt => opt.id === v) || { id: v, label: String(v) })
730
+ },
731
+
732
+ onMultiselectChange(field, selected) {
733
+ const values = Array.isArray(selected)
734
+ ? selected.map(opt => opt.id || opt)
735
+ : []
736
+ this.updateField(field.key, values)
737
+ },
738
+ },
739
+ }
740
+ </script>
741
+
742
+ <style scoped>
743
+ .cn-object-data-widget__grid {
744
+ display: grid;
745
+ gap: calc(2 * var(--default-grid-baseline, 4px)) calc(4 * var(--default-grid-baseline, 4px));
746
+ }
747
+
748
+ .cn-object-data-widget__cell {
749
+ display: flex;
750
+ flex-direction: column;
751
+ gap: 2px;
752
+ padding: calc(2 * var(--default-grid-baseline, 4px)) 0;
753
+ border-bottom: 1px solid var(--color-border-dark);
754
+ min-width: 0;
755
+ }
756
+
757
+ .cn-object-data-widget__label {
758
+ font-size: 0.85em;
759
+ color: var(--color-text-maxcontrast);
760
+ font-weight: 500;
761
+ }
762
+
763
+ .cn-object-data-widget__value {
764
+ font-size: 1em;
765
+ color: var(--color-main-text);
766
+ word-break: break-word;
767
+ position: relative;
768
+ padding-right: 20px;
769
+ }
770
+
771
+ .cn-object-data-widget__value--editable {
772
+ cursor: pointer;
773
+ border-radius: var(--border-radius);
774
+ padding: 4px 24px 4px 4px;
775
+ margin: -4px;
776
+ }
777
+
778
+ .cn-object-data-widget__value--editable:hover {
779
+ background: var(--color-background-dark);
780
+ }
781
+
782
+ .cn-object-data-widget__value--editable:focus-visible {
783
+ outline: 2px solid var(--color-primary-element);
784
+ outline-offset: 2px;
785
+ }
786
+
787
+ .cn-object-data-widget__value--empty {
788
+ color: var(--color-text-maxcontrast);
789
+ font-style: italic;
790
+ }
791
+
792
+ .cn-object-data-widget__edit-icon {
793
+ position: absolute;
794
+ right: 4px;
795
+ top: 50%;
796
+ transform: translateY(-50%);
797
+ color: var(--color-text-maxcontrast);
798
+ opacity: 0;
799
+ transition: opacity 0.15s ease;
800
+ }
801
+
802
+ .cn-object-data-widget__value--editable:hover .cn-object-data-widget__edit-icon,
803
+ .cn-object-data-widget__value--editable:focus-visible .cn-object-data-widget__edit-icon {
804
+ opacity: 1;
805
+ }
806
+
807
+ .cn-object-data-widget__editor {
808
+ display: flex;
809
+ flex-direction: column;
810
+ gap: 4px;
811
+ }
812
+
813
+ .cn-object-data-widget__editor-actions {
814
+ display: flex;
815
+ gap: 2px;
816
+ justify-content: flex-end;
817
+ }
818
+
819
+ .cn-object-data-widget__textarea {
820
+ width: 100%;
821
+ min-height: 80px;
822
+ padding: 8px;
823
+ border: 2px solid var(--color-border-dark);
824
+ border-radius: var(--border-radius);
825
+ background: var(--color-main-background);
826
+ color: var(--color-main-text);
827
+ font-family: inherit;
828
+ font-size: inherit;
829
+ resize: vertical;
830
+ }
831
+
832
+ .cn-object-data-widget__textarea:focus {
833
+ border-color: var(--color-primary-element);
834
+ outline: none;
835
+ }
836
+
837
+ .cn-object-data-widget__empty {
838
+ color: var(--color-text-maxcontrast);
839
+ font-style: italic;
840
+ padding: calc(2 * var(--default-grid-baseline, 4px));
841
+ }
842
+
843
+ /* Responsive: collapse to single column on narrow widths */
844
+ @media (max-width: 600px) {
845
+ .cn-object-data-widget__grid {
846
+ grid-template-columns: 1fr !important;
847
+ }
848
+
849
+ .cn-object-data-widget__cell {
850
+ grid-column: span 1 !important;
851
+ grid-row: span 1 !important;
852
+ }
853
+ }
854
+ </style>