@conduction/nextcloud-vue 2.61.0 → 2.63.0

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 (186) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -179,16 +179,20 @@
179
179
  references get CnResourceSelect; plain references keep the
180
180
  read-only NcSelect below. -->
181
181
  <div
182
- v-else-if="isReferenceField(field) && field.allowCreate"
182
+ v-else-if="(isReferenceField(field) || isReferenceArrayField(field)) && field.allowCreate"
183
183
  class="cn-form-dialog__select-wrapper">
184
184
  <CnResourceSelect
185
185
  :inputId="'cn-form-' + field.key"
186
186
  :inputLabel="field.label + (field.required ? ' *' : '')"
187
187
  :register="referenceRegister(field)"
188
- :schema="field.reference.schema"
188
+ :schema="String(field.reference.schema)"
189
189
  :labelField="referenceLabelField(field)"
190
- :modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
191
- :clearable="!field.required"
190
+ :multiple="!!field.reference.multiple"
191
+ :modelValue="referenceModelValue(field)"
192
+ :clearable="!field.required || !!field.reference.multiple"
193
+ :disabled="field.readOnly"
194
+ :preload="true"
195
+ :createHandler="term => openNestedCreate(field, term)"
192
196
  @update:modelValue="value => onReferenceSelected(field, value)"
193
197
  @create="obj => onReferenceCreated(field, obj)" />
194
198
  <CnFieldHelper
@@ -199,14 +203,19 @@
199
203
 
200
204
  <!-- Enum toggle (`widget: "switch"` on a 2-value enum):
201
205
  renders a switch mapping off→enum[0], on→last enum value. -->
202
- <NcCheckboxRadioSwitch
203
- v-else-if="field.widget === 'switch'"
204
- :modelValue="isSwitchOn(field)"
205
- :disabled="field.readOnly"
206
- type="switch"
207
- @update:modelValue="value => updateField(field.key, switchValueFor(field, value))">
208
- {{ field.label }}{{ field.required ? ' *' : '' }}
209
- </NcCheckboxRadioSwitch>
206
+ <div v-else-if="field.widget === 'switch'" class="cn-form-dialog__switch-wrapper">
207
+ <NcCheckboxRadioSwitch
208
+ :modelValue="isSwitchOn(field)"
209
+ :disabled="field.readOnly"
210
+ type="switch"
211
+ @update:modelValue="value => updateField(field.key, switchValueFor(field, value))">
212
+ {{ field.label }}{{ field.required ? ' *' : '' }}
213
+ </NcCheckboxRadioSwitch>
214
+ <CnFieldHelper
215
+ :text="field.description"
216
+ :more="field.descriptionLong"
217
+ :error="errors[field.key]" />
218
+ </div>
210
219
 
211
220
  <!-- Select (enum / $ref object reference / single Nextcloud user, supports async function).
212
221
  A single Nextcloud-user field (`widget: "user"`) renders here too:
@@ -214,7 +223,7 @@
214
223
  user picker, and the async load/search/select machinery (getEffective*,
215
224
  onAsyncSearch) resolves real users — mirroring how `user-multiselect`
216
225
  shares the multiselect branch below. -->
217
- <div v-else-if="field.widget === 'select' || field.widget === 'user'" class="cn-form-dialog__select-wrapper">
226
+ <div v-else-if="field.widget === 'select' || field.widget === 'user' || field.widget === 'group'" class="cn-form-dialog__select-wrapper">
218
227
  <!-- A `widget: "user"` field renders NcSelectUsers, everything
219
228
  else NcSelect. It used to be one NcSelect with
220
229
  `:user-select="isUserField(field)"`, and @nextcloud/vue 9
@@ -256,7 +265,7 @@
256
265
  </div>
257
266
 
258
267
  <!-- Multiselect (array enum items / $ref array / Nextcloud users, supports async function) -->
259
- <div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect'" class="cn-form-dialog__select-wrapper">
268
+ <div v-else-if="field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect'" class="cn-form-dialog__select-wrapper">
260
269
  <NcSelect
