@conduction/nextcloud-vue 2.56.0-beta.45 → 2.56.0-beta.47

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 (149) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  26. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  28. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  33. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  38. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  46. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  47. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
  48. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
  49. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
  51. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
  53. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
  54. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
  55. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
  56. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
  57. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
  58. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  59. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  60. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  62. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  64. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  65. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  66. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  67. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  69. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  70. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
  77. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
  78. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
  80. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
  81. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  82. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  83. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/l10n/en.json.js +45 -1
  92. package/dist/esm/l10n/en.json.js.map +1 -1
  93. package/dist/esm/l10n/nl.json.js +44 -1
  94. package/dist/esm/l10n/nl.json.js.map +1 -1
  95. package/dist/esm/library-style.js +1 -1
  96. package/dist/esm/nextcloud-vue.css +1182 -328
  97. package/dist/esm/utils/boardDate.js +35 -1
  98. package/dist/esm/utils/boardDate.js.map +1 -1
  99. package/dist/esm/utils/featureCatalog.js +135 -0
  100. package/dist/esm/utils/featureCatalog.js.map +1 -0
  101. package/dist/esm/utils/fillTemplate.js +84 -4
  102. package/dist/esm/utils/fillTemplate.js.map +1 -1
  103. package/dist/esm/utils/headerMeta.js +62 -12
  104. package/dist/esm/utils/headerMeta.js.map +1 -1
  105. package/dist/esm/utils/roadmapIssues.js +86 -0
  106. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  107. package/dist/esm/utils/userDisplayName.js +47 -0
  108. package/dist/esm/utils/userDisplayName.js.map +1 -0
  109. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  110. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  111. package/dist/nextcloud-vue.cjs.js +15618 -13097
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1182 -328
  114. package/l10n/en.json +45 -1
  115. package/l10n/nl.json +44 -1
  116. package/package.json +1 -1
  117. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  118. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  119. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  120. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  121. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
  122. package/src/components/CnDataTable/CnDataTable.vue +10 -39
  123. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  124. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  125. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  126. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  127. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  128. package/src/components/CnFormDialog/CnFormDialog.md +1 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
  130. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
  131. package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
  132. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  133. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  134. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  135. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
  136. package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
  137. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  138. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
  139. package/src/css/dialog.css +47 -0
  140. package/src/css/look-board-detail.css +8 -0
  141. package/src/css/look-board.css +33 -0
  142. package/src/schemas/app-manifest-v2.schema.json +67 -4
  143. package/src/utils/boardDate.js +34 -0
  144. package/src/utils/featureCatalog.js +132 -0
  145. package/src/utils/fillTemplate.js +83 -3
  146. package/src/utils/headerMeta.js +62 -12
  147. package/src/utils/roadmapIssues.js +82 -0
  148. package/src/utils/userDisplayName.js +54 -0
  149. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -91,586 +91,594 @@
91
91
  only match on the label TEXT — which is the thing under test
92
92
  the moment labels are translated, so the assertion passes by
93
93
  construction in whichever language it was written in. -->
94
- <div
95
- v-for="field in visibleFields"
96
- :key="field.key"
97
- :data-cn-field="field.key"
98
- class="cn-form-dialog__field"
99
- :class="{
100
- 'cn-form-dialog__field--wide': fieldSpansBothColumns(field),
101
- 'cn-form-field--half': isBoardLook && field.width === 'half',
102
- 'cn-form-field--invalid': isBoardLook && !!errors[field.key],
103
- }">
104
- <!-- Board look: the label sits above the control, the error between
105
- them; the control and the hint keep their place below. -->
106
- <CnFormField
107
- v-if="showsOuterLabel(field)"
108
- :controlId="'cn-form-' + field.key"
109
- :text="field.label"
110
- :optional="!field.required"
111
- :optionalLabel="optionalLabel"
112
- :error="errors[field.key] || ''"
113
- :errorId="errorIdFor(field)" />
114
- <!-- @slot field-{key} Replace one auto-generated field with your own control. -->
115
- <!-- @binding {object} field The field definition. -->
116
- <!-- @binding {*} value The field's current value. -->
117
- <!-- @binding {string} error The field's validation error, if any. -->
118
- <!-- @binding {Function} update-field Write the value: `updateField(key, value)`. -->
119
- <slot
120
- v-if="$slots['field-' + field.key]"
121
- :name="'field-' + field.key"
122
- :field="field"
123
- :value="formData[field.key]"
124
- :error="errors[field.key]"
125
- :updateField="updateField" />
126
-
127
- <!-- referenceType (AD-18): render the integration's
128
- single-entity widget instead of a plain input. -->
129
- <component
130
- :is="resolveReferenceWidget(field)"
131
- v-else-if="resolveReferenceWidget(field)"
132
- v-bind="referenceWidgetProps(field)"
133
- @input="value => updateField(field.key, value)" />
134
-
135
- <!-- referenceSemanticType (ADR-048): a field that declares a
136
- canonical semantic-type URI whose provider schema is NOT
137
- installed renders DISABLED with a mouse-over tooltip. When
138
- a provider IS installed the field is transformed upstream
139
- (applySemanticResolution → a `$ref` reference) so it flows
140
- through the normal select branch below; only the
141
- unresolved/loading case lands here. -->
94
+ <template v-for="field in visibleFields" :key="field.key">
95
+ <!-- A field whose `section` differs from the one before it opens a
96
+ new group under that heading, across both columns. -->
97
+ <h3
98
+ v-if="sectionStarts[field.key]"
99
+ class="cn-form-dialog__section"
100
+ data-testid="cn-form-dialog-section">
101
+ {{ sectionStarts[field.key] }}
102
+ </h3>
142
103
  <div
