@conduction/nextcloud-vue 2.97.0 → 2.98.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 (107) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
  9. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
  10. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
  11. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
  12. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
  13. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
  14. package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
  15. package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
  24. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
  28. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  31. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
  32. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +43 -3
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  38. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
  39. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
  40. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
  41. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
  42. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
  43. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
  44. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  45. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
  46. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  47. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
  48. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  49. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  50. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  51. package/dist/esm/composables/indexSources.js +238 -2
  52. package/dist/esm/composables/indexSources.js.map +1 -1
  53. package/dist/esm/composables/useOrganisationStore.js +115 -0
  54. package/dist/esm/composables/useOrganisationStore.js.map +1 -0
  55. package/dist/esm/index.js +2 -0
  56. package/dist/esm/index.js.map +1 -1
  57. package/dist/esm/l10n/en.json.js +15 -1
  58. package/dist/esm/l10n/en.json.js.map +1 -1
  59. package/dist/esm/l10n/nl.json.js +30 -1
  60. package/dist/esm/l10n/nl.json.js.map +1 -1
  61. package/dist/esm/library-style.js +1 -1
  62. package/dist/esm/nextcloud-vue.css +424 -206
  63. package/dist/esm/utils/builtInFormatters.js +34 -1
  64. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  65. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  66. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  67. package/dist/esm/utils/matrixCells.js +158 -0
  68. package/dist/esm/utils/matrixCells.js.map +1 -0
  69. package/dist/esm/utils/nextcloudDirectory.js +121 -0
  70. package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
  71. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  72. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  73. package/dist/nextcloud-vue.cjs.js +12538 -9716
  74. package/dist/nextcloud-vue.cjs.js.map +2 -2
  75. package/dist/nextcloud-vue.css +424 -206
  76. package/l10n/en.json +15 -1
  77. package/l10n/nl.json +30 -1
  78. package/package.json +2 -2
  79. package/scripts/merge-l10n.js +196 -0
  80. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  81. package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
  82. package/src/components/CnChartWidget/CnChartWidget.md +2 -1
  83. package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
  84. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
  85. package/src/components/CnChecklistRunWidget/index.js +27 -0
  86. package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
  87. package/src/components/CnDataTable/CnDataTable.vue +26 -8
  88. package/src/components/CnDetailPage/CnDetailPage.md +48 -0
  89. package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
  90. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
  91. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
  92. package/src/components/CnIndexPage/CnIndexPage.md +28 -0
  93. package/src/components/CnIndexPage/CnIndexPage.vue +43 -3
  94. package/src/components/CnOrderedListField/CnOrderedListField.vue +386 -0
  95. package/src/components/CnOrderedListField/index.js +7 -0
  96. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
  97. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  98. package/src/components/index.js +2 -0
  99. package/src/composables/indexSources.js +237 -1
  100. package/src/composables/useOrganisationStore.js +111 -0
  101. package/src/index.js +2 -0
  102. package/src/schemas/app-manifest-v2.schema.json +33 -3
  103. package/src/utils/builtInFormatters.js +33 -0
  104. package/src/utils/libraryWidgetKeys.js +1 -0
  105. package/src/utils/matrixCells.js +154 -0
  106. package/src/utils/nextcloudDirectory.js +117 -0
  107. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,9 @@