261
270
  :inputId="'cn-form-' + field.key"
262
271
  :inputLabel="field.label + (field.required ? ' *' : '')"
@@ -322,14 +331,19 @@
322
331
  </div>
323
332
 
324
333
  <!-- Checkbox / Switch (boolean) -->
325
- <NcCheckboxRadioSwitch
326
- v-else-if="field.widget === 'checkbox'"
327
- :modelValue="!!formData[field.key]"
328
- :disabled="field.readOnly"
329
- type="switch"
330
- @update:modelValue="value => updateField(field.key, value)">
331
- {{ field.label }}{{ field.required ? ' *' : '' }}
332
- </NcCheckboxRadioSwitch>
334
+ <div v-else-if="field.widget === 'checkbox'" class="cn-form-dialog__switch-wrapper">
335
+ <NcCheckboxRadioSwitch
336
+ :modelValue="!!formData[field.key]"
337
+ :disabled="field.readOnly"
338
+ type="switch"
339
+ @update:modelValue="value => updateField(field.key, value)">
340
+ {{ field.label }}{{ field.required ? ' *' : '' }}
341
+ </NcCheckboxRadioSwitch>
342
+ <CnFieldHelper
343
+ :text="field.description"
344
+ :more="field.descriptionLong"
345
+ :error="errors[field.key]" />
346
+ </div>
333
347
 
334
348
  <!-- Date / Datetime (NcTextField's type validator rejects
335
349
  'date'/'datetime-local', so use NcDateTimePickerNative) -->
@@ -438,6 +452,21 @@
438
452
  </template>
439
453
  </div>
440
454
 
455
+ <!-- "Create" on a select-or-create reference (`x-allow-create`) opens
456
+ the referenced schema's own form on top of this one. Saving it
457
+ selects the new object in the field that asked for it. -->
458
+ <CnFormDialog
459
+ v-if="nestedCreate !== null"
460
+ ref="nestedCreateDialog"
461
+ :schema="nestedCreate.schema"
462
+ :register="nestedCreate.register"
463
+ :item="null"
464
+ :initialData="nestedCreate.initialData"
465
+ :recoverDraft="false"
466
+ data-testid="cn-form-dialog-nested-create"
467
+ @confirm="onNestedCreateConfirm"
468
+ @close="onNestedCreateClose" />
469
+
441
470
  <template #actions>
442
471
  <!-- One announcement per state change, so a screen reader hears
443
472
  "Draft saved" once rather than on every keystroke. -->
@@ -494,7 +523,9 @@ import {
494
523
  splitDynamicFormData,
495
524
  } from '../../utils/dynamicProperties.js'
496
525
  import { shouldShow } from '../../utils/fieldCondition.js'
526
+ import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
497
527
  import { objectDisplayName } from '../../utils/objectName.js'
528
+ import { languageOptions, resolveDefaultToken, timezoneOptions } from '../../utils/pickerOptions.js'
498
529
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
499
530
  import { fieldsFromSchema, isTenantProperty } from '../../utils/schema.js'
500
531
  import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
@@ -1025,6 +1056,13 @@ export default {
1025
1056
  * `dynamicToken` guards the definitions fetch.
1026
1057
  */
1027
1058
  prefillToken: '',
1059
+ /**
1060
+ * The inline create in progress, or null: `{ field, schema,
1061
+ * register, slug, initialData, resolve }`. Set when "Create" is
1062
+ * chosen on a select-or-create reference; renders a nested form
1063
+ * for the referenced schema.
1064
+ */
1065
+ nestedCreate: null,
1028
1066
  }
1029
1067
  },
1030
1068
 
@@ -1295,6 +1333,7 @@ export default {
1295
1333
  .filter((field) => shouldShow(field, this.formData))
1296
1334
  .map((field) => this.applySemanticResolution(field))
1297
1335
  .map((field) => this.degradeUnresolvableReference(field))
1336
+ .map((field) => this.applyCodePicker(field))
1298
1337
  },
1299
1338
 
