@conduction/nextcloud-vue 2.97.0 → 2.99.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 (160) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
  9. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
  11. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  12. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
  19. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
  20. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
  21. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
  22. package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
  23. package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
  32. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  35. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  36. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
  37. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  40. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
  41. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
  43. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +43 -3
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
  48. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
  49. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +239 -0
  50. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
  51. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +182 -0
  52. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
  53. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
  54. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
  55. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
  56. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
  57. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
  58. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
  59. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
  60. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
  61. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +508 -0
  62. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
  63. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
  64. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
  65. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  66. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
  67. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  68. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
  69. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  71. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
  72. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  73. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
  74. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  75. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
  76. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  77. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
  78. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
  79. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
  80. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
  81. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
  82. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
  83. package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
  84. package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
  85. package/dist/esm/composables/indexSources.js +238 -2
  86. package/dist/esm/composables/indexSources.js.map +1 -1
  87. package/dist/esm/composables/useFlowStore.js +2 -1
  88. package/dist/esm/composables/useFlowStore.js.map +1 -1
  89. package/dist/esm/composables/useOrganisationStore.js +115 -0
  90. package/dist/esm/composables/useOrganisationStore.js.map +1 -0
  91. package/dist/esm/index.js +5 -0
  92. package/dist/esm/index.js.map +1 -1
  93. package/dist/esm/l10n/en.json.js +109 -1
  94. package/dist/esm/l10n/en.json.js.map +1 -1
  95. package/dist/esm/l10n/nl.json.js +138 -1
  96. package/dist/esm/l10n/nl.json.js.map +1 -1
  97. package/dist/esm/library-style.js +1 -1
  98. package/dist/esm/nextcloud-vue.css +1071 -274
  99. package/dist/esm/utils/builtInFormatters.js +34 -1
  100. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  101. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  102. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  103. package/dist/esm/utils/matrixCells.js +158 -0
  104. package/dist/esm/utils/matrixCells.js.map +1 -0
  105. package/dist/esm/utils/nextcloudDirectory.js +121 -0
  106. package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
  107. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/esm/utils/workingCalendarMonth.js +85 -0
  110. package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
  111. package/dist/nextcloud-vue.cjs.js +14888 -10014
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1071 -274
  114. package/l10n/en.json +109 -1
  115. package/l10n/nl.json +138 -1
  116. package/package.json +2 -2
  117. package/scripts/merge-l10n.js +196 -0
  118. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  119. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  120. package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
  121. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
  122. package/src/components/CnChartWidget/CnChartWidget.md +2 -1
  123. package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
  124. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
  125. package/src/components/CnChecklistRunWidget/index.js +27 -0
  126. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  127. package/src/components/CnDataTable/CnDataTable.vue +26 -8
  128. package/src/components/CnDetailPage/CnDetailPage.md +48 -0
  129. package/src/components/CnDetailPage/CnDetailPage.vue +189 -5
  130. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
  131. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
  132. package/src/components/CnIndexPage/CnIndexPage.md +28 -0
  133. package/src/components/CnIndexPage/CnIndexPage.vue +43 -3
  134. package/src/components/CnOrderedListField/CnOrderedListField.vue +458 -0
  135. package/src/components/CnOrderedListField/index.js +7 -0
  136. package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
  137. package/src/components/CnSettingsField/index.js +4 -0
  138. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +613 -0
  139. package/src/components/CnSettingsFieldsSection/index.js +4 -0
  140. package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
  141. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
  142. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
  143. package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
  144. package/src/components/CnWorkingCalendarWidget/index.js +26 -0
  145. package/src/components/index.js +5 -0
  146. package/src/composables/indexSources.js +237 -1
  147. package/src/composables/useFlowStore.js +2 -1
  148. package/src/composables/useOrganisationStore.js +111 -0
  149. package/src/css/badge.css +32 -16
  150. package/src/css/look-board-detail.css +80 -8
  151. package/src/css/look-board-index.css +52 -8
  152. package/src/css/look-board.css +219 -1
  153. package/src/index.js +5 -0
  154. package/src/schemas/app-manifest-v2.schema.json +33 -3
  155. package/src/utils/builtInFormatters.js +33 -0
  156. package/src/utils/libraryWidgetKeys.js +2 -0
  157. package/src/utils/matrixCells.js +154 -0
  158. package/src/utils/nextcloudDirectory.js +117 -0
  159. package/src/utils/validateManifestV2.compiled.js +3 -3
  160. package/src/utils/workingCalendarMonth.js +82 -0