1
+ import script from './CnOrderedListField.vue2.js';
2
+ import { render } from './CnOrderedListField.vue3.js';
3
+
4
+ script.render = render;
5
+ script.__scopeId = "data-v-81cf425a";
6
+ const __sfc_main = script;
7
+
8
+ export { __sfc_main as default };
9
+ //# sourceMappingURL=CnOrderedListField.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnOrderedListField.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,220 @@
1
+ import { translate } from '@nextcloud/l10n';
2
+ import { NcSelect } from '@nextcloud/vue';
3
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue';
4
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue';
5
+ import Close from 'vue-material-design-icons/Close.vue';
6
+
7
+ let orderedListUid = 0;
8
+
9
+ /**
10
+ * CnOrderedListField - an ordered list a user keeps for themselves: rows in
11
+ * their order with a move up, a move down and a remove button each, and a
12
+ * search-as-you-type picker under it that adds a row (board
13
+ * DqPersoonlijkeInstellingen, "Zaaktypen in mijn menu").
14
+ *
15
+ * The app supplies the choices (`options`) and keeps the value: every change
16
+ * emits `update:modelValue` with the new list of values. With `searchable`
17
+ * the picker emits `search` as the user types, so the app can fetch matching
18
+ * choices itself; otherwise it filters `options`. Every move is announced in a
19
+ * polite live region, so reordering works with the keyboard and a screen
20
+ * reader, not by dragging.
21
+ *
22
+ * ```vue
23
+ * <CnOrderedListField
24
+ * v-model="menuCaseTypes"
25
+ * label="Case types in my menu"
26
+ * add-label="Add case type"
27
+ * :options="caseTypes" />
28
+ * ```
29
+ *
30
+ * @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard
31
+ */
32
+ var script = {
33
+ name: 'CnOrderedListField',
34
+
35
+ components: { ChevronDown, ChevronUp, Close, NcSelect },
36
+
37
+ props: {
38
+ /** The values in their order. */
39
+ modelValue: {
40
+ type: Array,
41
+ default: () => [],
42
+ },
43
+
44
+ /**
45
+ * The choices, `{ value, label }` (a plain string is its own label).
46
+ * A value in `modelValue` without a choice shows the value itself.
47
+ */
48
+ options: {
49
+ type: Array,
50
+ default: () => [],
51
+ },
52
+
53
+ /** The list's label, also its accessible name. */
54
+ label: {
55
+ type: String,
56
+ default: '',
57
+ },
58
+
59
+ /** The picker's label. */
60
+ addLabel: {
61
+ type: String,
62
+ default: '',
63
+ },
64
+
65
+ /** Placeholder in the picker. */
66
+ addPlaceholder: {
67
+ type: String,
68
+ default: '',
69
+ },
70
+
71
+ /** Shown when the list is empty. */
72
+ emptyText: {
73
+ type: String,
74
+ default: '',
75
+ },
76
+
77
+ /** Whether the app searches (`search` event) instead of the picker filtering `options`. */
78
+ searchable: {
79
+ type: Boolean,
80
+ default: false,
81
+ },
82
+
83
+ /** Whether the app is fetching choices. */
84
+ loading: {
85
+ type: Boolean,
86
+ default: false,
87
+ },
88
+
89
+ /** A read-only list: no buttons and no picker. */
90
+ disabled: {
91
+ type: Boolean,
92
+ default: false,
93
+ },
94
+ },
95
+
96
+ emits: ['update:modelValue', 'search'],
97
+
98
+ data() {
99
+ orderedListUid += 1;
100
+ return {
101
+ labelId: `cn-ordered-list-label-${orderedListUid}`,
102
+ announcement: '',
103
+ }
104
+ },
105
+
106
+ computed: {
107
+ /** @return {Array<{value: string, label: string}>} The choices in one shape. */
108
+ choices() {
109
+ return (Array.isArray(this.options) ? this.options : [])
110
+ .map((option) => (option && typeof option === 'object'
111
+ ? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }
112
+ : { value: String(option), label: String(option) }))
113
+ .filter((option) => option.value !== '')
114
+ },
115
+
116
+ /** @return {Array<{value: string, label: string}>} The rows, in order. */
117
+ rows() {
118
+ return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
119
+ const key = String(value);
120
+ const choice = this.choices.find((option) => option.value === key);
121
+ return { value: key, label: choice ? choice.label : key }
122
+ })
123
+ },
124
+
125
+ /** @return {Array<{value: string, label: string}>} The choices not in the list yet. */
126
+ available() {
127
+ const taken = new Set(this.rows.map((row) => row.value));
128
+ return this.choices.filter((option) => !taken.has(option.value))
129
+ },
130
+
131
+ /** @return {string} The list's accessible name without a label. */
132
+ listLabel() {
133
+ return translate('nextcloud-vue', 'Ordered list')
134
+ },
135
+ },
136
+
137
+ methods: {
138
+ t: translate,
139
+
140
+ /**
141
+ * Emit the new order.
142
+ *
143
+ * @param {Array<string>} values The values.
144
+ */
145
+ emitValue(values) {
146
+ /**
147
+ * @event update:modelValue The list changed. Payload: the values in their order.
148
+ */
149
+ this.$emit('update:modelValue', values);
150
+ },
151
+
152
+ /**
153
+ * Move a row to another place and keep focus on the pressed button
154
+ * where it still makes sense.
155
+ *
156
+ * @param {number} from Its index.
157
+ * @param {number} to The index it moves to.
158
+ */
159
+ move(from, to) {
160
+ if (to < 0 || to >= this.rows.length) {
161
+ return
162
+ }
163
+ const values = this.rows.map((row) => row.value);
164
+ const [value] = values.splice(from, 1);
165
+ values.splice(to, 0, value);
166
+ const row = this.rows[from];
167
+ this.emitValue(values);
168
+ this.announcement = translate('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length });
169
+ this.$nextTick(() => {
170
+ const direction = to < from ? 'up' : 'down';
171
+ const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1);
172
+ const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`);
173
+ if (target && !target.disabled) {
174
+ target.focus();
175
+ }
176
+ });
177
+ },
178
+
179
+ /**
180
+ * Remove a row.
181
+ *
182
+ * @param {number} index Its index.
183
+ */
184
+ remove(index) {
185
+ const row = this.rows[index];
186
+ this.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value));
187
+ this.announcement = translate('nextcloud-vue', '{item} removed', { item: row.label });
188
+ },
189
+
190
+ /**
191
+ * Add the picked choice at the end.
192
+ *
193
+ * @param {{value: string, label: string}|null} option The choice.
194
+ */
195
+ add(option) {
196
+ if (!option) {
197
+ return
198
+ }
199
+ this.emitValue([...this.rows.map((row) => row.value), option.value]);
200
+ this.announcement = translate('nextcloud-vue', '{item} added', { item: option.label });
201
+ },
202
+
203
+ /**
204
+ * Pass the typed text on when the app searches.
205
+ *
206
+ * @param {string} query The text.
207
+ */
208
+ onSearch(query) {
209
+ if (this.searchable) {
210
+ /**
211
+ * @event search The user typed in the picker (with `searchable`). Payload: the text.
212
+ */
213
+ this.$emit('search', query);
214
+ }
215
+ },
216
+ },
217
+ };
218
+
219
+ export { script as default };
220
+ //# sourceMappingURL=CnOrderedListField.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnOrderedListField.vue2.js","sources":["../../../../src/components/CnOrderedListField/CnOrderedListField.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-ordered-list-field\" data-testid=\"cn-ordered-list-field\">\n\t\t<p v-if=\"label\" :id=\"labelId\" class=\"cn-ordered-list-field__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\t\t<ol\n\t\t\tv-if=\"rows.length > 0\"\n\t\t\tclass=\"cn-ordered-list-field__rows\"\n\t\t\t:aria-labelledby=\"label ? labelId : null\"\n\t\t\t:aria-label=\"label ? null : listLabel\">\n\t\t\t<li\n\t\t\t\tv-for=\"(row, index) in rows\"\n\t\t\t\t:key=\"row.value\"\n\t\t\t\tclass=\"cn-ordered-list-field__row\"\n\t\t\t\t:data-testid=\"`cn-ordered-list-row-${row.value}`\">\n\t\t\t\t<span class=\"cn-ordered-list-field__position\" aria-hidden=\"true\">{{ index + 1 }}</span>\n\t\t\t\t<span class=\"cn-ordered-list-field__text\">{{ row.label }}</span>\n\t\t\t\t<span class=\"cn-ordered-list-field__actions\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled || index === 0\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Move {item} up', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-up-${row.value}`\"\n\t\t\t\t\t\t@click=\"move(index, index - 1)\">\n\t\t\t\t\t\t<ChevronUp :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled || index === rows.length - 1\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Move {item} down', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-down-${row.value}`\"\n\t\t\t\t\t\t@click=\"move(index, index + 1)\">\n\t\t\t\t\t\t<ChevronDown :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Remove {item}', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-remove-${row.value}`\"\n\t\t\t\t\t\t@click=\"remove(index)\">\n\t\t\t\t\t\t<Close :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t</span>\n\t\t\t</li>\n\t\t</ol>\n\t\t<p v-else class=\"cn-ordered-list-field__empty\" data-testid=\"cn-ordered-list-empty\">\n\t\t\t{{ emptyText || t('nextcloud-vue', 'Nothing added yet.') }}\n\t\t</p>\n\t\t<div v-if=\"!disabled\" class=\"cn-ordered-list-field__add\">\n\t\t\t<NcSelect\n\t\t\t\t:modelValue=\"null\"\n\t\t\t\t:options=\"available\"\n\t\t\t\tlabel=\"label\"\n\t\t\t\t:inputLabel=\"addLabel || t('nextcloud-vue', 'Add')\"\n\t\t\t\t:placeholder=\"addPlaceholder\"\n\t\t\t\t:loading=\"loading\"\n\t\t\t\t:clearable=\"false\"\n\t\t\t\t:filterable=\"!searchable\"\n\t\t\t\tdata-testid=\"cn-ordered-list-add\"\n\t\t\t\t@search=\"onSearch\"\n\t\t\t\t@update:modelValue=\"add\" />\n\t\t</div>\n\t\t<p class=\"hidden-visually\" aria-live=\"polite\" data-testid=\"cn-ordered-list-live\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ChevronUp from 'vue-material-design-icons/ChevronUp.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\n\nlet orderedListUid = 0\n\n/**\n * CnOrderedListField - an ordered list a user keeps for themselves: rows in\n * their order with a move up, a move down and a remove button each, and a\n * search-as-you-type picker under it that adds a row (board\n * DqPersoonlijkeInstellingen, \"Zaaktypen in mijn menu\").\n *\n * The app supplies the choices (`options`) and keeps the value: every change\n * emits `update:modelValue` with the new list of values. With `searchable`\n * the picker emits `search` as the user types, so the app can fetch matching\n * choices itself; otherwise it filters `options`. Every move is announced in a\n * polite live region, so reordering works with the keyboard and a screen\n * reader, not by dragging.\n *\n * ```vue\n * <CnOrderedListField\n * v-model=\"menuCaseTypes\"\n * label=\"Case types in my menu\"\n * add-label=\"Add case type\"\n * :options=\"caseTypes\" />\n * ```\n *\n * @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard\n */\nexport default {\n\tname: 'CnOrderedListField',\n\n\tcomponents: { ChevronDown, ChevronUp, Close, NcSelect },\n\n\tprops: {\n\t\t/** The values in their order. */\n\t\tmodelValue: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The choices, `{ value, label }` (a plain string is its own label).\n\t\t * A value in `modelValue` without a choice shows the value itself.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The list's label, also its accessible name. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The picker's label. */\n\t\taddLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Placeholder in the picker. */\n\t\taddPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Shown when the list is empty. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the app searches (`search` event) instead of the picker filtering `options`. */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the app is fetching choices. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A read-only list: no buttons and no picker. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'search'],\n\n\tdata() {\n\t\torderedListUid += 1\n\t\treturn {\n\t\t\tlabelId: `cn-ordered-list-label-${orderedListUid}`,\n\t\t\tannouncement: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {Array<{value: string, label: string}>} The choices in one shape. */\n\t\tchoices() {\n\t\t\treturn (Array.isArray(this.options) ? this.options : [])\n\t\t\t\t.map((option) => (option && typeof option === 'object'\n\t\t\t\t\t? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }\n\t\t\t\t\t: { value: String(option), label: String(option) }))\n\t\t\t\t.filter((option) => option.value !== '')\n\t\t},\n\n\t\t/** @return {Array<{value: string, label: string}>} The rows, in order. */\n\t\trows() {\n\t\t\treturn (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {\n\t\t\t\tconst key = String(value)\n\t\t\t\tconst choice = this.choices.find((option) => option.value === key)\n\t\t\t\treturn { value: key, label: choice ? choice.label : key }\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<{value: string, label: string}>} The choices not in the list yet. */\n\t\tavailable() {\n\t\t\tconst taken = new Set(this.rows.map((row) => row.value))\n\t\t\treturn this.choices.filter((option) => !taken.has(option.value))\n\t\t},\n\n\t\t/** @return {string} The list's accessible name without a label. */\n\t\tlistLabel() {\n\t\t\treturn t('nextcloud-vue', 'Ordered list')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Emit the new order.\n\t\t *\n\t\t * @param {Array<string>} values The values.\n\t\t */\n\t\temitValue(values) {\n\t\t\t/**\n\t\t\t * @event update:modelValue The list changed. Payload: the values in their order.\n\t\t\t */\n\t\t\tthis.$emit('update:modelValue', values)\n\t\t},\n\n\t\t/**\n\t\t * Move a row to another place and keep focus on the pressed button\n\t\t * where it still makes sense.\n\t\t *\n\t\t * @param {number} from Its index.\n\t\t * @param {number} to The index it moves to.\n\t\t */\n\t\tmove(from, to) {\n\t\t\tif (to < 0 || to >= this.rows.length) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst values = this.rows.map((row) => row.value)\n\t\t\tconst [value] = values.splice(from, 1)\n\t\t\tvalues.splice(to, 0, value)\n\t\t\tconst row = this.rows[from]\n\t\t\tthis.emitValue(values)\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst direction = to < from ? 'up' : 'down'\n\t\t\t\tconst atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)\n\t\t\t\tconst target = this.$el.querySelector(`[data-testid=\"cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}\"]`)\n\t\t\t\tif (target && !target.disabled) {\n\t\t\t\t\ttarget.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a row.\n\t\t *\n\t\t * @param {number} index Its index.\n\t\t */\n\t\tremove(index) {\n\t\t\tconst row = this.rows[index]\n\t\t\tthis.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value))\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} removed', { item: row.label })\n\t\t},\n\n\t\t/**\n\t\t * Add the picked choice at the end.\n\t\t *\n\t\t * @param {{value: string, label: string}|null} option The choice.\n\t\t */\n\t\tadd(option) {\n\t\t\tif (!option) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.emitValue([...this.rows.map((row) => row.value), option.value])\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} added', { item: option.label })\n\t\t},\n\n\t\t/**\n\t\t * Pass the typed text on when the app searches.\n\t\t *\n\t\t * @param {string} query The text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tif (this.searchable) {\n\t\t\t\t/**\n\t\t\t\t * @event search The user typed in the picker (with `searchable`). Payload: the text.\n\t\t\t\t */\n\t\t\t\tthis.$emit('search', query)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-ordered-list-field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n}\n\n.cn-ordered-list-field__label {\n\tmargin: 0;\n\tfont-weight: 600;\n}\n\n.cn-ordered-list-field__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-element, 8px);\n\tlist-style: none;\n}\n\n.cn-ordered-list-field__row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 44px;\n\tpadding: 2px 6px 2px 12px;\n}\n\n.cn-ordered-list-field__row + .cn-ordered-list-field__row {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-ordered-list-field__position {\n\tflex: none;\n\tmin-width: 18px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.cn-ordered-list-field__text {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-ordered-list-field__actions {\n\tdisplay: flex;\n\tflex: none;\n\tgap: 2px;\n}\n\n.cn-ordered-list-field__button {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 40px;\n\tmin-width: 40px;\n\theight: 40px;\n\tmin-height: 40px;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: var(--border-radius-element, 8px);\n\tbackground: transparent;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ordered-list-field__button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-ordered-list-field__button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 1px;\n}\n\n.cn-ordered-list-field__button:disabled {\n\topacity: 0.4;\n\tcursor: default;\n}\n\n.cn-ordered-list-field__empty {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -0,0 +1,121 @@
1
+ import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, Fragment, renderList, createElementVNode, createVNode } from 'vue';
2
+
3
+ const _hoisted_1 = {
4
+ class: "cn-ordered-list-field",
5
+ "data-testid": "cn-ordered-list-field"
6
+ };
7
+ const _hoisted_2 = ["id"];
8
+ const _hoisted_3 = ["aria-labelledby", "aria-label"];
9
+ const _hoisted_4 = ["data-testid"];
10
+ const _hoisted_5 = {
11
+ class: "cn-ordered-list-field__position",
12
+ "aria-hidden": "true"
13
+ };
14
+ const _hoisted_6 = { class: "cn-ordered-list-field__text" };
15
+ const _hoisted_7 = { class: "cn-ordered-list-field__actions" };
16
+ const _hoisted_8 = ["disabled", "aria-label", "data-testid", "onClick"];
17
+ const _hoisted_9 = ["disabled", "aria-label", "data-testid", "onClick"];
18
+ const _hoisted_10 = ["disabled", "aria-label", "data-testid", "onClick"];
19
+ const _hoisted_11 = {
20
+ key: 2,
21
+ class: "cn-ordered-list-field__empty",
22
+ "data-testid": "cn-ordered-list-empty"
23
+ };
24
+ const _hoisted_12 = {
25
+ key: 3,
26
+ class: "cn-ordered-list-field__add"
27
+ };
28
+ const _hoisted_13 = {
29
+ class: "hidden-visually",
30
+ "aria-live": "polite",
31
+ "data-testid": "cn-ordered-list-live"
32
+ };
33
+
34
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
35
+ const _component_ChevronUp = resolveComponent("ChevronUp");
36
+ const _component_ChevronDown = resolveComponent("ChevronDown");
37
+ const _component_Close = resolveComponent("Close");
38
+ const _component_NcSelect = resolveComponent("NcSelect");
39
+
40
+ return (openBlock(), createElementBlock("div", _hoisted_1, [
41
+ ($props.label)
42
+ ? (openBlock(), createElementBlock("p", {
43
+ key: 0,
44
+ id: $data.labelId,
45
+ class: "cn-ordered-list-field__label"
46
+ }, toDisplayString($props.label), 9 /* TEXT, PROPS */, _hoisted_2))
47
+ : createCommentVNode("v-if", true),
48
+ ($options.rows.length > 0)
49
+ ? (openBlock(), createElementBlock("ol", {
50
+ key: 1,
51
+ class: "cn-ordered-list-field__rows",
52
+ "aria-labelledby": $props.label ? $data.labelId : null,
53
+ "aria-label": $props.label ? null : $options.listLabel
54
+ }, [
55
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.rows, (row, index) => {
56
+ return (openBlock(), createElementBlock("li", {
57
+ key: row.value,
58
+ class: "cn-ordered-list-field__row",
59
+ "data-testid": `cn-ordered-list-row-${row.value}`
60
+ }, [
61
+ createElementVNode("span", _hoisted_5, toDisplayString(index + 1), 1 /* TEXT */),
62
+ createElementVNode("span", _hoisted_6, toDisplayString(row.label), 1 /* TEXT */),
63
+ createElementVNode("span", _hoisted_7, [
64
+ createElementVNode("button", {
65
+ type: "button",
66
+ class: "cn-ordered-list-field__button",
67
+ disabled: $props.disabled || index === 0,
68
+ "aria-label": $options.t('nextcloud-vue', 'Move {item} up', { item: row.label }),
69
+ "data-testid": `cn-ordered-list-up-${row.value}`,
70
+ onClick: $event => ($options.move(index, index - 1))
71
+ }, [
72
+ createVNode(_component_ChevronUp, { size: 20 })
73
+ ], 8 /* PROPS */, _hoisted_8),
74
+ createElementVNode("button", {
75
+ type: "button",
76
+ class: "cn-ordered-list-field__button",
77
+ disabled: $props.disabled || index === $options.rows.length - 1,
78
+ "aria-label": $options.t('nextcloud-vue', 'Move {item} down', { item: row.label }),
79
+ "data-testid": `cn-ordered-list-down-${row.value}`,
80
+ onClick: $event => ($options.move(index, index + 1))
81
+ }, [
82
+ createVNode(_component_ChevronDown, { size: 20 })
83
+ ], 8 /* PROPS */, _hoisted_9),
84
+ createElementVNode("button", {
85
+ type: "button",
86
+ class: "cn-ordered-list-field__button",
87
+ disabled: $props.disabled,
88
+ "aria-label": $options.t('nextcloud-vue', 'Remove {item}', { item: row.label }),
89
+ "data-testid": `cn-ordered-list-remove-${row.value}`,
90
+ onClick: $event => ($options.remove(index))
91
+ }, [
92
+ createVNode(_component_Close, { size: 20 })
93
+ ], 8 /* PROPS */, _hoisted_10)
94
+ ])
95
+ ], 8 /* PROPS */, _hoisted_4))
96
+ }), 128 /* KEYED_FRAGMENT */))
97
+ ], 8 /* PROPS */, _hoisted_3))
98
+ : (openBlock(), createElementBlock("p", _hoisted_11, toDisplayString($props.emptyText || $options.t('nextcloud-vue', 'Nothing added yet.')), 1 /* TEXT */)),
99
+ (!$props.disabled)
100
+ ? (openBlock(), createElementBlock("div", _hoisted_12, [
101
+ createVNode(_component_NcSelect, {
102
+ modelValue: null,
103
+ options: $options.available,
104
+ label: "label",
105
+ inputLabel: $props.addLabel || $options.t('nextcloud-vue', 'Add'),
106
+ placeholder: $props.addPlaceholder,
107
+ loading: $props.loading,
108
+ clearable: false,
109
+ filterable: !$props.searchable,
110
+ "data-testid": "cn-ordered-list-add",
111
+ onSearch: $options.onSearch,
112
+ "onUpdate:modelValue": $options.add
113
+ }, null, 8 /* PROPS */, ["options", "inputLabel", "placeholder", "loading", "filterable", "onSearch", "onUpdate:modelValue"])
114
+ ]))
115
+ : createCommentVNode("v-if", true),
116
+ createElementVNode("p", _hoisted_13, toDisplayString($data.announcement), 1 /* TEXT */)
117
+ ]))
118
+ }
119
+
120
+ export { render };
121
+ //# sourceMappingURL=CnOrderedListField.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnOrderedListField.vue3.js","sources":["../../../../src/components/CnOrderedListField/CnOrderedListField.vue?vue&type=template&id=81cf425a&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-ordered-list-field\" data-testid=\"cn-ordered-list-field\">\n\t\t<p v-if=\"label\" :id=\"labelId\" class=\"cn-ordered-list-field__label\">\n\t\t\t{{ label }}\n\t\t</p>\n\t\t<ol\n\t\t\tv-if=\"rows.length > 0\"\n\t\t\tclass=\"cn-ordered-list-field__rows\"\n\t\t\t:aria-labelledby=\"label ? labelId : null\"\n\t\t\t:aria-label=\"label ? null : listLabel\">\n\t\t\t<li\n\t\t\t\tv-for=\"(row, index) in rows\"\n\t\t\t\t:key=\"row.value\"\n\t\t\t\tclass=\"cn-ordered-list-field__row\"\n\t\t\t\t:data-testid=\"`cn-ordered-list-row-${row.value}`\">\n\t\t\t\t<span class=\"cn-ordered-list-field__position\" aria-hidden=\"true\">{{ index + 1 }}</span>\n\t\t\t\t<span class=\"cn-ordered-list-field__text\">{{ row.label }}</span>\n\t\t\t\t<span class=\"cn-ordered-list-field__actions\">\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled || index === 0\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Move {item} up', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-up-${row.value}`\"\n\t\t\t\t\t\t@click=\"move(index, index - 1)\">\n\t\t\t\t\t\t<ChevronUp :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled || index === rows.length - 1\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Move {item} down', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-down-${row.value}`\"\n\t\t\t\t\t\t@click=\"move(index, index + 1)\">\n\t\t\t\t\t\t<ChevronDown :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t\t<button\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tclass=\"cn-ordered-list-field__button\"\n\t\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Remove {item}', { item: row.label })\"\n\t\t\t\t\t\t:data-testid=\"`cn-ordered-list-remove-${row.value}`\"\n\t\t\t\t\t\t@click=\"remove(index)\">\n\t\t\t\t\t\t<Close :size=\"20\" />\n\t\t\t\t\t</button>\n\t\t\t\t</span>\n\t\t\t</li>\n\t\t</ol>\n\t\t<p v-else class=\"cn-ordered-list-field__empty\" data-testid=\"cn-ordered-list-empty\">\n\t\t\t{{ emptyText || t('nextcloud-vue', 'Nothing added yet.') }}\n\t\t</p>\n\t\t<div v-if=\"!disabled\" class=\"cn-ordered-list-field__add\">\n\t\t\t<NcSelect\n\t\t\t\t:modelValue=\"null\"\n\t\t\t\t:options=\"available\"\n\t\t\t\tlabel=\"label\"\n\t\t\t\t:inputLabel=\"addLabel || t('nextcloud-vue', 'Add')\"\n\t\t\t\t:placeholder=\"addPlaceholder\"\n\t\t\t\t:loading=\"loading\"\n\t\t\t\t:clearable=\"false\"\n\t\t\t\t:filterable=\"!searchable\"\n\t\t\t\tdata-testid=\"cn-ordered-list-add\"\n\t\t\t\t@search=\"onSearch\"\n\t\t\t\t@update:modelValue=\"add\" />\n\t\t</div>\n\t\t<p class=\"hidden-visually\" aria-live=\"polite\" data-testid=\"cn-ordered-list-live\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcSelect } from '@nextcloud/vue'\nimport ChevronDown from 'vue-material-design-icons/ChevronDown.vue'\nimport ChevronUp from 'vue-material-design-icons/ChevronUp.vue'\nimport Close from 'vue-material-design-icons/Close.vue'\n\nlet orderedListUid = 0\n\n/**\n * CnOrderedListField - an ordered list a user keeps for themselves: rows in\n * their order with a move up, a move down and a remove button each, and a\n * search-as-you-type picker under it that adds a row (board\n * DqPersoonlijkeInstellingen, \"Zaaktypen in mijn menu\").\n *\n * The app supplies the choices (`options`) and keeps the value: every change\n * emits `update:modelValue` with the new list of values. With `searchable`\n * the picker emits `search` as the user types, so the app can fetch matching\n * choices itself; otherwise it filters `options`. Every move is announced in a\n * polite live region, so reordering works with the keyboard and a screen\n * reader, not by dragging.\n *\n * ```vue\n * <CnOrderedListField\n * v-model=\"menuCaseTypes\"\n * label=\"Case types in my menu\"\n * add-label=\"Add case type\"\n * :options=\"caseTypes\" />\n * ```\n *\n * @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard\n */\nexport default {\n\tname: 'CnOrderedListField',\n\n\tcomponents: { ChevronDown, ChevronUp, Close, NcSelect },\n\n\tprops: {\n\t\t/** The values in their order. */\n\t\tmodelValue: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The choices, `{ value, label }` (a plain string is its own label).\n\t\t * A value in `modelValue` without a choice shows the value itself.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The list's label, also its accessible name. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The picker's label. */\n\t\taddLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Placeholder in the picker. */\n\t\taddPlaceholder: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Shown when the list is empty. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the app searches (`search` event) instead of the picker filtering `options`. */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the app is fetching choices. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A read-only list: no buttons and no picker. */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['update:modelValue', 'search'],\n\n\tdata() {\n\t\torderedListUid += 1\n\t\treturn {\n\t\t\tlabelId: `cn-ordered-list-label-${orderedListUid}`,\n\t\t\tannouncement: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {Array<{value: string, label: string}>} The choices in one shape. */\n\t\tchoices() {\n\t\t\treturn (Array.isArray(this.options) ? this.options : [])\n\t\t\t\t.map((option) => (option && typeof option === 'object'\n\t\t\t\t\t? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }\n\t\t\t\t\t: { value: String(option), label: String(option) }))\n\t\t\t\t.filter((option) => option.value !== '')\n\t\t},\n\n\t\t/** @return {Array<{value: string, label: string}>} The rows, in order. */\n\t\trows() {\n\t\t\treturn (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {\n\t\t\t\tconst key = String(value)\n\t\t\t\tconst choice = this.choices.find((option) => option.value === key)\n\t\t\t\treturn { value: key, label: choice ? choice.label : key }\n\t\t\t})\n\t\t},\n\n\t\t/** @return {Array<{value: string, label: string}>} The choices not in the list yet. */\n\t\tavailable() {\n\t\t\tconst taken = new Set(this.rows.map((row) => row.value))\n\t\t\treturn this.choices.filter((option) => !taken.has(option.value))\n\t\t},\n\n\t\t/** @return {string} The list's accessible name without a label. */\n\t\tlistLabel() {\n\t\t\treturn t('nextcloud-vue', 'Ordered list')\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Emit the new order.\n\t\t *\n\t\t * @param {Array<string>} values The values.\n\t\t */\n\t\temitValue(values) {\n\t\t\t/**\n\t\t\t * @event update:modelValue The list changed. Payload: the values in their order.\n\t\t\t */\n\t\t\tthis.$emit('update:modelValue', values)\n\t\t},\n\n\t\t/**\n\t\t * Move a row to another place and keep focus on the pressed button\n\t\t * where it still makes sense.\n\t\t *\n\t\t * @param {number} from Its index.\n\t\t * @param {number} to The index it moves to.\n\t\t */\n\t\tmove(from, to) {\n\t\t\tif (to < 0 || to >= this.rows.length) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst values = this.rows.map((row) => row.value)\n\t\t\tconst [value] = values.splice(from, 1)\n\t\t\tvalues.splice(to, 0, value)\n\t\t\tconst row = this.rows[from]\n\t\t\tthis.emitValue(values)\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst direction = to < from ? 'up' : 'down'\n\t\t\t\tconst atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)\n\t\t\t\tconst target = this.$el.querySelector(`[data-testid=\"cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}\"]`)\n\t\t\t\tif (target && !target.disabled) {\n\t\t\t\t\ttarget.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Remove a row.\n\t\t *\n\t\t * @param {number} index Its index.\n\t\t */\n\t\tremove(index) {\n\t\t\tconst row = this.rows[index]\n\t\t\tthis.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value))\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} removed', { item: row.label })\n\t\t},\n\n\t\t/**\n\t\t * Add the picked choice at the end.\n\t\t *\n\t\t * @param {{value: string, label: string}|null} option The choice.\n\t\t */\n\t\tadd(option) {\n\t\t\tif (!option) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.emitValue([...this.rows.map((row) => row.value), option.value])\n\t\t\tthis.announcement = t('nextcloud-vue', '{item} added', { item: option.label })\n\t\t},\n\n\t\t/**\n\t\t * Pass the typed text on when the app searches.\n\t\t *\n\t\t * @param {string} query The text.\n\t\t */\n\t\tonSearch(query) {\n\t\t\tif (this.searchable) {\n\t\t\t\t/**\n\t\t\t\t * @event search The user typed in the picker (with `searchable`). Payload: the text.\n\t\t\t\t */\n\t\t\t\tthis.$emit('search', query)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-ordered-list-field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 10px;\n}\n\n.cn-ordered-list-field__label {\n\tmargin: 0;\n\tfont-weight: 600;\n}\n\n.cn-ordered-list-field__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-element, 8px);\n\tlist-style: none;\n}\n\n.cn-ordered-list-field__row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 10px;\n\tmin-height: 44px;\n\tpadding: 2px 6px 2px 12px;\n}\n\n.cn-ordered-list-field__row + .cn-ordered-list-field__row {\n\tborder-top: 1px solid var(--color-border);\n}\n\n.cn-ordered-list-field__position {\n\tflex: none;\n\tmin-width: 18px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n\tfont-variant-numeric: tabular-nums;\n}\n\n.cn-ordered-list-field__text {\n\tflex: 1 1 auto;\n\tmin-width: 0;\n\toverflow-wrap: anywhere;\n}\n\n.cn-ordered-list-field__actions {\n\tdisplay: flex;\n\tflex: none;\n\tgap: 2px;\n}\n\n.cn-ordered-list-field__button {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 40px;\n\tmin-width: 40px;\n\theight: 40px;\n\tmin-height: 40px;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: var(--border-radius-element, 8px);\n\tbackground: transparent;\n\tcolor: var(--color-main-text);\n\tcursor: pointer;\n}\n\n.cn-ordered-list-field__button:hover:not(:disabled) {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-ordered-list-field__button:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 1px;\n}\n\n.cn-ordered-list-field__button:disabled {\n\topacity: 0.4;\n\tcursor: default;\n}\n\n.cn-ordered-list-field__empty {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_Fragment","_renderList","_createElementVNode","_toDisplayString","_createVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnUserSettingsDialog.vue2.js';
2
2
  import { render } from './CnUserSettingsDialog.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-8ebfd3c8";