1300
1339
  /**
@@ -1809,11 +1848,16 @@ export default {
1809
1848
  // Create mode: initialize with field defaults
1810
1849
  const data = {}
1811
1850
  for (const field of this.resolvedFields) {
1851
+ const tokenDefault = field.defaultToken ? resolveDefaultToken(field.defaultToken, field) : null
1812
1852
  if (field.default !== null && field.default !== undefined) {
1813
1853
  data[field.key] = field.default
1854
+ } else if (tokenDefault !== null) {
1855
+ // `x-default: current-language` / `current-timezone`:
1856
+ // a NEW object starts with the user's own value.
1857
+ data[field.key] = tokenDefault
1814
1858
  } else if (field.widget === 'checkbox') {
1815
1859
  data[field.key] = false
1816
- } else if (field.widget === 'tags' || field.widget === 'multiselect' || field.widget === 'user-multiselect') {
1860
+ } else if (field.widget === 'tags' || field.widget === 'multiselect' || field.widget === 'user-multiselect' || field.widget === 'group-multiselect') {
1817
1861
  data[field.key] = []
1818
1862
  } else if (field.widget === 'code') {
1819
1863
  data[field.key] = ''
@@ -1880,10 +1924,35 @@ export default {
1880
1924
  this.resolveUserLabel(uid)
1881
1925
  }
1882
1926
  }
1927
+ } else if (this.isGroupField(field) || this.isGroupArrayField(field)) {
1928
+ const value = this.formData[field.key]
1929
+ for (const gid of (Array.isArray(value) ? value : [value])) {
1930
+ if (gid) {
1931
+ this.resolveGroupLabel(gid)
1932
+ }
1933
+ }
1883
1934
  }
1884
1935
  }
1885
1936
  },
1886
1937
 
1938
+ /**
1939
+ * Resolve a stored group id to its display name and cache it in
1940
+ * `referenceLabels`. No-op once cached; falls back to the gid.
1941
+ *
1942
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
1943
+ * @param {string} gid The stored group id.
1944
+ * @return {Promise<void>}
1945
+ */
1946
+ async resolveGroupLabel(gid) {
1947
+ if (!gid || this.referenceLabels[gid]) {
1948
+ return
1949
+ }
1950
+ const option = await resolveNextcloudGroup(gid)
1951
+ if (option && option.id) {
1952
+ this.referenceLabels = { ...this.referenceLabels, [option.id]: option.label || String(option.id) }
1953
+ }
1954
+ },
1955
+
1887
1956
  /**
1888
1957
  * Resolve a single user UID to its display name (edit mode) and cache it
1889
1958
  * in `referenceLabels` so the select shows the name instead of the UID.
@@ -2380,6 +2449,67 @@ export default {
2380
2449
  return !!(field && field.userPicker && field.userPicker.multiple)
2381
2450
  },
2382
2451
 
2452
+ /**
2453
+ * Whether a field is a single Nextcloud group (`format: 'nc-group'` or
2454
+ * `referenceType: 'nextcloud-group'`). Stores the group id.
2455
+ *
2456
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2457
+ * @param {object} field The field definition
2458
+ * @return {boolean}
2459
+ */
2460
+ isGroupField(field) {
2461
+ return !!(field && field.groupPicker && !field.groupPicker.multiple)
2462
+ },
2463
+
2464
+ /**
2465
+ * Whether a field is an array of Nextcloud groups.
2466
+ *
2467
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2468
+ * @param {object} field The field definition
2469
+ * @return {boolean}
2470
+ */
2471
+ isGroupArrayField(field) {
2472
+ return !!(field && field.groupPicker && field.groupPicker.multiple)
2473
+ },
2474
+
2475
+ /**
2476
+ * Whether a single-value field stores an id that maps to a label
2477
+ * fetched elsewhere: an object reference, a user or a group.
2478
+ *
2479
+ * @param {object} field The field definition
2480
+ * @return {boolean}
2481
+ */
2482
+ isIdPickerField(field) {
2483
+ return this.isReferenceField(field) || this.isUserField(field) || this.isGroupField(field)
2484
+ },
2485
+
2486
+ /**
2487
+ * The multi-value sibling of `isIdPickerField`.
2488
+ *
2489
+ * @param {object} field The field definition
2490
+ * @return {boolean}
2491
+ */
2492
+ isIdPickerArrayField(field) {
2493
+ return this.isReferenceArrayField(field) || this.isUserArrayField(field) || this.isGroupArrayField(field)
2494
+ },
2495
+
2496
+ /**
2497
+ * Turn a language or time zone field into a searchable select over
2498
+ * a fixed code list: labels in the user's language, the code stored.
2499
+ * Other fields pass through unchanged.
2500
+ *
2501
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2502
+ * @param {object} field A resolved field descriptor.
2503
+ * @return {object} The field, as a `select` when it is a code picker.
2504
+ */
2505
+ applyCodePicker(field) {
2506
+ if (!field || (field.widget !== 'language' && field.widget !== 'timezone')) {
2507
+ return field
2508
+ }
2509
+ const list = field.widget === 'language' ? languageOptions() : timezoneOptions()
2510
+ return { ...field, widget: 'select', codePicker: field.widget, enum: list.codes, enumLabels: list.labels }
2511
+ },
2512
+
2383
2513
  /**
2384
2514
  * Check if a field has an async enum (function instead of static array).
2385
2515
  * Object references (`$ref`) and Nextcloud user references are treated
@@ -2389,7 +2519,7 @@ export default {
2389
2519
  * @return {boolean}
2390
2520
  */