143
- v-else-if="field.referenceSemanticType && !isSemanticResolved(field)"
144
- class="cn-form-dialog__semantic-unresolved"
145
- :title="semanticUnavailableText(field)">
146
- <NcTextField
147
- :id="fieldControlId(field)"
148
- :label="fieldLabelText(field)"
149
- :labelOutside="isBoardLook"
150
- v-bind="fieldAria(field)"
151
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
152
- :helperText="isSemanticLoading(field) ? '' : semanticUnavailableText(field)"
153
- :disabled="true"
154
- :loading="isSemanticLoading(field)"
155
- :placeholder="field.description" />
156
- </div>
157
-
158
- <!-- Concept-scheme choice whose options could not be loaded: the
159
- plain text input it had before, with a line saying so. -->
160
- <div
161
- v-else-if="field.codeList && codedFailed[field.key]"
162
- class="cn-form-dialog__coded-fallback">
163
- <NcTextField
164
- :id="fieldControlId(field)"
165
- :label="fieldLabelText(field)"
166
- :labelOutside="isBoardLook"
167
- v-bind="fieldAria(field)"
168
- :modelValue="codedFallbackText(field)"
169
- :disabled="field.readOnly"
170
- :helperText="t('nextcloud-vue', 'The list of choices could not be loaded. Enter the value by hand.')"
171
- @update:modelValue="onCodedFallbackInput(field, $event)" />
172
- </div>
173
-
174
- <!-- Auto-generated field -->
175
- <template v-else>
176
- <!-- Text / Email / URL. The helper line lives outside the
177
- input (rather than on `helper-text`) so an over-long
178
- description can hang its info popover off it. -->
179
- <template v-if="field.widget === 'text' || field.widget === 'email' || field.widget === 'url'">
104
+ :data-cn-field="field.key"
105
+ class="cn-form-dialog__field"
106
+ :class="{
107
+ 'cn-form-dialog__field--wide': fieldSpansBothColumns(field),
108
+ 'cn-form-field--half': isBoardLook && field.width === 'half',
109
+ 'cn-form-field--invalid': isBoardLook && !!errors[field.key],
110
+ }">
111
+ <!-- Board look: the label sits above the control, the error between
112
+ them; the control and the hint keep their place below. -->
113
+ <CnFormField
114
+ v-if="showsOuterLabel(field)"
115
+ :controlId="'cn-form-' + field.key"
116
+ :text="field.label"
117
+ :optional="!field.required"
118
+ :optionalLabel="optionalLabel"
119
+ :error="errors[field.key] || ''"
120
+ :errorId="errorIdFor(field)" />
121
+ <!-- @slot field-{key} Replace one auto-generated field with your own control. -->
122
+ <!-- @binding {object} field The field definition. -->
123
+ <!-- @binding {*} value The field's current value. -->
124
+ <!-- @binding {string} error The field's validation error, if any. -->
125
+ <!-- @binding {Function} update-field Write the value: `updateField(key, value)`. -->
126
+ <slot
127
+ v-if="$slots['field-' + field.key]"
128
+ :name="'field-' + field.key"
129
+ :field="field"
130
+ :value="formData[field.key]"
131
+ :error="errors[field.key]"
132
+ :updateField="updateField" />
133
+
134
+ <!-- referenceType (AD-18): render the integration's
135
+ single-entity widget instead of a plain input. -->
136
+ <component
137
+ :is="resolveReferenceWidget(field)"
138
+ v-else-if="resolveReferenceWidget(field)"
139
+ v-bind="referenceWidgetProps(field)"
140
+ @input="value => updateField(field.key, value)" />
141
+
142
+ <!-- referenceSemanticType (ADR-048): a field that declares a
143
+ canonical semantic-type URI whose provider schema is NOT
144
+ installed renders DISABLED with a mouse-over tooltip. When
145
+ a provider IS installed the field is transformed upstream
146
+ (applySemanticResolution → a `$ref` reference) so it flows
147
+ through the normal select branch below; only the
148
+ unresolved/loading case lands here. -->
149
+ <div
150
+ v-else-if="field.referenceSemanticType && !isSemanticResolved(field)"
151
+ class="cn-form-dialog__semantic-unresolved"
152
+ :title="semanticUnavailableText(field)">
180
153
  <NcTextField
181
154
  :id="fieldControlId(field)"
182
155
  :label="fieldLabelText(field)"
183
156
  :labelOutside="isBoardLook"
184
157
  v-bind="fieldAria(field)"
185
158
  :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
186
- :error="!!errors[field.key]"
187
- :type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
188
- :disabled="field.readOnly"
189
- :placeholder="fieldPlaceholder(field)"
190
- @update:modelValue="value => updateField(field.key, value)" />
191
- <CnFieldHelper
192
- :id="helpId(field)"
193
- :text="field.description"
194
- :more="field.descriptionLong"
195
- :help="field.help"
196
- :label="field.label"
197
- :error="helperError(field)" />
198
- </template>
159
+ :helperText="isSemanticLoading(field) ? '' : semanticUnavailableText(field)"
160
+ :disabled="true"
161
+ :loading="isSemanticLoading(field)"
162
+ :placeholder="field.description" />
163
+ </div>
199
164
 
200
- <!-- Number -->
201
- <template v-else-if="field.widget === 'number'">
165
+ <!-- Concept-scheme choice whose options could not be loaded: the
166
+ plain text input it had before, with a line saying so. -->
167
+ <div
168
+ v-else-if="field.codeList && codedFailed[field.key]"
169
+ class="cn-form-dialog__coded-fallback">
202
170
  <NcTextField
203
171
  :id="fieldControlId(field)"
204
172
  :label="fieldLabelText(field)"
205
173
  :labelOutside="isBoardLook"
206
174
  v-bind="fieldAria(field)"
207
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
208
- :error="!!errors[field.key]"
209
- type="number"
210
- :disabled="field.readOnly"
211
- :placeholder="fieldPlaceholder(field)"
212
- @update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
213
- <CnFieldHelper
214
- :id="helpId(field)"
215
- :text="field.description"
216
- :more="field.descriptionLong"
217
- :help="field.help"
218
- :label="field.label"
219
- :error="helperError(field)" />
220
- </template>
221
-
222
- <!-- Textarea -->
223
- <div v-else-if="field.widget === 'textarea'" class="cn-form-dialog__textarea-wrapper">
224
- <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
225
- {{ field.label }}{{ requiredSuffix(field) }}
226
- </label>
227
- <textarea
228
- :id="'cn-form-' + field.key"
229
- class="cn-form-dialog__textarea"
230
- v-bind="fieldAria(field)"
231
- :value="formData[field.key] || ''"
232
- :disabled="field.readOnly"
233
- :placeholder="fieldPlaceholder(field)"
234
- rows="4"
235
- @input="updateField(field.key, $event.target.value)" />
236
- <CnFieldHelper
237
- :id="helpId(field)"
238
- :text="field.description"
239
- :more="field.descriptionLong"
240
- :help="field.help"
241
- :label="field.label"
242
- :error="helperError(field)" />
243
- </div>
244
-
245
- <!-- Object reference with inline create (`x-allow-create`):
246
- a select-OR-create picker over the referenced schema.
247
- Checked before the plain select branch so create-enabled
248
- references get CnResourceSelect; plain references keep the
249
- read-only NcSelect below. -->
250
- <div
251
- v-else-if="(isReferenceField(field) || isReferenceArrayField(field)) && field.allowCreate"
252
- class="cn-form-dialog__select-wrapper">
253
- <CnResourceSelect
254
- :inputId="'cn-form-' + field.key"
255
- :inputLabel="fieldLabelText(field)"
256
- :labelOutside="isBoardLook"
257
- :register="referenceRegister(field)"
258
- :schema="String(field.reference.schema)"
259
- :labelField="referenceLabelField(field)"
260
- :multiple="!!field.reference.multiple"
261
- :modelValue="referenceModelValue(field)"
262
- :clearable="!field.required || !!field.reference.multiple"
263
- :disabled="field.readOnly"
264
- :preload="true"
265
- :createHandler="term => openNestedCreate(field, term)"
266
- @update:modelValue="value => onReferenceSelected(field, value)"
267
- @create="obj => onReferenceCreated(field, obj)" />
268
- <CnFieldHelper
269
- :id="helpId(field)"
270
- :text="field.description"
271
- :more="field.descriptionLong"
272
- :help="field.help"
273
- :label="field.label"
274
- :error="helperError(field)" />
275
- </div>
276
-
277
- <!-- Enum toggle (`widget: "switch"` on a 2-value enum):
278
- renders a switch mapping off→enum[0], on→last enum value. -->
279
- <div v-else-if="field.widget === 'switch'" class="cn-form-dialog__switch-wrapper">
280
- <NcCheckboxRadioSwitch
281
- :modelValue="isSwitchOn(field)"
175
+ :modelValue="codedFallbackText(field)"
282
176
  :disabled="field.readOnly"