5
+ script.__scopeId = "data-v-e537e50a";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1,7 +1,9 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
+ import { generateUrl } from '@nextcloud/router';
2
3
  import { NcButton } from '@nextcloud/vue';
3
- import { markRaw } from 'vue';
4
+ import { ref, markRaw, cloneVNode } from 'vue';
4
5
  import __sfc_main from '../CnDialog/CnDialog.vue.js';
6
+ import __sfc_main$1 from '../CnIcon/CnIcon.vue.js';
5
7
 
6
8
  /**
7
9
  * The key NcAppSettingsSection injects to register itself with its dialog
@@ -9,20 +11,26 @@ import __sfc_main from '../CnDialog/CnDialog.vue.js';
9
11
  */
10
12
  const REGISTRATION_KEY = Symbol.for('NcAppSettingsDialog:registration');
11
13
 
14
+ /** The key NcAppSettingsSection injects for the legacy design flag. */
15
+ const LEGACY_DESIGN_KEY = Symbol.for('NcAppSettingsDialog:legacy');
16
+
12
17
  /** Renders a section's icon vnodes as they were registered. */
13
18
  const CnVNodes = {
14
19
  name: 'CnVNodes',
15
20
  props: { vnodes: { type: [Array, Object], default: null } },
16
21
  render() {
17
- return this.vnodes
22
+ // A section's icon is drawn twice (navigation and heading), so each draw gets its own copy.
23
+ const list = Array.isArray(this.vnodes) ? this.vnodes : [this.vnodes];
24
+ return list.map((vnode) => (vnode && typeof vnode === 'object' ? cloneVNode(vnode) : vnode))
18
25
  },
19
26
  };