2391
2521
  isAsyncEnum(field) {
2392
- return typeof field.enum === 'function' || this.isReferenceField(field) || this.isUserField(field)
2522
+ return typeof field.enum === 'function' || this.isIdPickerField(field)
2393
2523
  },
2394
2524
 
2395
2525
  /**
@@ -2401,7 +2531,7 @@ export default {
2401
2531
  * @return {boolean}
2402
2532
  */
2403
2533
  isAsyncItemsEnum(field) {
2404
- return !!(field.items && typeof field.items.enum === 'function') || this.isReferenceArrayField(field) || this.isUserArrayField(field)
2534
+ return !!(field.items && typeof field.items.enum === 'function') || this.isIdPickerArrayField(field)
2405
2535
  },
2406
2536
 
2407
2537
  /**
@@ -2468,12 +2598,119 @@ export default {
2468
2598
  * @return {void}
2469
2599
  */
2470
2600
  onReferenceSelected(field, value) {
2601
+ if (Array.isArray(value)) {
2602
+ this.updateField(field.key, value)
2603
+ return
2604
+ }
2471
2605
  this.updateField(field.key, value || null)
2472
2606
  if (value) {
2473
2607
  this.applyTemplateFill(field, value)
2474
2608
  }
2475
2609
  },
2476
2610
 