283
- type="switch"
284
- @update:modelValue="value => updateField(field.key, switchValueFor(field, value))">
285
- {{ field.label }}{{ requiredSuffix(field) }}
286
- </NcCheckboxRadioSwitch>
287
- <CnFieldHelper
288
- :id="helpId(field)"
289
- :text="field.description"
290
- :more="field.descriptionLong"
291
- :help="field.help"
292
- :label="field.label"
293
- :error="helperError(field)" />
294
- </div>
295
-
296
- <!-- Select (enum / $ref object reference / single Nextcloud user, supports async function).
297
- A single Nextcloud-user field (`widget: "user"`) renders here too:
298
- `:user-select="isUserField(field)"` flips this NcSelect into NC's native
299
- user picker, and the async load/search/select machinery (getEffective*,
300
- onAsyncSearch) resolves real users — mirroring how `user-multiselect`
301
- shares the multiselect branch below. -->
302
- <div v-else-if="field.widget === 'select' || field.widget === 'user' || field.widget === 'group'" class="cn-form-dialog__select-wrapper">
303
- <!-- A `widget: "user"` field renders NcSelectUsers, everything
304
- else NcSelect. It used to be one NcSelect with
305
- `:user-select="isUserField(field)"`, and @nextcloud/vue 9
306
- REMOVED that prop, so a user field had silently become a
307
- plain select. NcSelectUsers wraps NcSelect and merges the
308
- rest of these bindings through, so the async load, search
309
- and select machinery is unchanged either way. -->
310
- <component
311
- :is="isUserField(field) ? 'NcSelectUsers' : 'NcSelect'"
312
- v-cn-select-aria="fieldAria(field)"
313
- :inputId="'cn-form-' + field.key"
314
- :inputLabel="fieldLabelText(field)"
315
- :labelOutside="isBoardLook"
316
- :options="getEffectiveOptions(field)"
317
- :modelValue="getEffectiveSelectedOption(field)"
318
- :clearable="!field.required"
319
- :disabled="field.readOnly"
320
- :loading="isFieldLoading(field)"
321
- :filterable="!isAsyncEnum(field)"
322
- @update:modelValue="onEffectiveSelectChange(field, $event)"
323
- @search="isAsyncEnum(field) ? onAsyncSearch(field, $event) : undefined">
324
- <template
325
- v-if="$slots['field-' + field.key + '-option']"
326
- #option="optionProps">
327
- <!-- @slot field-{key}-option Render one dropdown option for a select, multiselect or tags field. -->
328
- <!-- @binding {object} option The option being rendered (`{ id, label }`). -->
329
- <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
330
- </template>
331
- <!-- NcSelect's own option splits a label in two spans
332
- (NcEllipsisedOption, keeping its last characters
333
- visible), and in a narrow dropdown the halves broke
334
- onto two lines mid-word: "Agricu lture". A plain
335
- label wraps at spaces only. NcSelectUsers keeps its
336
- avatar rendering. -->
337
- <template
338
- v-else-if="!isUserField(field)"
339
- #option="optionProps">
340
- <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
341
- </template>
342
- <template
343
- v-if="$slots['field-' + field.key + '-selected-option']"
344
- #selected-option="optionProps">
345
- <!-- @slot field-{key}-selected-option Render the chosen option of a select, multiselect or tags field. -->
346
- <!-- @binding {object} option The selected option (`{ id, label }`). -->
347
- <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
348
- </template>
349
- </component>
350
- <CnFieldHelper
351
- :id="helpId(field)"
352
- :text="field.description"
353
- :more="field.descriptionLong"
354
- :help="field.help"
355
- :label="field.label"
356
- :error="helperError(field)" />
357
- </div>
358
-
359
- <!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
360
- <div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
361
- <NcSelect
362
- v-cn-select-aria="fieldAria(field)"
363
- :inputId="'cn-form-' + field.key"
364
- :inputLabel="fieldLabelText(field)"
365
- :labelOutside="isBoardLook"
366
- :options="getEffectiveArrayOptions(field)"
367
- :modelValue="getEffectiveSelectedArrayOptions(field)"
368
- :multiple="true"
369
- :keepOpen="true"
370
- :clearable="true"
371
- :disabled="field.readOnly"
372
- :loading="isFieldLoading(field)"
373
- :filterable="!isAsyncItemsEnum(field)"
374
- @update:modelValue="onEffectiveMultiSelectChange(field, $event)"
375
- @search="isAsyncItemsEnum(field) ? onAsyncSearch(field, $event) : undefined">
376
- <template
377
- v-if="$slots['field-' + field.key + '-option']"
378
- #option="optionProps">
379
- <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
380
- </template>
381
- <template v-else #option="optionProps">
382
- <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
383
- </template>
384
- <template
385
- v-if="$slots['field-' + field.key + '-selected-option']"
386
- #selected-option="optionProps">
387
- <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
388
- </template>
389
- <template v-else #selected-option="optionProps">
390
- <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
391
- </template>
392
- </NcSelect>
393
- <CnFieldHelper
394
- :id="helpId(field)"
395
- :text="field.description"
396
- :more="field.descriptionLong"
397
- :help="field.help"
398
- :label="field.label"
399
- :error="helperError(field)" />
400
- </div>
401
-
402
- <!-- Tags (array, freeform, supports async suggestions) -->
403
- <div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
404
- <!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
405
- <NcSelect
406
- v-cn-select-aria="fieldAria(field)"
407
- :inputId="'cn-form-' + field.key"
408
- :inputLabel="fieldLabelText(field)"
409
- :labelOutside="isBoardLook"
410
- :modelValue="formData[field.key] || []"
411
- :options="isFieldAsync(field) ? ((asyncState[field.key] && asyncState[field.key].options) || []) : []"
412
- :multiple="true"
413
- :keepOpen="true"
414
- :taggable="true"
415
- :clearable="true"
416
- :disabled="field.readOnly"
417
- :loading="isFieldLoading(field)"
418
- :filterable="!isFieldAsync(field)"
419
- @update:modelValue="updateField(field.key, $event)"
420
- @search="isFieldAsync(field) ? onAsyncSearch(field, $event) : undefined">
421
- <template
422
- v-if="$slots['field-' + field.key + '-option']"
423
- #option="optionProps">
424
- <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
425
- </template>
426
- <template v-else #option="optionProps">
427
- <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
428
- </template>
429
- <template
430
- v-if="$slots['field-' + field.key + '-selected-option']"
431
- #selected-option="optionProps">
432
- <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
433
- </template>
434
- <template v-else #selected-option="optionProps">
435
- <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
436
- </template>
437
- </NcSelect>
438
- <CnFieldHelper
439
- :id="helpId(field)"
440
- :text="field.description"
441
- :more="field.descriptionLong"
442
- :help="field.help"
443
- :label="field.label"
444
- :error="helperError(field)" />
445
- </div>
446
-
447
- <!-- Checkbox / Switch (boolean) -->
448
- <div v-else-if="field.widget === 'checkbox'" class="cn-form-dialog__switch-wrapper">
449
- <NcCheckboxRadioSwitch
450
- :modelValue="!!formData[field.key]"
451
- :disabled="field.readOnly"
452
- type="switch"
453
- @update:modelValue="value => updateField(field.key, value)">
454
- {{ field.label }}{{ requiredSuffix(field) }}
455
- </NcCheckboxRadioSwitch>
456
- <CnFieldHelper
457
- :id="helpId(field)"
458
- :text="field.description"
459
- :more="field.descriptionLong"
460
- :help="field.help"
461
- :label="field.label"
462
- :error="helperError(field)" />
463
- </div>
464
-
465
- <!-- Date / Datetime (NcTextField's type validator rejects
466
- 'date'/'datetime-local', so use NcDateTimePickerNative) -->
467
- <div
468
- v-else-if="field.widget === 'date' || field.widget === 'datetime'"
469
- class="cn-form-dialog__select-wrapper">
470
- <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
471
- {{ field.label }}{{ requiredSuffix(field) }}
472
- </label>
473
- <NcDateTimePickerNative
474
- :id="'cn-form-' + field.key"
475
- :type="field.widget === 'datetime' ? 'datetime-local' : 'date'"
476
- :label="field.label"
477
- :hideLabel="true"
478
- :modelValue="dateValueFor(field)"
479
- :disabled="field.readOnly"
480
- @update:modelValue="date => onDateFieldInput(field, date)" />
481
- <CnFieldHelper
482
- :id="helpId(field)"
483
- :text="field.description"
484
- :more="field.descriptionLong"
485
- :help="field.help"
486
- :label="field.label"
487
- :error="helperError(field)" />
488
- </div>
489
-
490
- <!-- JSON (type: 'object'|'array'|... with widget: 'json'): parses on input, stores parsed value in formData -->
491
- <div v-else-if="field.widget === 'json'" class="cn-form-dialog__json-wrapper">
492
- <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
493
- {{ field.label }}{{ requiredSuffix(field) }}
494
- </label>
495
- <CnJsonViewer
496
- :value="jsonStringFor(field)"
497
- language="json"
498
- :readOnly="field.readOnly"
499
- :errorText="jsonErrors[field.key] || ''"
500
- @update:value="value => onJsonFieldInput(field, value)" />
501
- <CnFieldHelper
502
- :id="helpId(field)"
503
- :text="field.description"
504
- :more="field.descriptionLong"
505
- :help="field.help"
506
- :label="field.label"
507
- :error="helperError(field)" />
508
- </div>
509
-
510
- <!-- Code (freeform editor, stored as raw string; optional `field.language` chooses highlighting) -->
511
- <div v-else-if="field.widget === 'code'" class="cn-form-dialog__json-wrapper">
512
- <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
513
- {{ field.label }}{{ requiredSuffix(field) }}
514
- </label>
515
- <CnJsonViewer
516
- :value="formData[field.key] != null ? String(formData[field.key]) : ''"
517
- :language="field.language || 'auto'"
518
- :readOnly="field.readOnly"
519
- @update:value="value => updateField(field.key, value)" />
520
- <CnFieldHelper
521
- :id="helpId(field)"
522
- :text="field.description"
523
- :more="field.descriptionLong"
524
- :help="field.help"
525
- :label="field.label"
526
- :error="helperError(field)" />
177
+ :helperText="t('nextcloud-vue', 'The list of choices could not be loaded. Enter the value by hand.')"
178
+ @update:modelValue="onCodedFallbackInput(field, $event)" />
527
179
  </div>