20
27
 
21
28
  /**
22
29
  * CnUserSettingsDialog - the user settings dialog in the board look (board
23
30
  * PqPersoonlijk): eyebrow, title and subtitle, a left navigation listing the
24
- * sections with their icons, the sections themselves, and a footer with a
25
- * note that changes save straight away and a Close button.
31
+ * sections with their icons, the current section (the others stay mounted
32
+ * but hidden), and a footer with a note that changes save straight away and
33
+ * a Close button.
26
34
  *
27
35
  * The sections stay the app's: any `NcAppSettingsSection` in the default slot
28
36
  * registers itself with this dialog exactly as it does with
@@ -36,7 +44,7 @@ const CnVNodes = {
36
44
  var script = {
37
45
  name: 'CnUserSettingsDialog',
38
46
 
39
- components: { CnDialog: __sfc_main, CnVNodes, NcButton },
47
+ components: { CnDialog: __sfc_main, CnIcon: __sfc_main$1, CnVNodes, NcButton },
40
48
 
41
49
  provide() {
42
50
  return {
@@ -44,6 +52,9 @@ var script = {
44
52
  registerSection: this.registerSection,
45
53
  unregisterSection: this.unregisterSection,
46
54
  },
55
+
56
+ // NcAppSettingsSection also asks its dialog whether to use the legacy design.
57
+ [LEGACY_DESIGN_KEY]: ref(false),
47
58
  }
48
59
  },
49
60
 
@@ -56,8 +67,15 @@ var script = {
56
67
  eyebrow: { type: String, default: () => translate('nextcloud-vue', 'Your account') },
57
68
  /** Sentence under the title. */