@@ -172,34 +172,9 @@
172
172
  :field="field"
173
173
  :value="formData[field.key]"
174
174
  :onInput="(v) => updateField(field.key, v)">
175
- <NcCheckboxRadioSwitch
176
- v-if="field.type === 'boolean'"
177
- :modelValue="!!formData[field.key]"
178
- @update:modelValue="updateField(field.key, $event)">
179
- {{ resolveLabel(field.label) }}
180
- </NcCheckboxRadioSwitch>
181
- <NcTextField
182
- v-else-if="field.type === 'number'"
183
- :label="resolveLabel(field.label)"
184
- type="number"
185
- :modelValue="String(fieldValue(field.key, ''))"
186
- @update:modelValue="updateField(field.key, $event === '' ? null : Number($event))" />
187
- <NcTextField
188
- v-else-if="field.type === 'password'"
189
- :label="resolveLabel(field.label)"
190
- type="password"
191
- :modelValue="fieldValue(field.key, '')"
192
- @update:modelValue="updateField(field.key, $event)" />
193
- <NcSelect
194
- v-else-if="field.type === 'enum' && Array.isArray(field.options)"
195
- :modelValue="selectedOption(field)"
196
- :options="field.options"
197
- :inputLabel="resolveLabel(field.label)"
198
- @update:modelValue="updateField(field.key, optionValue($event))" />
199
- <NcTextField
200
- v-else
201
- :label="resolveLabel(field.label)"
202
- :modelValue="fieldValue(field.key, '')"
175
+ <CnSettingsField
176
+ :field="{ ...field, label: resolveLabel(field.label) }"
177
+ :modelValue="formData[field.key]"
203
178
  @update:modelValue="updateField(field.key, $event)" />
204
179
  </slot>
205
180
  <small
@@ -297,14 +272,12 @@ import axios from '@nextcloud/axios'
297
272
  import { translate as t } from '@nextcloud/l10n'
298
273
  import {
299
274
  NcButton,
300
- NcCheckboxRadioSwitch,
301
275
  NcLoadingIcon,
302
- NcSelect,
303
- NcTextField,
304
276
  } from '@nextcloud/vue'
305
277
  import ContentSave from 'vue-material-design-icons/ContentSave.vue'
306
278
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
307
279
  import CnRegisterMapping from '../CnRegisterMapping/CnRegisterMapping.vue'
280
+ import CnSettingsField from '../CnSettingsField/CnSettingsField.vue'
308
281
  import CnToggleTableWidget from '../CnToggleTableWidget/CnToggleTableWidget.vue'
309
282
  import CnVersionInfoCard from '../CnVersionInfoCard/CnVersionInfoCard.vue'
310
283
  import { useLook } from '../../composables/useLook.js'