528
180
 
529
- <!-- File (widget: 'file'): one file or several, small ones inline, big ones uploaded after save, optional camera. -->
530
- <div v-else-if="field.widget === 'file'" class="cn-form-dialog__file-wrapper">
531
- <CnFileField
532
- :modelValue="formData[field.key]"
533
- :label="fieldLabelText(field)"
534
- :accept="field.file ? field.file.accept : ''"
535
- :multiple="!!(field.file && field.file.multiple)"
536
- :capture="field.file ? field.file.capture : ''"
537
- v-bind="fileLimits(field)"
538
- :disabled="field.readOnly"
539
- :helperText="errors[field.key] || ''"
540
- @update:modelValue="value => updateField(field.key, value)" />
541
- <CnFieldHelper
542
- :text="field.description"
543
- :more="field.descriptionLong"
544
- :help="field.help"
545
- :label="field.label" />
546
- </div>
547
-
548
- <!-- Duration (widget: 'duration'): number + unit over an ISO 8601 string. -->
549
- <div v-else-if="field.widget === 'duration'" class="cn-form-dialog__duration-wrapper">
550
- <CnDurationField
551
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : null"
552
- :inputLabel="fieldLabelText(field)"
553
- :disabled="field.readOnly"
554
- :error="!!errors[field.key]"
555
- @update:modelValue="value => updateField(field.key, value)" />
556
- <CnFieldHelper
557
- :id="helpId(field)"
558
- :text="field.description"
559
- :more="field.descriptionLong"
560
- :help="field.help"
561
- :label="field.label"
562
- :error="helperError(field)" />
563
- </div>
564
-
565
- <!-- Child records (widget: 'child-records'): another schema's records as an editable table, saved after the parent. -->
566
- <div v-else-if="field.widget === 'child-records' && field.childRecords" class="cn-form-dialog__child-records-wrapper">
567
- <CnChildRecordsField
568
- :modelValue="Array.isArray(formData[field.key]) ? formData[field.key] : []"
569
- :config="field.childRecords"
570
- :register="register"
571
- :parentId="item ? (item.id || item.uuid || '') : ''"
572
- :inputLabel="fieldLabelText(field)"
573
- :disabled="field.readOnly"
574
- :error="errors[field.key] || ''"
575
- @update:modelValue="value => updateField(field.key, value)"
576
- @loaded="rows => { childOriginals[field.key] = rows }"
577
- @validity="problems => { childProblems[field.key] = problems }" />
578
- <CnFieldHelper
579
- :text="field.description"
580
- :more="field.descriptionLong"
581
- :help="field.help"
582
- :label="field.label" />
583
- </div>
584
-
585
- <!-- Sub-objects (widget: 'sub-objects'): an array of objects as an editable table. -->
586
- <div v-else-if="field.widget === 'sub-objects' && field.items" class="cn-form-dialog__sub-objects-wrapper">
587
- <CnSubObjectsField
588
- :modelValue="Array.isArray(formData[field.key]) ? formData[field.key] : []"
589
- :items="field.items"
590
- :inputLabel="fieldLabelText(field)"
591
- :disabled="field.readOnly"
592
- :error="errors[field.key] || ''"
593
- @update:modelValue="value => updateField(field.key, value)" />
594
- <CnFieldHelper
595
- :text="field.description"
596
- :more="field.descriptionLong"
597
- :help="field.help"
598
- :label="field.label" />
599
- </div>
600
-
601
- <!-- Registry type-ahead (widget: 'property-source'): CnPropertySourceField;
602
- a pick can fill empty sibling fields from config.fill. -->
603
- <div v-else-if="field.widget === 'property-source' && field.propertySource" class="cn-form-dialog__property-source-wrapper">
604
- <CnPropertySourceField
605
- :provider="field.propertySource.provider"
606
- :mode="field.propertySource.mode"
607
- :inputId="'cn-form-' + field.key"
608
- :inputLabel="fieldLabelText(field)"
609
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
610
- :disabled="field.readOnly"
611
- :error="!!errors[field.key]"
612
- @update:modelValue="value => updateField(field.key, value)"
613
- @resolved="payload => onPropertySourceResolved(field, payload)" />
614
- <CnFieldHelper
615
- :id="helpId(field)"
616
- :text="field.description"
617
- :more="field.descriptionLong"
618
- :help="field.help"
619
- :label="field.label"
620
- :error="helperError(field)" />
621
- </div>
622
-
623
- <!-- Icon (widget: 'icon'): renders CnIconBrowser, forwarding the field's icon config.
624
- `searchable` is gone — the browser always searches. -->
625
- <div v-else-if="field.widget === 'icon'" class="cn-form-dialog__icon-wrapper">
626
- <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
627
- {{ field.label }}{{ requiredSuffix(field) }}
628
- </label>
629
- <CnIconBrowser
630
- :value="formData[field.key] != null ? String(formData[field.key]) : null"
631
- :sources="field.iconSources || ['mdi']"
632
- :catalogues="field.catalogues || {}"
633
- :allowCustomSvg="!!field.allowCustomSvg"
634
- :clearable="!field.required"
635
- @input="value => updateField(field.key, value)" />
636
- <CnFieldHelper
637
- :id="helpId(field)"
638
- :text="field.description"
639
- :more="field.descriptionLong"
640
- :help="field.help"
641
- :label="field.label"
642
- :error="helperError(field)" />
643
- </div>
644
-
645
- <!-- Fallback: text input -->
181
+ <!-- Auto-generated field -->
646
182
  <template v-else>
