@conduction/nextcloud-vue 2.62.0 → 2.64.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 (209) 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 +425 -39
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
  10. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
  14. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  25. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  26. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  27. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
  29. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  35. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  36. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  37. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
  39. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
  41. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  42. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
  43. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  44. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
  45. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  47. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  48. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  50. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  51. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  52. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  54. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
  58. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
  60. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  65. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  66. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  67. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
  68. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  72. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  73. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  74. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  76. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  77. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  78. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  80. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  82. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  84. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  85. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
  86. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  87. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
  88. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  89. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  90. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  91. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  92. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  99. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  100. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  101. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  102. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  103. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  104. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  110. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  111. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  112. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  113. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  114. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  115. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  116. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  117. package/dist/esm/composables/useDependencyCheck.js +65 -0
  118. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  119. package/dist/esm/composables/useWalkthrough.js +218 -6
  120. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  121. package/dist/esm/index.js +2 -0
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/l10n/en.json.js +45 -1
  124. package/dist/esm/l10n/en.json.js.map +1 -1
  125. package/dist/esm/l10n/nl.json.js +45 -1
  126. package/dist/esm/l10n/nl.json.js.map +1 -1
  127. package/dist/esm/nextcloud-vue.css +780 -81
  128. package/dist/esm/store/useObjectStore.js +8 -0
  129. package/dist/esm/store/useObjectStore.js.map +1 -1
  130. package/dist/esm/utils/columnFilters.js +311 -0
  131. package/dist/esm/utils/columnFilters.js.map +1 -0
  132. package/dist/esm/utils/groupAutocomplete.js +123 -0
  133. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  134. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  135. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  136. package/dist/esm/utils/pickerOptions.js +198 -0
  137. package/dist/esm/utils/pickerOptions.js.map +1 -0
  138. package/dist/esm/utils/schema.js +83 -7
  139. package/dist/esm/utils/schema.js.map +1 -1
  140. package/dist/esm/utils/schemaRefSlug.js +19 -3
  141. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  142. package/dist/esm/utils/timelineEvents.js +174 -0
  143. package/dist/esm/utils/timelineEvents.js.map +1 -0
  144. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  145. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  146. package/dist/esm/utils/walkthroughSignals.js +88 -0
  147. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  148. package/dist/nextcloud-vue.cjs.js +13088 -7937
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +780 -81
  151. package/l10n/en.json +45 -0
  152. package/l10n/nl.json +45 -0
  153. package/package.json +1 -1
  154. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  155. package/src/components/CnAdminActionCard/index.js +4 -0
  156. package/src/components/CnAppNav/CnAppNav.vue +568 -43
  157. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  158. package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
  159. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  160. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  161. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  162. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  163. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
  164. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  165. package/src/components/CnDataTable/CnDataTable.md +3 -0
  166. package/src/components/CnDataTable/CnDataTable.vue +305 -5
  167. package/src/components/CnDetailPage/CnDetailPage.md +4 -0
  168. package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
  169. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  170. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  171. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  172. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
  173. package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
  174. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  175. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
  176. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  177. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  178. package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
  179. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  180. package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  182. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  183. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  184. package/src/components/CnTimelineWidget/index.js +4 -0
  185. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  186. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  187. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  188. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  189. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  190. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  191. package/src/components/index.js +2 -0
  192. package/src/composables/useDependencyCheck.js +61 -0
  193. package/src/composables/useWalkthrough.js +217 -5
  194. package/src/css/dashboard.css +49 -0
  195. package/src/css/detail-page.css +29 -4
  196. package/src/css/kpi-card.css +24 -1
  197. package/src/css/table.css +99 -6
  198. package/src/index.js +2 -0
  199. package/src/schemas/app-manifest-v2.schema.json +135 -3
  200. package/src/store/useObjectStore.js +8 -0
  201. package/src/utils/columnFilters.js +324 -0
  202. package/src/utils/groupAutocomplete.js +119 -0
  203. package/src/utils/libraryWidgetKeys.js +2 -0
  204. package/src/utils/pickerOptions.js +194 -0
  205. package/src/utils/schema.js +83 -7
  206. package/src/utils/schemaRefSlug.js +19 -3
  207. package/src/utils/timelineEvents.js +171 -0
  208. package/src/utils/validateManifestV2.compiled.js +2 -2
  209. 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;
@@ -4,7 +4,7 @@
4
4
  -->
5
5
 
6
6
  <template>
7
- <div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain }" :style="wrapperStyle">
7
+ <div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
8
8
  <div
9
9
  v-if="hasOverlay"
10
10
  class="cn-header-widget__overlay"
@@ -34,6 +34,16 @@
34
34
  {{ ctaLabel }}
35
35
  </a>
36
36
  </div>
37
+ <!-- A view switch at the right of the heading (`content.views`): "My
38
+ work / My team" as a segmented control whose options are routes. -->
39
+ <div v-if="viewOptions.length > 0" class="cn-header-widget__views">
40
+ <CnSegmentedControl
41
+ :options="viewOptions"
42
+ :modelValue="activeView"
43
+ :ariaLabel="viewsLabel"
44
+ data-testid="cn-header-widget-views"
45
+ @update:modelValue="onViewChange" />
46
+ </div>
37
47
  <img
38
48
  v-if="hasBackgroundImage"
39
49
  class="cn-header-widget__probe"
@@ -47,6 +57,7 @@
47
57
  <script>
48
58
  import { getCurrentUser } from '@nextcloud/auth'
49
59
  import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
60
+ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
50
61
  import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