58
69
  subtitle: { type: String, default: () => translate('nextcloud-vue', 'Only for you. Colleagues keep their own settings.') },
59
- /** The app's name, after the footer note. */
70
+ /** The app's name, after the footer note and as the navigation entry holding its sections. */
60
71
  appName: { type: String, default: '' },
72
+ /**
73
+ * Nextcloud's own personal settings, listed first in the navigation as
74
+ * links: `[{ label, section? | href?, icon? }]`. `section` is a
75
+ * personal settings section id (`/settings/user/<section>`). With any,
76
+ * the app's sections sit under one entry named after the app.
77
+ */
78
+ nextcloudLinks: { type: Array, default: () => [] },
61
79
  },
62
80
 
63
81
  emits: ['update:open'],
@@ -66,11 +84,30 @@ var script = {
66
84
  return {
67
85
  sections: [],
68
86
  selected: '',
87
+ /** Ids of the sections whose heading is in the page, for the heading icons. */
88
+ headingTargets: [],
69
89
  picked: false,
70
90
  }
71
91
  },
72
92
 
73
93
  computed: {
94
+ /**
95
+ * The Nextcloud links with their URL.
96
+ *
97
+ * @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
98
+ * @return {Array<{key: string, label: string, href: string, icon: string}>}
99
+ */
100
+ ncLinks() {
101
+ return (Array.isArray(this.nextcloudLinks) ? this.nextcloudLinks : [])
102
+ .filter((link) => link && typeof link === 'object' && typeof link.label === 'string' && link.label !== '' && (link.href || link.section))
103
+ .map((link, index) => ({
104
+ key: String(link.section || link.href || index),
105
+ label: link.label,
106
+ href: link.href || generateUrl('/settings/user/{section}', { section: String(link.section) }),
107
+ icon: typeof link.icon === 'string' ? link.icon : '',
108
+ }))
109
+ },
110
+
74
111
  /** @return {object} The board's eyebrow: muted, in sentence case. */
75
112
  eyebrowStyle() {
76
113
  return { '--cn-dialog-eyebrow-transform': 'none', '--cn-dialog-eyebrow-color': 'var(--color-text-maxcontrast)' }
@@ -93,6 +130,21 @@ var script = {
93
130
  },
94
131
  },
95
132
 
133
+ watch: {
134
+ sections() {
135
+ this.$nextTick(() => {
136
+ this.headingTargets = typeof document === 'undefined'
137
+ ? []
138
+ : this.sections.filter((section) => section.icon && document.getElementById(`settings-section_${section.id}--label`)).map((section) => section.id);
139
+ this.showSelected();
140
+ });
141
+ },
142
+
143
+ selected() {
144
+ this.$nextTick(() => this.showSelected());
145
+ },
146
+ },
147
+
96
148
  methods: {
97
149
  /**
98
150
  * Add a section to the navigation, kept in `order`; a section
@@ -133,9 +185,31 @@ var script = {
133
185
  selectSection(id) {
134
186
  this.picked = true;
135
187
  this.selected = id;
136
- const target = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}`) : null;
137
- if (target && typeof target.scrollIntoView === 'function') {
138
- target.scrollIntoView({ behavior: 'smooth', block: 'start' });
188
+ this.$nextTick(() => {
189
+ this.showSelected();
190
+ const heading = typeof document !== 'undefined' ? document.getElementById(`settings-section_${id}--label`) : null;
191
+ if (heading) {
192
+ heading.setAttribute('tabindex', '-1');
193
+ heading.focus();
194
+ }
195
+ });
196
+ },
197
+
198
+ /**
199
+ * Show the current section and hide the others. They stay mounted,
200
+ * so what a reader typed in one survives a switch.
201
+ *
202
+ * @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
203
+ */
204
+ showSelected() {
205
+ if (typeof document === 'undefined') {
206
+ return
207
+ }
208
+ for (const section of this.sections) {
209
+ const el = document.getElementById(`settings-section_${section.id}`);
210
+ if (el) {
211
+ el.hidden = section.id !== this.selected;
212
+ }
139
213
  }
140
214
  },
141
215