2611
+ /**
2612
+ * The value CnResourceSelect binds for a select-or-create reference:
2613
+ * an array of ids for a multi-value reference, else the id string.
2614
+ *
2615
+ * @param {object} field The reference field descriptor.
2616
+ * @return {string|string[]} The bound value.
2617
+ */
2618
+ referenceModelValue(field) {
2619
+ const value = this.formData[field.key]
2620
+ if (field.reference && field.reference.multiple) {
2621
+ return Array.isArray(value) ? value.map((v) => String(v)) : []
2622
+ }
2623
+ return value !== null && value !== undefined ? String(value) : ''
2624
+ },
2625
+
2626
+ /**
2627
+ * "Create" on a select-or-create reference: open the referenced
2628
+ * schema's own form, prefilled with the typed term, on top of this one.
2629
+ *
2630
+ * Resolves with the created object once that form is saved, or with
2631
+ * null when it is closed without saving. CnResourceSelect then selects
2632
+ * the new object. When the referenced schema cannot be loaded, the
2633
+ * term alone is saved as before, so the field never dead-ends.
2634
+ *
2635
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2636
+ * @param {object} field The reference field descriptor.
2637
+ * @param {string} term The typed search term.
2638
+ * @return {Promise<object|null>} The created object, or null.
2639
+ */
2640
+ async openNestedCreate(field, term) {
2641
+ const store = this.getObjectStore()
2642
+ const register = this.referenceRegister(field)
2643
+ if (!store || !register || !field.reference) {
2644
+ return null
2645
+ }
2646
+ const schemaRef = String(field.reference.schema)
2647
+ const slug = store.createObjectTypeSlug(register, schemaRef)
2648
+ if (!store.objectTypeRegistry[slug]) {
2649
+ store.registerObjectType(slug, schemaRef, register)
2650
+ }
2651
+ const labelField = this.referenceLabelField(field)
2652
+ let schema = null
2653
+ try {
2654
+ schema = await store.fetchSchema(slug)
2655
+ } catch {
2656
+ schema = null
2657
+ }
2658
+ if (!schema || !schema.properties) {
2659
+ return store.saveObject(slug, { [labelField]: term })
2660
+ }
2661
+ const initialData = {}
2662
+ if (term && schema.properties[labelField]) {
2663
+ initialData[labelField] = term
2664
+ }
2665
+ return new Promise((resolve) => {
2666
+ this.nestedCreate = { field, schema, register, slug, initialData, resolve }
2667
+ })
2668
+ },
2669
+
2670
+ /**
2671
+ * The nested create form was confirmed: save the object, hand it to
2672
+ * the waiting picker and close the nested form. A refused save keeps
2673
+ * the nested form open with the server's message.
2674
+ *
2675
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2676
+ * @param {object} formData The nested form's values.
2677
+ * @return {Promise<void>}
2678
+ */
2679
+ async onNestedCreateConfirm(formData) {
2680
+ const pending = this.nestedCreate
2681
+ if (!pending) {
2682
+ return
2683
+ }
2684
+ const store = this.getObjectStore()
2685
+ const created = store ? await store.saveObject(pending.slug, formData) : null
2686
+ if (!created) {
2687
+ const error = store && store.errors ? store.errors[pending.slug] : null
2688
+ const message = (error && error.message)
2689
+ || t('nextcloud-vue', 'The item could not be created.')
2690
+ const dialog = this.$refs.nestedCreateDialog
2691
+ if (dialog && typeof dialog.setValidationErrors === 'function') {
2692
+ dialog.setValidationErrors({}, message)
2693
+ }
2694
+ return
2695
+ }
2696
+ this.nestedCreate = null
2697
+ pending.resolve(created)
2698
+ },
2699
+
2700
+ /**
2701
+ * The nested create form was closed without saving: leave the field
2702
+ * as it was.
2703
+ *
2704
+ * @return {void}
2705
+ */
2706
+ onNestedCreateClose() {
2707
+ const pending = this.nestedCreate
2708
+ this.nestedCreate = null
2709
+ if (pending) {
2710
+ pending.resolve(null)
2711
+ }
2712
+ },
2713
+
2477
2714
  /**
2478
2715
  * Copy template values off a selected/created reference object into the
2479
2716
  * form, per the field's `fillFrom` map (`{ formKey: sourceKey }`). When
@@ -2920,9 +3157,14 @@ export default {
2920
3157
  }
2921
3158
  this.asyncState = newState
2922
3159
 
2923
- // Trigger initial load for each async field
3160
+ // Trigger initial load for each async field. A select-or-create
3161
+ // reference loads its own options (CnResourceSelect), so it is
3162
+ // skipped here rather than fetched twice.
2924
3163
  this.$nextTick(() => {
2925
3164
  for (const field of fields) {
3165
+ if (field.allowCreate && field.reference) {
3166
+ continue
3167
+ }
2926
3168
  if (this.isAsyncEnum(field) || this.isAsyncItemsEnum(field)) {
2927
3169
  this.loadAsyncOptions(field, '')
2928
3170
  }
@@ -2963,6 +3205,18 @@ export default {
2963
3205
  if (Object.keys(labels).length > 0) {
2964
3206
  this.referenceLabels = { ...this.referenceLabels, ...labels }
2965
3207
  }
3208
+ } else if (this.isGroupField(field) || this.isGroupArrayField(field)) {
3209
+ // Nextcloud group: search groups, store the gid, show the name.
3210
+ results = await searchNextcloudGroups(query)
3211
+ const labels = {}
3212
+ for (const opt of results) {
3213
+ if (opt && opt.id) {
3214
+ labels[opt.id] = opt.label || String(opt.id)
3215
+ }
3216
+ }
3217
+ if (Object.keys(labels).length > 0) {
3218
+ this.referenceLabels = { ...this.referenceLabels, ...labels }
3219
+ }
2966
3220
  } else {
2967
3221
  const enumFn = typeof field.enum === 'function' ? field.enum : field.items.enum
2968
3222
  results = await enumFn(query)
@@ -3021,7 +3275,7 @@ export default {
3021
3275
  * @return {object|null}
3022
3276
  */