@@ -356,7 +329,7 @@ const BUILTIN_SETTINGS_WIDGETS = Object.freeze({
356
329
  * three body kinds:
357
330
  *
358
331
  * - `fields[]` — flat form fields (back-compat default).
359
- * Each field's `type` controls which input renders:
332
+ * Each field's `type` controls which input CnSettingsField renders:
360
333
  * - `boolean` → NcCheckboxRadioSwitch
361
334
  * - `number` → NcTextField (type=number)
362
335
  * - `string` → NcTextField (default)
@@ -409,11 +382,9 @@ export default {
409
382
 
410
383
  components: {
411
384
  NcButton,
412
- NcCheckboxRadioSwitch,
413
385
  NcLoadingIcon,
414
- NcSelect,
415
- NcTextField,
416
386
  ContentSave,
387
+ CnSettingsField,
417
388
  CnBuildiqEditButton,
418
389
  CnSettingsCard,
419
390
  CnSettingsSection,
@@ -823,57 +794,6 @@ export default {
823
794
  },
824
795
 
825
796
  methods: {
826
- // Coalesce a field value to a fallback when null / undefined.
827
- // Used in template bindings instead of `formData[key] ?? ''` because
828
- // vue-template-es2015-compiler (the Rollup VuePlugin's parser) does
829
- // not accept ES2020 nullish-coalescing inside template expressions.
830
- fieldValue(key, fallback) {
831
- const v = this.formData[key]
832
- return (v === null || v === undefined) ? fallback : v
833
- },
834
-
835
- // Resolve the currently-selected option for an enum field.
836
- // `field.options` may be an array of strings/numbers OR an array
837
- // of `{ label, value }`-shaped objects. NcSelect needs the actual
838
- // option entry (not the bare value) to render the selection
839
- // correctly, so this resolver looks up the matching object form
840
- // when a primitive value is stored. Returns `null` when nothing
841
- // is selected so NcSelect renders the empty state instead of
842
- // emitting a Vue warning about an undefined `value` prop.
843
- selectedOption(field) {
844
- const v = this.formData[field.key]
845
- if (v === null || v === undefined) {
846
- return null
847
- }
848
- if (!Array.isArray(field.options)) {
849
- return v
850
- }
851
- for (const opt of field.options) {
852
- if (opt && typeof opt === 'object') {
853
- if (opt.value === v) {
854
- return opt
855
- }
856
- } else if (opt === v) {
857
- return opt
858
- }
859
- }
860
- return v
861
- },
862
-
863
- // Inverse of `selectedOption` — extract the storable value from
864
- // whatever NcSelect emits. NcSelect emits the full option entry
865
- // when options are objects, the primitive when options are
866
- // primitives, or `null` on clear.
867
- optionValue(emitted) {
868
- if (emitted === null || emitted === undefined) {
869
- return null
870
- }
871
- if (typeof emitted === 'object' && 'value' in emitted) {
872
- return emitted.value
873
- }
874
- return emitted
875
- },
876
-
877
797
  cloneInitial() {
878
798
  const merged = { ...(this.initialValues || {}) }
879
799
  // Collect every section across both modes (flat
@@ -21,7 +21,24 @@
21
21
  class="cn-user-settings-dialog__nav"
22
22
  :aria-label="navLabel"
23
23
  data-testid="cn-user-settings-nav">
24
- <ul>
24
+ <!-- `nextcloudLinks`: Nextcloud's own personal settings first, as
25
+ links, then one entry for this app holding its sections. -->
26
+ <ul v-if="ncLinks.length > 0" class="cn-user-settings-dialog__nc-links" data-testid="cn-user-settings-nc-links">
27
+ <li v-for="link in ncLinks" :key="link.key">
28
+ <a :href="link.href" class="cn-user-settings-dialog__nav-link">
29
+ <span v-if="link.icon" class="cn-user-settings-dialog__nav-icon" aria-hidden="true">
30
+ <CnIcon :name="link.icon" :size="18" />
31
+ </span>
32
+ <span>{{ link.label }}</span>
33
+ </a>
34
+ </li>
35
+ </ul>
36
+ <p v-if="ncLinks.length > 0 && appName"
37
+ class="cn-user-settings-dialog__group"
38
+ data-testid="cn-user-settings-app-group">
39
+ {{ appName }}
40
+ </p>
41
+ <ul :class="{ 'cn-user-settings-dialog__group-sections': ncLinks.length > 0 }">
25
42
  <li v-for="section in sections" :key="section.id">
26
43
  <a
27
44
  :href="`#settings-section_${section.id}`"
@@ -41,6 +58,14 @@
41
58
  <div class="cn-user-settings-dialog__sections">
42
59
  <!-- @slot default NcAppSettingsSection children; each registers itself in the navigation. -->
43
60
  <slot />
61
+ <!-- A section's `#icon` also stands before its heading. -->
62
+ <template v-for="section in sections" :key="`heading-icon-${section.id}-${section.name}`">
63
+ <Teleport v-if="section.icon && headingTargets.includes(section.id)" :to="`#settings-section_${section.id}--label`">
64
+ <span class="cn-user-settings-dialog__heading-icon" aria-hidden="true" :data-testid="`cn-user-settings-heading-icon-${section.id}`">
65
+ <CnVNodes :vnodes="section.icon" />
66
+ </span>
67
+ </Teleport>
68
+ </template>
44
69
  </div>
45
70
  </div>
46
71
  <template #actions>
@@ -56,9 +81,11 @@
56
81
 
57
82
  <script>
58
83
  import { translate as t } from '@nextcloud/l10n'
84
+ import { generateUrl } from '@nextcloud/router'
59
85
  import { NcButton } from '@nextcloud/vue'
60
- import { markRaw } from 'vue'
86
+ import { cloneVNode, markRaw, ref } from 'vue'
61
87
  import CnDialog from '../CnDialog/CnDialog.vue'
88
+ import CnIcon from '../CnIcon/CnIcon.vue'
62
89
 
63
90
  /**
64
91
  * The key NcAppSettingsSection injects to register itself with its dialog
@@ -66,20 +93,26 @@ import CnDialog from '../CnDialog/CnDialog.vue'
66
93
  */
67
94
  const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration')
68
95
 
96
+ /** The key NcAppSettingsSection injects for the legacy design flag. */
97
+ const LEGACY_DESIGN_KEY = Symbol.for('NcAppSettingsDialog:legacy')
98
+
69
99
  /** Renders a section's icon vnodes as they were registered. */
70
100
  const CnVNodes = {
71
101
  name: 'CnVNodes',
72
102
  props: { vnodes: { type: [Array, Object], default: null } },
73
103
  render() {
74
- return this.vnodes
104
+ // A section's icon is drawn twice (navigation and heading), so each draw gets its own copy.
105
+ const list = Array.isArray(this.vnodes) ? this.vnodes : [this.vnodes]
106
+ return list.map((vnode) => (vnode && typeof vnode === 'object' ? cloneVNode(vnode) : vnode))
75
107
  },
76
108
  }
77
109
 
78
110
  /**
79
111
  * CnUserSettingsDialog - the user settings dialog in the board look (board
80
112
  * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
81
- * sections with their icons, the sections themselves, and a footer with a
82
- * note that changes save straight away and a Close button.
113
+ * sections with their icons, the current section (the others stay mounted
114
+ * but hidden), and a footer with a note that changes save straight away and
115
+ * a Close button.
83
116
  *
84
117
  * The sections stay the app's: any `NcAppSettingsSection` in the default slot
85
118
  * registers itself with this dialog exactly as it does with
@@ -93,7 +126,7 @@ const CnVNodes = {
93
126
  export default {
94
127
  name: 'CnUserSettingsDialog',
95
128
 
96
- components: { CnDialog, CnVNodes, NcButton },
129
+ components: { CnDialog, CnIcon, CnVNodes, NcButton },
97
130
 
98
131
  provide() {
99
132
  return {
@@ -101,6 +134,9 @@ export default {
101
134
  registerSection: this.registerSection,
102
135
  unregisterSection: this.unregisterSection,
103
136
  },
137
+
138
+ // NcAppSettingsSection also asks its dialog whether to use the legacy design.
139
+ [LEGACY_DESIGN_KEY]: ref(false),
104
140
  }
105
141
  },
106
142
 
@@ -113,8 +149,15 @@ export default {
113
149
  eyebrow: { type: String, default: () => t('nextcloud-vue', 'Your account') },
114
150
  /** Sentence under the title. */
115
151
  subtitle: { type: String, default: () => t('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
116
- /** The app's name, after the footer note. */
152
+ /** The app's name, after the footer note and as the navigation entry holding its sections. */
117
153
  appName: { type: String, default: '' },
154
+ /**
155
+ * Nextcloud's own personal settings, listed first in the navigation as
156
+ * links: `[{ label, section? | href?, icon? }]`. `section` is a
157
+ * personal settings section id (`/settings/user/<section>`). With any,
158
+ * the app's sections sit under one entry named after the app.
159
+ */
160
+ nextcloudLinks: { type: Array, default: () => [] },
118
161
  },
119
162
 
120
163
  emits: ['update:open'],
@@ -123,11 +166,30 @@ export default {
123
166
  return {
124
167
  sections: [],
125
168
  selected: '',
169
+ /** Ids of the sections whose heading is in the page, for the heading icons. */
170
+ headingTargets: [],
126
171
  picked: false,
127
172
  }
128
173
  },
129
174
 
130
175
  computed: {
176
+ /**
177
+ * The Nextcloud links with their URL.
178
+ *
179
+ * @spec openspec/changes/user-settings-nav-groups/specs/user-settings-board-dialog/spec.md#requirement-the-board-user-settings-navigation-can-list-nextclouds-own-sections-first
180
+ * @return {Array<{key: string, label: string, href: string, icon: string}>}
181
+ */
182
+ ncLinks() {
183
+ return (Array.isArray(this.nextcloudLinks) ? this.nextcloudLinks : [])
184
+ .filter((link) => link && typeof link === 'object' && typeof link.label === 'string' && link.label !== '' && (link.href || link.section))
185
+ .map((link, index) => ({
186
+ key: String(link.section || link.href || index),
187
+ label: link.label,
188
+ href: link.href || generateUrl('/settings/user/{section}', { section: String(link.section) }),
189
+ icon: typeof link.icon === 'string' ? link.icon : '',
190
+ }))
191
+ },
192
+
131
193
  /** @return {object} The board's eyebrow: muted, in sentence case. */
132
194
  eyebrowStyle() {
133
195
  return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
@@ -150,6 +212,21 @@ export default {
150
212
  },
151
213
  },
152
214
 
215
+ watch: {
216
+ sections() {
217
+ this.$nextTick(() => {
218
+ this.headingTargets = typeof document === 'undefined'
219
+ ? []
220
+ : this.sections.filter((section) => section.icon && document.getElementById(`settings-section_${section.id}--label`)).map((section) => section.id)
221
+ this.showSelected()
222
+ })
223
+ },
224
+
225
+ selected() {
226
+ this.$nextTick(() => this.showSelected())
227
+ },
228
+ },
229
+
153
230
  methods: {
154
231
  /**
155
232
  * Add a section to the navigation, kept in `order`; a section
@@ -190,9 +267,31 @@ export default {
190
267
  selectSection(id) {
191
268
  this.picked = true
192
269
  this.selected = id
193
- const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null
194
- if (target && typeof target.scrollIntoView === 'function') {
195
- target.scrollIntoView({ behavior: 'smooth', block: 'start' })
270
+ this.$nextTick(() => {
271
+ this.showSelected()
272
+ const heading = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}--label`) : null
273
+ if (heading) {
274
+ heading.setAttribute('tabindex', '-1')
275
+ heading.focus()
276
+ }
277
+ })
278
+ },
279
+
280
+ /**
281
+ * Show the current section and hide the others. They stay mounted,
282
+ * so what a reader typed in one survives a switch.
283
+ *
284
+ * @spec openspec/changes/user-settings-one-section/specs/user-settings-board-dialog/spec.md#requirement-the-board-user-settings-dialog-shows-one-section-at-a-time
285
+ */
286
+ showSelected() {
287
+ if (typeof document === 'undefined') {
288
+ return
289
+ }
290
+ for (const section of this.sections) {
291
+ const el = document.getElementById(`settings-section_${section.id}`)
292
+ if (el) {
293
+ el.hidden = section.id !== this.selected
294
+ }
196
295
  }
197
296
  },
198
297
 
@@ -294,6 +393,9 @@ export default {
294
393
  margin: 0 0 12px;
295
394
  font-size: 17px;
296
395
  font-weight: 700;
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 10px;
297
399
  }
298
400
 
299
401
  .cn-user-settings-dialog__note {
@@ -302,4 +404,31 @@ export default {
302
404
  font-size: 14px;
303
405
  color: var(--color-text-maxcontrast);
304
406
  }
407
+
408
+ .cn-user-settings-dialog__nc-links {
409
+ margin-bottom: 2px;
410
+ }
411
+
412
+ .cn-user-settings-dialog__group {
413
+ margin: 10px 0 2px;
414
+ padding: 0 12px;
415
+ font-size: 15px;
416
+ font-weight: 600;
417
+ color: var(--color-main-text);
418
+ }
419
+
420
+ .cn-user-settings-dialog__group-sections {
421
+ padding-inline-start: 12px;
422
+ }
423
+
424
+ .cn-user-settings-dialog__heading-icon {
425
+ display: inline-flex;
426
+ order: -1;
427
+ color: var(--color-text-maxcontrast);
428
+ }
429
+
430
+ /* One section at a time: the others are mounted but hidden. */
431
+ .cn-user-settings-dialog__sections :deep(.app-settings-section[hidden]) {
432
+ display: none;
433
+ }
305
434
  </style>
@@ -75,6 +75,8 @@ import '../CnTasksWidget/index.js'
75
75
  import '../CnSectionsWidget/index.js'
76
76
  import '../CnFactsWidget/index.js'
77
77
  import '../CnDeviceFramesWidget/index.js'
78
+ import '../CnChecklistRunWidget/index.js'
79
+ import '../CnWorkingCalendarWidget/index.js'
78
80
  import '../CnPartyActionsWidget/index.js'
79
81
  import '../CnFieldListWidget/index.js'
80
82
  import '../CnSettingsFormWidget/index.js'