647
- <NcTextField
648
- :id="fieldControlId(field)"
649
- :label="fieldLabelText(field)"
650
- :labelOutside="isBoardLook"
651
- v-bind="fieldAria(field)"
652
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
653
- :error="!!errors[field.key]"
654
- :disabled="field.readOnly"
655
- :placeholder="fieldPlaceholder(field)"
656
- @update:modelValue="value => updateField(field.key, value)" />
657
- <CnFieldHelper
658
- :id="helpId(field)"
659
- :text="field.description"
660
- :more="field.descriptionLong"
661
- :help="field.help"
662
- :label="field.label"
663
- :error="helperError(field)" />
183
+ <!-- Text / Email / URL. The helper line lives outside the
184
+ input (rather than on `helper-text`) so an over-long
185
+ description can hang its info popover off it. -->
186
+ <template v-if="field.widget === 'text' || field.widget === 'email' || field.widget === 'url'">
187
+ <NcTextField
188
+ :id="fieldControlId(field)"
189
+ :label="fieldLabelText(field)"
190
+ :labelOutside="isBoardLook"
191
+ v-bind="fieldAria(field)"
192
+ :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
193
+ :error="!!errors[field.key]"
194
+ :type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
195
+ :disabled="field.readOnly"
196
+ :placeholder="fieldPlaceholder(field)"
197
+ @update:modelValue="value => updateField(field.key, value)" />
198
+ <CnFieldHelper
199
+ :id="helpId(field)"
200
+ :text="field.description"
201
+ :more="field.descriptionLong"
202
+ :help="field.help"
203
+ :label="field.label"
204
+ :error="helperError(field)" />
205
+ </template>
206
+
207
+ <!-- Number -->
208
+ <template v-else-if="field.widget === 'number'">
209
+ <NcTextField
210
+ :id="fieldControlId(field)"
211
+ :label="fieldLabelText(field)"
212
+ :labelOutside="isBoardLook"
213
+ v-bind="fieldAria(field)"
214
+ :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
215
+ :error="!!errors[field.key]"
216
+ type="number"
217
+ :disabled="field.readOnly"
218
+ :placeholder="fieldPlaceholder(field)"
219
+ @update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
220
+ <CnFieldHelper
221
+ :id="helpId(field)"
222
+ :text="field.description"
223
+ :more="field.descriptionLong"
224
+ :help="field.help"
225
+ :label="field.label"
226
+ :error="helperError(field)" />
227
+ </template>
228
+
229
+ <!-- Textarea -->
230
+ <div v-else-if="field.widget === 'textarea'" class="cn-form-dialog__textarea-wrapper">
231
+ <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
232
+ {{ field.label }}{{ requiredSuffix(field) }}
233
+ </label>
234
+ <textarea
235
+ :id="'cn-form-' + field.key"
236
+ class="cn-form-dialog__textarea"
237
+ v-bind="fieldAria(field)"
238
+ :value="formData[field.key] || ''"
239
+ :disabled="field.readOnly"
240
+ :placeholder="fieldPlaceholder(field)"
241
+ rows="4"
242
+ @input="updateField(field.key, $event.target.value)" />
243
+ <CnFieldHelper
244
+ :id="helpId(field)"
245
+ :text="field.description"
246
+ :more="field.descriptionLong"
247
+ :help="field.help"
248
+ :label="field.label"
249
+ :error="helperError(field)" />
250
+ </div>
251
+
252
+ <!-- Object reference with inline create (`x-allow-create`):
253
+ a select-OR-create picker over the referenced schema.
254
+ Checked before the plain select branch so create-enabled
255
+ references get CnResourceSelect; plain references keep the
256
+ read-only NcSelect below. -->
257
+ <div
258
+ v-else-if="(isReferenceField(field) || isReferenceArrayField(field)) && field.allowCreate"
259
+ class="cn-form-dialog__select-wrapper">
260
+ <CnResourceSelect
261
+ :inputId="'cn-form-' + field.key"
262
+ :inputLabel="fieldLabelText(field)"
263
+ :labelOutside="isBoardLook"
264
+ :register="referenceRegister(field)"
265
+ :schema="String(field.reference.schema)"
266
+ :labelField="referenceLabelField(field)"
267
+ :multiple="!!field.reference.multiple"
268
+ :modelValue="referenceModelValue(field)"
269
+ :clearable="!field.required || !!field.reference.multiple"
270
+ :disabled="field.readOnly"
271
+ :preload="true"
272
+ :createHandler="term => openNestedCreate(field, term)"
273
+ @update:modelValue="value => onReferenceSelected(field, value)"
274
+ @create="obj => onReferenceCreated(field, obj)" />
275
+ <CnFieldHelper
276
+ :id="helpId(field)"
277
+ :text="field.description"
278
+ :more="field.descriptionLong"
279
+ :help="field.help"
280
+ :label="field.label"
281
+ :error="helperError(field)" />
282
+ </div>
283
+
284
+ <!-- Enum toggle (`widget: "switch"` on a 2-value enum):
285
+ renders a switch mapping off→enum[0], on→last enum value. -->
286
+ <div v-else-if="field.widget === 'switch'" class="cn-form-dialog__switch-wrapper">
287
+ <NcCheckboxRadioSwitch
288
+ :modelValue="isSwitchOn(field)"
289
+ :disabled="field.readOnly"
290
+ type="switch"
291
+ @update:modelValue="value => updateField(field.key, switchValueFor(field, value))">
292
+ {{ field.label }}{{ requiredSuffix(field) }}
293
+ </NcCheckboxRadioSwitch>
294
+ <CnFieldHelper
295
+ :id="helpId(field)"
296
+ :text="field.description"
297
+ :more="field.descriptionLong"
298
+ :help="field.help"
299
+ :label="field.label"
300
+ :error="helperError(field)" />
301
+ </div>
302
+
303
+ <!-- Select (enum / $ref object reference / single Nextcloud user, supports async function).
304
+ A single Nextcloud-user field (`widget: "user"`) renders here too:
305
+ `:user-select="isUserField(field)"` flips this NcSelect into NC's native
306
+ user picker, and the async load/search/select machinery (getEffective*,
307
+ onAsyncSearch) resolves real users — mirroring how `user-multiselect`
308
+ shares the multiselect branch below. -->
309
+ <div v-else-if="field.widget === 'select' || field.widget === 'user' || field.widget === 'group'" class="cn-form-dialog__select-wrapper">
310
+ <!-- A `widget: "user"` field renders NcSelectUsers, everything
311
+ else NcSelect. It used to be one NcSelect with
312
+ `:user-select="isUserField(field)"`, and @nextcloud/vue 9
313
+ REMOVED that prop, so a user field had silently become a
314
+ plain select. NcSelectUsers wraps NcSelect and merges the
315
+ rest of these bindings through, so the async load, search
316
+ and select machinery is unchanged either way. -->
317
+ <component
318
+ :is="isUserField(field) ? 'NcSelectUsers' : 'NcSelect'"
319
+ v-cn-select-aria="fieldAria(field)"
320
+ :inputId="'cn-form-' + field.key"
321
+ :inputLabel="fieldLabelText(field)"
322
+ :labelOutside="isBoardLook"
323
+ :options="getEffectiveOptions(field)"
324
+ :modelValue="getEffectiveSelectedOption(field)"
325
+ :clearable="!field.required"
326
+ :disabled="field.readOnly"
327
+ :loading="isFieldLoading(field)"
328
+ :filterable="!isAsyncEnum(field)"
329
+ @update:modelValue="onEffectiveSelectChange(field, $event)"
330
+ @search="isAsyncEnum(field) ? onAsyncSearch(field, $event) : undefined">
331
+ <template
332
+ v-if="$slots['field-' + field.key + '-option']"
333
+ #option="optionProps">
334
+ <!-- @slot field-{key}-option Render one dropdown option for a select, multiselect or tags field. -->
335
+ <!-- @binding {object} option The option being rendered (`{ id, label }`). -->
336
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
337
+ </template>
338
+ <!-- NcSelect's own option splits a label in two spans
339
+ (NcEllipsisedOption, keeping its last characters
340
+ visible), and in a narrow dropdown the halves broke
341
+ onto two lines mid-word: "Agricu lture". A plain
342
+ label wraps at spaces only. NcSelectUsers keeps its
343
+ avatar rendering. -->
344
+ <template
345
+ v-else-if="!isUserField(field)"
346
+ #option="optionProps">
347
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
348
+ </template>
349
+ <template
350
+ v-if="$slots['field-' + field.key + '-selected-option']"
351
+ #selected-option="optionProps">
352
+ <!-- @slot field-{key}-selected-option Render the chosen option of a select, multiselect or tags field. -->
353
+ <!-- @binding {object} option The selected option (`{ id, label }`). -->
354
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
355
+ </template>
356
+ </component>
357
+ <CnFieldHelper
358
+ :id="helpId(field)"
359
+ :text="field.description"
360
+ :more="field.descriptionLong"
361
+ :help="field.help"
362
+ :label="field.label"
363
+ :error="helperError(field)" />
364
+ </div>
365
+
366
+ <!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
367
+ <div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
368
+ <NcSelect
369
+ v-cn-select-aria="fieldAria(field)"
370
+ :inputId="'cn-form-' + field.key"
371
+ :inputLabel="fieldLabelText(field)"
372
+ :labelOutside="isBoardLook"
373
+ :options="getEffectiveArrayOptions(field)"
374
+ :modelValue="getEffectiveSelectedArrayOptions(field)"
375
+ :multiple="true"
376
+ :keepOpen="true"
377
+ :clearable="true"
378
+ :disabled="field.readOnly"
379
+ :loading="isFieldLoading(field)"
380
+ :filterable="!isAsyncItemsEnum(field)"
381
+ @update:modelValue="onEffectiveMultiSelectChange(field, $event)"
382
+ @search="isAsyncItemsEnum(field) ? onAsyncSearch(field, $event) : undefined">
383
+ <template
384
+ v-if="$slots['field-' + field.key + '-option']"
385
+ #option="optionProps">
386
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
387
+ </template>
388
+ <template v-else #option="optionProps">
389
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
390
+ </template>
391
+ <template
392
+ v-if="$slots['field-' + field.key + '-selected-option']"
393
+ #selected-option="optionProps">
394
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
395
+ </template>
396
+ <template v-else #selected-option="optionProps">
397
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
398
+ </template>
399
+ </NcSelect>
400
+ <CnFieldHelper
401
+ :id="helpId(field)"
402
+ :text="field.description"
403
+ :more="field.descriptionLong"
404
+ :help="field.help"
405
+ :label="field.label"
406
+ :error="helperError(field)" />
407
+ </div>
408
+
409
+ <!-- Tags (array, freeform, supports async suggestions) -->
410
+ <div v-else-if="field.widget === 'tags'" class="cn-form-dialog__select-wrapper">
411
+ <!-- TODO: restore `:options` to `asyncState[field.key]?.options` once on Vue 3 (buble doesn't support optional chaining) -->
412
+ <NcSelect
413
+ v-cn-select-aria="fieldAria(field)"
414
+ :inputId="'cn-form-' + field.key"
415
+ :inputLabel="fieldLabelText(field)"
416
+ :labelOutside="isBoardLook"
417
+ :modelValue="formData[field.key] || []"
418
+ :options="isFieldAsync(field) ? ((asyncState[field.key] && asyncState[field.key].options) || []) : []"
419
+ :multiple="true"
420
+ :keepOpen="true"
421
+ :taggable="true"
422
+ :clearable="true"
423
+ :disabled="field.readOnly"
424
+ :loading="isFieldLoading(field)"
425
+ :filterable="!isFieldAsync(field)"
426
+ @update:modelValue="updateField(field.key, $event)"
427
+ @search="isFieldAsync(field) ? onAsyncSearch(field, $event) : undefined">
428
+ <template
429
+ v-if="$slots['field-' + field.key + '-option']"
430
+ #option="optionProps">
431
+ <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
432
+ </template>
433
+ <template v-else #option="optionProps">
434
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
435
+ </template>
436
+ <template
437
+ v-if="$slots['field-' + field.key + '-selected-option']"
438
+ #selected-option="optionProps">
439
+ <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
440
+ </template>
441
+ <template v-else #selected-option="optionProps">
442
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
443
+ </template>
444
+ </NcSelect>
445
+ <CnFieldHelper
446
+ :id="helpId(field)"
447
+ :text="field.description"
448
+ :more="field.descriptionLong"
449
+ :help="field.help"
450
+ :label="field.label"
451
+ :error="helperError(field)" />
452
+ </div>
453
+
454
+ <!-- Checkbox / Switch (boolean) -->
455
+ <div v-else-if="field.widget === 'checkbox'" class="cn-form-dialog__switch-wrapper">
456
+ <NcCheckboxRadioSwitch
457
+ :modelValue="!!formData[field.key]"
458
+ :disabled="field.readOnly"
459
+ type="switch"
460
+ @update:modelValue="value => updateField(field.key, value)">
461
+ {{ field.label }}{{ requiredSuffix(field) }}
462
+ </NcCheckboxRadioSwitch>
463
+ <CnFieldHelper
464
+ :id="helpId(field)"
465
+ :text="field.description"
466
+ :more="field.descriptionLong"
467
+ :help="field.help"
468
+ :label="field.label"
469
+ :error="helperError(field)" />
470
+ </div>
471
+
472
+ <!-- Date / Datetime (NcTextField's type validator rejects
473
+ 'date'/'datetime-local', so use NcDateTimePickerNative) -->
474
+ <div
475
+ v-else-if="field.widget === 'date' || field.widget === 'datetime'"
476
+ class="cn-form-dialog__select-wrapper">
477
+ <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
478
+ {{ field.label }}{{ requiredSuffix(field) }}
479
+ </label>
480
+ <NcDateTimePickerNative
481
+ :id="'cn-form-' + field.key"
482
+ :type="field.widget === 'datetime' ? 'datetime-local' : 'date'"
483
+ :label="field.label"
484
+ :hideLabel="true"
485
+ :modelValue="dateValueFor(field)"
486
+ :disabled="field.readOnly"
487
+ @update:modelValue="date => onDateFieldInput(field, date)" />
488
+ <CnFieldHelper
489
+ :id="helpId(field)"
490
+ :text="field.description"
491
+ :more="field.descriptionLong"
492
+ :help="field.help"
493
+ :label="field.label"
494
+ :error="helperError(field)" />
495
+ </div>
496
+
497
+ <!-- JSON (type: 'object'|'array'|... with widget: 'json'): parses on input, stores parsed value in formData -->
498
+ <div v-else-if="field.widget === 'json'" class="cn-form-dialog__json-wrapper">
499
+ <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
500
+ {{ field.label }}{{ requiredSuffix(field) }}
501
+ </label>
502
+ <CnJsonViewer
503
+ :value="jsonStringFor(field)"
504
+ language="json"
505
+ :readOnly="field.readOnly"
506
+ :errorText="jsonErrors[field.key] || ''"
507
+ @update:value="value => onJsonFieldInput(field, value)" />
508
+ <CnFieldHelper
509
+ :id="helpId(field)"
510
+ :text="field.description"
511
+ :more="field.descriptionLong"
512
+ :help="field.help"
513
+ :label="field.label"
514
+ :error="helperError(field)" />
515
+ </div>
516
+
517
+ <!-- Code (freeform editor, stored as raw string; optional `field.language` chooses highlighting) -->
518
+ <div v-else-if="field.widget === 'code'" class="cn-form-dialog__json-wrapper">
519
+ <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
520
+ {{ field.label }}{{ requiredSuffix(field) }}
521
+ </label>
522
+ <CnJsonViewer
523
+ :value="formData[field.key] != null ? String(formData[field.key]) : ''"
524
+ :language="field.language || 'auto'"
525
+ :readOnly="field.readOnly"
526
+ @update:value="value => updateField(field.key, value)" />
527
+ <CnFieldHelper
528
+ :id="helpId(field)"
529
+ :text="field.description"
530
+ :more="field.descriptionLong"
531
+ :help="field.help"
532
+ :label="field.label"
533
+ :error="helperError(field)" />
534
+ </div>
535
+
536
+ <!-- File (widget: 'file'): one file or several, small ones inline, big ones uploaded after save, optional camera. -->
537
+ <div v-else-if="field.widget === 'file'" class="cn-form-dialog__file-wrapper">
538
+ <CnFileField
539
+ :modelValue="formData[field.key]"
540
+ :label="fieldLabelText(field)"
541
+ :accept="field.file ? field.file.accept : ''"
542
+ :multiple="!!(field.file && field.file.multiple)"
543
+ :capture="field.file ? field.file.capture : ''"
544
+ v-bind="fileLimits(field)"
545
+ :disabled="field.readOnly"
546
+ :helperText="errors[field.key] || ''"
547
+ @update:modelValue="value => updateField(field.key, value)" />
548
+ <CnFieldHelper
549
+ :text="field.description"
550
+ :more="field.descriptionLong"
551
+ :help="field.help"
552
+ :label="field.label" />
553
+ </div>
554
+
555
+ <!-- Duration (widget: 'duration'): number + unit over an ISO 8601 string. -->
556
+ <div v-else-if="field.widget === 'duration'" class="cn-form-dialog__duration-wrapper">
557
+ <CnDurationField
558
+ :modelValue="formData[field.key] != null ? String(formData[field.key]) : null"
559
+ :inputLabel="fieldLabelText(field)"
560
+ :disabled="field.readOnly"
561
+ :error="!!errors[field.key]"
562
+ @update:modelValue="value => updateField(field.key, value)" />
563
+ <CnFieldHelper
564
+ :id="helpId(field)"
565
+ :text="field.description"
566
+ :more="field.descriptionLong"
567
+ :help="field.help"
568
+ :label="field.label"
569
+ :error="helperError(field)" />
570
+ </div>
571
+
572
+ <!-- Child records (widget: 'child-records'): another schema's records as an editable table, saved after the parent. -->
573
+ <div v-else-if="field.widget === 'child-records' && field.childRecords" class="cn-form-dialog__child-records-wrapper">
574
+ <CnChildRecordsField
575
+ :modelValue="Array.isArray(formData[field.key]) ? formData[field.key] : []"
576
+ :config="field.childRecords"
577
+ :register="register"
578
+ :parentId="item ? (item.id || item.uuid || '') : ''"
579
+ :inputLabel="fieldLabelText(field)"
580
+ :disabled="field.readOnly"
581
+ :error="errors[field.key] || ''"
582
+ @update:modelValue="value => updateField(field.key, value)"
583
+ @loaded="rows => { childOriginals[field.key] = rows }"
584
+ @validity="problems => { childProblems[field.key] = problems }" />
585
+ <CnFieldHelper
586
+ :text="field.description"
587
+ :more="field.descriptionLong"
588
+ :help="field.help"
589
+ :label="field.label" />
590
+ </div>
591
+
592
+ <!-- Sub-objects (widget: 'sub-objects'): an array of objects as an editable table. -->
593
+ <div v-else-if="field.widget === 'sub-objects' && field.items" class="cn-form-dialog__sub-objects-wrapper">
594
+ <CnSubObjectsField
595
+ :modelValue="Array.isArray(formData[field.key]) ? formData[field.key] : []"
596
+ :items="field.items"
597
+ :inputLabel="fieldLabelText(field)"
598
+ :disabled="field.readOnly"
599
+ :error="errors[field.key] || ''"
600
+ @update:modelValue="value => updateField(field.key, value)" />
601
+ <CnFieldHelper
602
+ :text="field.description"
603
+ :more="field.descriptionLong"
604
+ :help="field.help"
605
+ :label="field.label" />
606
+ </div>
607
+
608
+ <!-- Registry type-ahead (widget: 'property-source'): CnPropertySourceField;
609
+ a pick can fill empty sibling fields from config.fill. -->
610
+ <div v-else-if="field.widget === 'property-source' && field.propertySource" class="cn-form-dialog__property-source-wrapper">
611
+ <CnPropertySourceField
612
+ :provider="field.propertySource.provider"
613
+ :mode="field.propertySource.mode"
614
+ :inputId="'cn-form-' + field.key"
615
+ :inputLabel="fieldLabelText(field)"
616
+ :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
617
+ :disabled="field.readOnly"
618
+ :error="!!errors[field.key]"
619
+ @update:modelValue="value => updateField(field.key, value)"
620
+ @resolved="payload => onPropertySourceResolved(field, payload)" />
621
+ <CnFieldHelper
622
+ :id="helpId(field)"
623
+ :text="field.description"
624
+ :more="field.descriptionLong"
625
+ :help="field.help"
626
+ :label="field.label"
627
+ :error="helperError(field)" />
628
+ </div>
629
+
630
+ <!-- Icon (widget: 'icon'): renders CnIconBrowser, forwarding the field's icon config.
631
+ `searchable` is gone — the browser always searches. -->
632
+ <div v-else-if="field.widget === 'icon'" class="cn-form-dialog__icon-wrapper">
633
+ <label v-if="!showsOuterLabel(field)" :for="'cn-form-' + field.key" class="cn-form-dialog__label">
634
+ {{ field.label }}{{ requiredSuffix(field) }}
635
+ </label>
636
+ <CnIconBrowser
637
+ :value="formData[field.key] != null ? String(formData[field.key]) : null"
638
+ :sources="field.iconSources || ['mdi']"
639
+ :catalogues="field.catalogues || {}"
640
+ :allowCustomSvg="!!field.allowCustomSvg"
641
+ :clearable="!field.required"
642
+ @input="value => updateField(field.key, value)" />
643
+ <CnFieldHelper
644
+ :id="helpId(field)"
645
+ :text="field.description"
646
+ :more="field.descriptionLong"
647
+ :help="field.help"
648
+ :label="field.label"
649
+ :error="helperError(field)" />
650
+ </div>
651
+
652
+ <!-- Fallback: text input -->
653
+ <template v-else>
654
+ <NcTextField
655
+ :id="fieldControlId(field)"
656
+ :label="fieldLabelText(field)"
657
+ :labelOutside="isBoardLook"
658
+ v-bind="fieldAria(field)"
659
+ :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
660
+ :error="!!errors[field.key]"
661
+ :disabled="field.readOnly"
662
+ :placeholder="fieldPlaceholder(field)"
663
+ @update:modelValue="value => updateField(field.key, value)" />
664
+ <CnFieldHelper
665
+ :id="helpId(field)"
666
+ :text="field.description"
667
+ :more="field.descriptionLong"
668
+ :help="field.help"
669
+ :label="field.label"
670
+ :error="helperError(field)" />
671
+ </template>
672
+ <small
673
+ v-if="assignedFrom[field.key]"
674
+ class="cn-form-dialog__assigned"
675
+ aria-live="polite"
676
+ data-testid="cn-form-dialog-assigned">
677
+ {{ t('nextcloud-vue', 'Filled in from {field}', { field: assignedFrom[field.key] }) }}
678
+ </small>
664
679
  </template>
665
- <small
666
- v-if="assignedFrom[field.key]"
667
- class="cn-form-dialog__assigned"
668
- aria-live="polite"
669
- data-testid="cn-form-dialog-assigned">
670
- {{ t('nextcloud-vue', 'Filled in from {field}', { field: assignedFrom[field.key] }) }}
671
- </small>
672
- </template>
673
- </div>
680
+ </div>
681
+ </template>
674
682
 
675
683
  <!-- The extra questions a chosen case type / product line asks
676
684
  are fetched, so say they are coming. Silence here reads as
@@ -758,6 +766,11 @@
758
766
  @click="executeConfirm">
759
767
  <template #icon>
760
768
  <NcLoadingIcon v-if="loading" :size="20" />
769
+ <CnIcon
770
+ v-else-if="confirmIcon"
771
+ :name="confirmIcon"
772
+ :size="20"
773
+ data-testid="cn-form-dialog-confirm-icon" />
761
774
  <Plus v-else-if="isCreateMode" :size="20" />
762
775
  <ContentSaveOutline v-else :size="20" />
763
776
  </template>
@@ -781,6 +794,7 @@ import CnDurationField from '../CnDurationField/CnDurationField.vue'
781
794
  import CnFieldHelper from '../CnFieldHelper/CnFieldHelper.vue'
782
795
  import CnFileField from '../CnFileField/CnFileField.vue'
783
796
  import CnFormField from '../CnFormField/CnFormField.vue'
797
+ import CnIcon from '../CnIcon/CnIcon.vue'
784
798
  import CnIconBrowser from '../CnIconBrowser/CnIconBrowser.vue'
785
799
  import CnJsonViewer from '../CnJsonViewer/CnJsonViewer.vue'
786
800
  import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceField.vue'
@@ -1023,6 +1037,7 @@ export default {
1023
1037
  CnSubObjectsField,
1024
1038
  CnChildRecordsField,
1025
1039
  CnDialog,
1040
+ CnIcon,
1026
1041
  CnFormField,
1027
1042
  NcButton,
1028
1043
  NcNoteCard,
@@ -1303,6 +1318,12 @@ export default {
1303
1318
  default: '',
1304
1319
  },
1305
1320
 
1321
+ /** MDI icon name on the Confirm button (`SwapHorizontal`, `Archive`, `Send`, `Gavel`). Defaults to Plus when creating, else a save icon. */
1322
+ confirmIcon: {
1323
+ type: String,
1324
+ default: '',
1325
+ },
1326
+
1306
1327
  /** Keep the Confirm button disabled regardless of the form state, for a host that knows the form cannot be completed. */
1307
1328
  confirmDisabled: {
1308
1329
  type: Boolean,
@@ -1735,6 +1756,27 @@ export default {
1735
1756
  .map((field) => this.applyCodePicker(field))
1736
1757
  },
1737
1758
 
1759
+ /**
1760
+ * The section headings, keyed by the field that opens each: a field
1761
+ * whose `section` (set through `fieldOverrides`) differs from the
1762
+ * visible field before it starts a group. Sections follow the field
1763
+ * order; the heading goes through the app's translate function.
1764
+ *
1765
+ * @return {Object<string, string>} Field key to heading.
1766
+ */
1767
+ sectionStarts() {
1768
+ const starts = {}
1769
+ let current = ''
1770
+ for (const field of this.visibleFields) {
1771
+ const section = typeof field.section === 'string' ? field.section : ''
1772
+ if (section !== '' && section !== current) {
1773
+ starts[field.key] = typeof this.cnTranslate === 'function' ? this.cnTranslate(section) : section
1774
+ }
1775
+ current = section
1776
+ }
1777
+ return starts
1778
+ },
1779
+
1738
1780
  /**
1739
1781
  * The schema properties that hold the record's tenant, which the form
1740
1782
  * hides (see `isTenantProperty`) and fills itself: from the record
@@ -4932,6 +4974,21 @@ export default {
4932
4974
  from pushing its column past its share: a grid item's default `auto`
4933
4975
  minimum is its content, so without it one long option label widens the
4934
4976
  whole column and the two stop being equal. */
4977
+ .cn-form-dialog__section {
4978
+ margin: 12px 0 0;
4979
+ padding-top: 12px;
4980
+ border-top: 1px solid var(--color-border);
4981
+ font-size: 15px;
4982
+ font-weight: 700;
4983
+ }
4984
+
4985
+ /* The first heading opens the form: no rule above it. */
4986
+ .cn-form-dialog__form .cn-form-dialog__section:first-of-type {
4987
+ margin-top: 0;
4988
+ padding-top: 0;
4989
+ border-top: 0;
4990
+ }
4991
+
4935
4992
  .cn-form-dialog__form--two-column {
4936
4993
  display: grid;
4937
4994
  grid-template-columns: 1fr 1fr;