51
62
 
52
63
  const ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']
@@ -99,6 +110,8 @@ const VERTICAL_ALIGN_FLEX = Object.freeze({
99
110
  export default {
100
111
  name: 'CnHeaderWidget',
101
112
 
113
+ components: { CnSegmentedControl },
114
+
102
115
  inject: {
103
116
  /**
104
117
  * Host translate function provided by CnAppRoot as
@@ -115,9 +128,12 @@ export default {
115
128
  * Persisted widget content: `{title, subtitle, backgroundImageUrl,
116
129
  * backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
117
130
  * overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
118
- * greeting, showDate, plain}`. All fields are optional except `title`
119
- * (or `greeting`); unknown enum values collapse to documented defaults
120
- * and the renderer never throws.
131
+ * greeting, showDate, plain, views}`. All fields are optional except
132
+ * `title` (or `greeting`); unknown enum values collapse to documented
133
+ * defaults and the renderer never throws. `views` is
134
+ * `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
135
+ * control at the right of the heading whose checked option is the
136
+ * current route; choosing another pushes its route.
121
137
  *
122
138
  * @type {object}
123
139
  */
@@ -198,6 +214,55 @@ export default {
198
214
  return this.now instanceof Date ? this.now : new Date()
199
215
  },
200
216
 
217
+ /**
218
+ * The view switch's options from `content.views.options`: each
219
+ * `{ label, route, params? }` with a non-empty label and route becomes
220
+ * a segmented-control option keyed on its route name (the label goes
221
+ * through the host translate function). Empty without a router, so
222
+ * the switch cannot offer routes it cannot follow.
223
+ *
224
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
225
+ * @return {Array<{value: string, label: string}>}
226
+ */
227
+ viewOptions() {
228
+ const raw = this.content && this.content.views && this.content.views.options
229
+ if (!Array.isArray(raw) || !this.$router) {
230
+ return []
231
+ }
232
+ return raw
233
+ .filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')
234
+ .map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))
235
+ },
236
+
237
+ /**
238
+ * The checked view: the option whose route is the current route, else
239
+ * the first option (a switch always has one checked option).
240
+ *
241
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
242
+ * @return {string|null}
243
+ */
244
+ activeView() {
245
+ const options = this.viewOptions
246
+ if (options.length === 0) {
247
+ return null
248
+ }
249
+ const current = this.$route && this.$route.name
250
+ const match = options.find((o) => o.value === current)
251
+ return match ? match.value : options[0].value
252
+ },
253
+
254
+ /**
255
+ * Accessible name of the view switch: `content.views.ariaLabel`
256
+ * through the host translate function, else "View".
257
+ *
258
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
259
+ * @return {string}
260
+ */
261
+ viewsLabel() {
262
+ const label = this.content && this.content.views && this.content.views.ariaLabel
263
+ return label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')
264
+ },
265
+
201
266
  /**
202
267
  * Whether the plain presentation is on (no coloured background).
203
268
  *
@@ -490,7 +555,9 @@ export default {
490
555
  return {
491
556
  position: 'relative',
492
557
  'z-index': 1,
493
- width: '100%',
558
+ // Beside a view switch the content shares the row instead of
559
+ // taking the whole width (see .cn-header-widget--with-views).
560
+ width: this.viewOptions.length > 0 ? 'auto' : '100%',
494
561
  height: '100%',
495
562
  display: 'flex',
496
563
  'flex-direction': 'column',
@@ -604,6 +671,26 @@ export default {
604
671
  },
605
672
 
606
673
  methods: {
674
+ /**
675
+ * Follow the chosen view: push its route (with the option's `params`
676
+ * when declared). Nothing happens for the route already shown.
677
+ *
678
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
679
+ * @param {string} route The chosen option's route name.
680
+ * @return {void}
681
+ */
682
+ onViewChange(route) {
683
+ if (!this.$router || !route || route === (this.$route && this.$route.name)) {
684
+ return
685
+ }
686
+ const declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}
687
+ const target = { name: route }
688
+ if (declared.params && typeof declared.params === 'object') {
689
+ target.params = declared.params
690
+ }
691
+ this.$router.push(target)
692
+ },
693
+
607
694
  /**
608
695
  * Mark the background image as broken so the renderer falls back to
609
696
  * the solid colour.
@@ -655,6 +742,29 @@ export default {
655
742
  border-radius: 0;
656
743
  }
657
744
 
745
+ /* With a view switch the heading and the switch share one row, the switch
746
+ at the right and bottom-aligned with the heading; on a narrow card the
747
+ switch wraps under it. */
748
+ .cn-header-widget--with-views {
749
+ display: flex;
750
+ flex-wrap: wrap;
751
+ align-items: flex-end;
752
+ justify-content: space-between;
753
+ gap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));
754
+ }
755
+
756
+ .cn-header-widget--with-views .cn-header-widget__content {
757
+ flex: 1 1 320px;
758
+ width: auto;
759
+ }
760
+
761
+ .cn-header-widget__views {
762
+ position: relative;
763
+ z-index: 1;
764
+ flex: 0 0 auto;
765
+ padding: 16px;
766
+ }
767
+
658
768
  .cn-header-widget__date {
659
769
  font-size: 14px;
660
770
  line-height: 1.4;
@@ -726,7 +836,7 @@ export default {
726
836
  height: 1px;
727
837
  opacity: 0;
728
838
  pointer-events: none;
729
- left: -9999px;
839
+ inset-inline-start: -9999px;
730
840
  top: -9999px;
731
841
  }
732
842