3023
3277
  getEffectiveSelectedOption(field) {
3024
- if (this.isReferenceField(field) || this.isUserField(field)) {
3278
+ if (this.isIdPickerField(field)) {
3025
3279
  // Reference / user fields store the UUID/UID — resolve it to a
3026
3280
  // display option `{ id, label }` (label from the resolved-labels
3027
3281
  // cache, falling back to the id until it loads).
@@ -3048,7 +3302,7 @@ export default {
3048
3302
  * @param {object|null} option The selected option
3049
3303
  */
3050
3304
  onEffectiveSelectChange(field, option) {
3051
- if (this.isReferenceField(field) || this.isUserField(field)) {
3305
+ if (this.isIdPickerField(field)) {
3052
3306
  // Reference / user fields store the chosen id (UUID / UID),
3053
3307
  // not the full option. Cache its label so the selection displays.
3054
3308
  if (option && option.id) {
@@ -3057,7 +3311,7 @@ export default {
3057
3311
  this.updateField(field.key, option ? option.id : null)
3058
3312
  // Reference options are label-only ({id,label}) — pass the id so
3059
3313
  // template pre-fill fetches the full object.
3060
- if (option && option.id) {
3314
+ if (option && option.id && this.isReferenceField(field)) {
3061
3315
  this.applyTemplateFill(field, String(option.id))
3062
3316
  }
3063
3317
  } else if (this.isAsyncEnum(field)) {
@@ -3089,7 +3343,7 @@ export default {
3089
3343
  * @return {Array}
3090
3344
  */
3091
3345
  getEffectiveSelectedArrayOptions(field) {
3092
- if (this.isReferenceArrayField(field) || this.isUserArrayField(field)) {
3346
+ if (this.isIdPickerArrayField(field)) {
3093
3347
  // Reference / user arrays store an array of ids (UUIDs / UIDs) —
3094
3348
  // resolve each to a display option `{ id, label }`.
3095
3349
  const uuids = this.formData[field.key]
@@ -3112,7 +3366,7 @@ export default {
3112
3366
  * @param {Array} options The selected options
3113
3367
  */
3114
3368
  onEffectiveMultiSelectChange(field, options) {
3115
- if (this.isReferenceArrayField(field) || this.isUserArrayField(field)) {
3369
+ if (this.isIdPickerArrayField(field)) {
3116
3370
  // Reference / user arrays store an array of ids (UUIDs / UIDs).
3117
3371
  // Cache labels so the chips still display the human names.
3118
3372
  const list = options || []
@@ -3408,6 +3662,11 @@ export default {
3408
3662
  margin-inline-end: auto;
3409
3663
  }
3410
3664
 
3665
+ .cn-form-dialog__switch-wrapper {
3666
+ display: flex;
3667
+ flex-direction: column;
3668
+ }
3669
+
3411
3670
  .cn-form-dialog__form {
3412
3671
  display: flex;
3413
3672
  flex-direction: column;
@@ -450,6 +450,10 @@
450
450
  :includeColumns="includeColumns"
451
451
  :columnOverrides="columnOverrides"
452
452
  :rowClass="rowClass"
453
+ :filterable="headerFilters && !!effectiveSchema"
454
+ :activeFilters="effectiveActiveFilters"
455
+ :filterRegister="typeof register === 'string' ? register : ''"
456
+ @columnFilter="onColumnFilterEvent"
453
457
  @sort="onSortEvent"
454
458
  @select="onSelect"
455
459
  @rowClick="onRowClick"
@@ -798,6 +802,7 @@ import { DEFAULT_ROW_INDICATOR_CAP } from '../../utils/rowIndicators.js'
798
802
  import { buildRouteQueryFromViewState, buildViewCreatePayload, extractViewState, extractViewStateFromRouteQuery, savedViewScope, viewMatchesScope } from '../../utils/savedViewHelpers.js'
799
803
  import { columnsFromSchema } from '../../utils/schema.js'
800
804
  import { resolveScopeLayout } from '../../utils/scopeListLayout.js'
805
+ import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
801
806
  import { CnActionsBar } from '../CnActionsBar/index.js'
802
807
  import { CnAdvancedFormDialog } from '../CnAdvancedFormDialog/index.js'
803
808
  import { CnBoardView } from '../CnBoardView/index.js'
@@ -2285,6 +2290,22 @@ export default {
2285
2290
  default: false,
2286
2291
  },
2287
2292
 
2293
+ /**
2294
+ * Per-column filters in the table header: a filter button on every
2295
+ * column that can filter, opening a small panel that fits the column
2296
+ * (checkboxes, yes/no/any, from and to, a searchable reference list).
2297
+ * On by default; a column opts out with `filterable: false`, a page
2298
+ * with `pages[].config.headerFilters: false`. Filters write into the
2299
+ * same active-filter map as the facet sidebar, so they persist in the
2300
+ * route query and reach the same OpenRegister query parameters.
2301
+ *
2302
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
2303
+ */
2304
+ headerFilters: {
2305
+ type: Boolean,
2306
+ default: true,
2307
+ },
2308
+
2288
2309
  /**
2289
2310
  * Show a filter menu (funnel button) in the table header, above the
2290
2311
  * row-actions column. Its menu lists every enum/badge column's values as
@@ -5225,6 +5246,39 @@ export default {
5225
5246
  this.$emit('filter-change', payload)
5226
5247
  },
5227
5248
 
5249
+ /**
5250
+ * A header filter was applied or cleared. One column can own several
5251
+ * query parameters (a range is `key[gte]` and `key[lte]`), so they are
5252
+ * applied together: one fetch, one route update.
5253
+ *
5254
+ * @param {{key: string, params: object}} payload From CnDataTable's `column-filter`.
5255
+ * @return {void}
5256
+ * @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-a-header-filter-speaks-the-sidebars-query-language
5257
+ */
5258
+ onColumnFilterEvent(payload) {
5259
+ const params = (payload && payload.params) || {}
5260
+ const entries = Object.entries(params).map(([key, values]) => [key, Array.isArray(values) ? values : []])
5261
+ if (this.isSelfFetchMode) {
5262
+ const next = { ...this.list.activeFilters.value }
5263
+ for (const [key, values] of entries) {
5264
+ if (values.length === 0) {
5265
+ delete next[key]
5266
+ } else {
5267
+ next[key] = values
5268
+ }
5269
+ }
5270
+ this.list.activeFilters.value = next
5271
+ this.list.refresh(1)
5272
+ this.persistViewStateToRoute(this.currentViewState())
5273
+ }
5274
+ for (const [key, values] of entries) {
5275
+ if (this.isNamedSource) {
5276
+ this.setNamedFilter(key, values)
5277
+ }
5278
+ this.$emit('filter-change', { key, values })
5279
+ }
5280
+ },
5281
+
5228
5282
  /**
5229
5283
  * Record one sidebar choice on a named-source page and mirror it into
5230
5284
  * the URL.
@@ -6460,8 +6514,7 @@ export default {
6460
6514
  if (typeof window === 'undefined' || !created) {
6461
6515
  return
6462
6516
  }
6463
- const detail = { ...created, register: this.register, schema: this.exportSchemaSlug }
6464
- window.dispatchEvent(new CustomEvent('cn-walkthrough:object-created', { detail }))
6517
+ dispatchObjectCreated({ register: this.register, schema: this.exportSchemaSlug, object: created })
6465
6518
  },
6466
6519
 
6467
6520
  /**
@@ -1,3 +1,4 @@
1
+ import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
1
2
  import {
2
3
  cloneObjectForCopy,
3
4
  runSelfExportRequest,
@@ -196,9 +197,8 @@ export function createSelfModeActions(ctx) {
196
197
  // @self.register/@self.schema are numeric DB ids, not slugs, so
197
198
  // override with this page's own slug props (self-fetch mode
198
199
  // guarantees ctx.schema() is already a string).
199
- if (isCreate && typeof window !== 'undefined') {
200
- const detail = { ...saved, register: ctx.register(), schema: ctx.schema() }
201
- window.dispatchEvent(new CustomEvent('cn-walkthrough:object-created', { detail }))
200
+ if (isCreate) {
201
+ dispatchObjectCreated({ register: ctx.register(), schema: ctx.schema(), object: saved })
202
202
  }
203
203
  refreshList(ctx)
204
204
  if (isCreate && typeof ctx.afterCreateSuccess === 'function') {
@@ -395,6 +395,13 @@ export default {
395
395
  padding: calc(5 * var(--default-grid-baseline));
396
396
  }
397
397
 
398
+ /* Clear the Nextcloud navigation toggle button (44px wide, absolutely
399
+ positioned at the left edge of .app-content) plus 12px breathing room, as
400
+ CnPageHeader does. Only the header shifts; the cards keep the full width. */
401
+ .cn-link-cards-page__header {
402
+ padding-inline-start: 56px;
403
+ }
404
+
398
405
  .cn-link-cards-page__title {
399
406
  margin: 0 0 var(--default-grid-baseline);
400
407
  }
@@ -445,12 +452,32 @@ export default {
445
452
  gap: calc(3 * var(--default-grid-baseline));
446
453
  min-width: 0;
447
454
  padding: calc(4 * var(--default-grid-baseline));
448
- text-decoration: none;
455
+ transition: background-color var(--animation-quick, 100ms) ease, border-color var(--animation-quick, 100ms) ease, box-shadow var(--animation-quick, 100ms) ease;
456
+ }
457
+
458
+ /* A card is a link, and it must not look like a line of prose. A theme may
459
+ underline every link with `!important` (thematiq: `a { text-decoration:
460
+ underline !important }`, specificity 0,0,1). Among `!important` rules the
461
+ higher specificity wins, so this class rule has to carry it too; without it
462
+ the label and the description both rendered underlined on a live Nextcloud. */
463
+ .cn-link-cards-page__card,
464
+ .cn-link-cards-page__card:hover,
465
+ .cn-link-cards-page__card:focus,
466
+ .cn-link-cards-page__card:active {
467
+ text-decoration: none !important;
449
468
  }
450
469
 
470
+ /* Hover moves the whole card: border, background and a soft lift. */
451
471
  .cn-link-cards-page__card:hover {
452
472
  background: var(--color-background-hover);
453
473
  border-color: var(--color-primary-element);
474
+ box-shadow: 0 2px 6px var(--color-box-shadow);
475
+ }
476
+
477
+ @media (prefers-reduced-motion: reduce) {
478
+ .cn-link-cards-page__card {
479
+ transition: none;
480
+ }
454
481
  }
455
482
 
456
483
  .cn-link-cards-page__card:focus-visible {
@@ -471,8 +498,14 @@ export default {
471
498
  min-width: 0;
472
499
  }
473
500
 
501
+ /* The label is the card's title. Its colour is set here, on the span, because
502
+ a theme may force its link colour on the `a` itself; an inherited link
503
+ colour made the title read as a hyperlink. */
474
504
  .cn-link-cards-page__label {
475
- font-weight: 600;
505
+ color: var(--color-main-text);
506
+ font-size: 1.05em;
507
+ font-weight: 700;
508
+ line-height: 1.3;
476
509
  overflow-wrap: anywhere;
477
510
  }
478
511