@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
@@ -1224,6 +1224,7 @@ import { isModifiedClick, openRowTarget } from '../../utils/linkNavigation.js'
1224
1224
  import { resolveClaimedTeams, resolveClaimTokens, splitViewsIntoTabs, viewAsTab, viewIdOf } from '../../utils/listLenses.js'
1225
1225
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js'
1226
1226
  import { multiKeySort } from '../../utils/multiKeySort.js'
1227
+ import { fetchGroups } from '../../utils/nextcloudDirectory.js'
1227
1228
  import { OBJECTS_CHANGED_EVENT } from '../../utils/objectSignals.js'
1228
1229
  import { objectUrlOf, patchObject } from '../../utils/objectWrite.js'
1229
1230
  import { withPersonalLenses } from '../../utils/personalLenses.js'
@@ -3749,6 +3750,8 @@ export default {
3749
3750
  */
3750
3751
  folderSidebarAllValues: [],
3751
3752
  folderRegisterList: [],
3753
+ /** The Nextcloud groups a `folderSidebar.source: 'groups'` pane lists. */
3754
+ folderGroupList: [],
3752
3755
  // `x-index` blocks read off the schema rows a `source: 'register'`
3753
3756
  // folder list was built from, keyed by folder id. The layout a case
3754
3757
  // type carries travels with the record rather than being restated
@@ -4666,7 +4669,7 @@ export default {
4666
4669
  */
4667
4670
  folderSidebarSource() {
4668
4671
  const s = this.folderSidebar && this.folderSidebar.source
4669
- return (s === 'register' || !s) ? 'custom' : s
4672
+ return (s === 'register' || s === 'groups' || !s) ? 'custom' : s
4670
4673
  },
4671
4674
 
4672
4675
  /**
@@ -4683,6 +4686,9 @@ export default {
4683
4686
  if (this.folderSidebar.source === 'register') {
4684
4687
  return this.folderRegisterList
4685
4688
  }
4689
+ if (this.folderSidebar.source === 'groups') {
4690
+ return this.folderGroupList
4691
+ }
4686
4692
  return this.folderSidebar.folders || []
4687
4693
  },
4688
4694
 
@@ -5448,7 +5454,7 @@ export default {
5448
5454
  * @return {string} The id field key.
5449
5455
  */
5450
5456
  folderPassthroughIdField() {
5451
- if (this.folderSidebar && this.folderSidebar.source === 'register') {
5457
+ if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
5452
5458
  return 'id'
5453
5459
  }
5454
5460
  return (this.folderSidebar && this.folderSidebar.idField) || 'id'
@@ -5458,7 +5464,7 @@ export default {
5458
5464
  * @return {string} The name field key for CnFolderSidebar's custom list.
5459
5465
  */
5460
5466
  folderPassthroughNameField() {
5461
- if (this.folderSidebar && this.folderSidebar.source === 'register') {
5467
+ if (this.folderSidebar && (this.folderSidebar.source === 'register' || this.folderSidebar.source === 'groups')) {
5462
5468
  return 'name'
5463
5469
  }
5464
5470
  return (this.folderSidebar && this.folderSidebar.nameField) || 'name'
@@ -7113,6 +7119,7 @@ export default {
7113
7119
  this.maybeOpenCreateFromQuery()
7114
7120
  if (this.folderSidebar) {
7115
7121
  this.loadFolderRegister()
7122
+ this.loadFolderGroups()
7116
7123
  // Reflect a deep-link filter (e.g. ?caseType=<id>) as the active folder.
7117
7124
  const key = this.folderSidebar.filterField || this.folderSidebar.field
7118
7125
  const active = key && this.effectiveActiveFilters[key]
@@ -8985,6 +8992,39 @@ export default {
8985
8992
  }
8986
8993
  },
8987
8994
 
8995
+ /**
8996
+ * Load the Nextcloud groups for `folderSidebar.source: 'groups'`, each
8997
+ * with its member count. `folderSidebar.groups` (`[{ id, parent? }]`)
8998
+ * picks and orders them and nests a group under its `parent`; without
8999
+ * it every group shows, by name. A refused request lists none.
9000
+ *
9001
+ * @return {Promise<void>}
9002
+ * @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
9003
+ */
9004
+ async loadFolderGroups() {
9005
+ const cfg = this.folderSidebar
9006
+ if (!cfg || cfg.source !== 'groups') {
9007
+ return
9008
+ }
9009
+ try {
9010
+ const { default: axios } = await import('@nextcloud/axios')
9011
+ const groups = await fetchGroups(axios, { search: cfg.search || '', limit: cfg.limit || 200 })
9012
+ const declared = Array.isArray(cfg.groups) ? cfg.groups.filter((entry) => entry && entry.id) : null
9013
+ if (!declared) {
9014
+ this.folderGroupList = [...groups].sort((a, b) => a.name.localeCompare(b.name))
9015
+ return
9016
+ }
9017
+ const byId = new Map(groups.map((group) => [group.id, group]))
9018
+ this.folderGroupList = declared
9019
+ .filter((entry) => byId.has(String(entry.id)))
9020
+ .map((entry) => ({ ...byId.get(String(entry.id)), parentId: entry.parent ? String(entry.parent) : null }))
9021
+ } catch (e) {
9022
+ // eslint-disable-next-line no-console
9023
+ console.error('[CnIndexPage] failed to load the Nextcloud groups', e)
9024
+ this.folderGroupList = []
9025
+ }
9026
+ },
9027
+
8988
9028
  /**
8989
9029
  * Load the folder list from an OpenRegister register/schema for a
8990
9030
  * `folderSidebar.source === 'register'` config. Maps each object to
@@ -0,0 +1,386 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-ordered-list-field" data-testid="cn-ordered-list-field">
8
+ <p v-if="label" :id="labelId" class="cn-ordered-list-field__label">
9
+ {{ label }}
10
+ </p>
11
+ <ol
12
+ v-if="rows.length > 0"
13
+ class="cn-ordered-list-field__rows"
14
+ :aria-labelledby="label ? labelId : null"
15
+ :aria-label="label ? null : listLabel">
16
+ <li
17
+ v-for="(row, index) in rows"
18
+ :key="row.value"
19
+ class="cn-ordered-list-field__row"
20
+ :data-testid="`cn-ordered-list-row-${row.value}`">
21
+ <span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
22
+ <span class="cn-ordered-list-field__text">{{ row.label }}</span>
23
+ <span class="cn-ordered-list-field__actions">
24
+ <button
25
+ type="button"
26
+ class="cn-ordered-list-field__button"
27
+ :disabled="disabled || index === 0"
28
+ :aria-label="t('nextcloud-vue', 'Move {item} up', { item: row.label })"
29
+ :data-testid="`cn-ordered-list-up-${row.value}`"
30
+ @click="move(index, index - 1)">
31
+ <ChevronUp :size="20" />
32
+ </button>
33
+ <button
34
+ type="button"
35
+ class="cn-ordered-list-field__button"
36
+ :disabled="disabled || index === rows.length - 1"
37
+ :aria-label="t('nextcloud-vue', 'Move {item} down', { item: row.label })"
38
+ :data-testid="`cn-ordered-list-down-${row.value}`"
39
+ @click="move(index, index + 1)">
40
+ <ChevronDown :size="20" />
41
+ </button>
42
+ <button
43
+ type="button"
44
+ class="cn-ordered-list-field__button"
45
+ :disabled="disabled"
46
+ :aria-label="t('nextcloud-vue', 'Remove {item}', { item: row.label })"
47
+ :data-testid="`cn-ordered-list-remove-${row.value}`"
48
+ @click="remove(index)">
49
+ <Close :size="20" />
50
+ </button>
51
+ </span>
52
+ </li>
53
+ </ol>
54
+ <p v-else class="cn-ordered-list-field__empty" data-testid="cn-ordered-list-empty">
55
+ {{ emptyText || t('nextcloud-vue', 'Nothing added yet.') }}
56
+ </p>
57
+ <div v-if="!disabled" class="cn-ordered-list-field__add">
58
+ <NcSelect
59
+ :modelValue="null"
60
+ :options="available"
61
+ label="label"
62
+ :inputLabel="addLabel || t('nextcloud-vue', 'Add')"
63
+ :placeholder="addPlaceholder"
64
+ :loading="loading"
65
+ :clearable="false"
66
+ :filterable="!searchable"
67
+ data-testid="cn-ordered-list-add"
68
+ @search="onSearch"
69
+ @update:modelValue="add" />
70
+ </div>
71
+ <p class="hidden-visually" aria-live="polite" data-testid="cn-ordered-list-live">
72
+ {{ announcement }}
73
+ </p>
74
+ </div>
75
+ </template>
76
+
77
+ <script>
78
+ import { translate as t } from '@nextcloud/l10n'
79
+ import { NcSelect } from '@nextcloud/vue'
80
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
81
+ import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
82
+ import Close from 'vue-material-design-icons/Close.vue'
83
+
84
+ let orderedListUid = 0
85
+
86
+ /**
87
+ * CnOrderedListField - an ordered list a user keeps for themselves: rows in
88
+ * their order with a move up, a move down and a remove button each, and a
89
+ * search-as-you-type picker under it that adds a row (board
90
+ * DqPersoonlijkeInstellingen, "Zaaktypen in mijn menu").
91
+ *
92
+ * The app supplies the choices (`options`) and keeps the value: every change
93
+ * emits `update:modelValue` with the new list of values. With `searchable`
94
+ * the picker emits `search` as the user types, so the app can fetch matching
95
+ * choices itself; otherwise it filters `options`. Every move is announced in a
96
+ * polite live region, so reordering works with the keyboard and a screen
97
+ * reader, not by dragging.
98
+ *
99
+ * ```vue
100
+ * <CnOrderedListField
101
+ * v-model="menuCaseTypes"
102
+ * label="Case types in my menu"
103
+ * add-label="Add case type"
104
+ * :options="caseTypes" />
105
+ * ```
106
+ *
107
+ * @spec openspec/changes/ordered-list-field/specs/ordered-list-field/spec.md#requirement-a-user-keeps-an-ordered-list-with-the-keyboard
108
+ */
109
+ export default {
110
+ name: 'CnOrderedListField',
111
+
112
+ components: { ChevronDown, ChevronUp, Close, NcSelect },
113
+
114
+ props: {
115
+ /** The values in their order. */
116
+ modelValue: {
117
+ type: Array,
118
+ default: () => [],
119
+ },
120
+
121
+ /**
122
+ * The choices, `{ value, label }` (a plain string is its own label).
123
+ * A value in `modelValue` without a choice shows the value itself.
124
+ */
125
+ options: {
126
+ type: Array,
127
+ default: () => [],
128
+ },
129
+
130
+ /** The list's label, also its accessible name. */
131
+ label: {
132
+ type: String,
133
+ default: '',
134
+ },
135
+
136
+ /** The picker's label. */
137
+ addLabel: {
138
+ type: String,
139
+ default: '',
140
+ },
141
+
142
+ /** Placeholder in the picker. */
143
+ addPlaceholder: {
144
+ type: String,
145
+ default: '',
146
+ },
147
+
148
+ /** Shown when the list is empty. */
149
+ emptyText: {
150
+ type: String,
151
+ default: '',
152
+ },
153
+
154
+ /** Whether the app searches (`search` event) instead of the picker filtering `options`. */
155
+ searchable: {
156
+ type: Boolean,
157
+ default: false,
158
+ },
159
+
160
+ /** Whether the app is fetching choices. */
161
+ loading: {
162
+ type: Boolean,
163
+ default: false,
164
+ },
165
+
166
+ /** A read-only list: no buttons and no picker. */
167
+ disabled: {
168
+ type: Boolean,
169
+ default: false,
170
+ },
171
+ },
172
+
173
+ emits: ['update:modelValue', 'search'],
174
+
175
+ data() {
176
+ orderedListUid += 1
177
+ return {
178
+ labelId: `cn-ordered-list-label-${orderedListUid}`,
179
+ announcement: '',
180
+ }
181
+ },
182
+
183
+ computed: {
184
+ /** @return {Array<{value: string, label: string}>} The choices in one shape. */
185
+ choices() {
186
+ return (Array.isArray(this.options) ? this.options : [])
187
+ .map((option) => (option && typeof option === 'object'
188
+ ? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }
189
+ : { value: String(option), label: String(option) }))
190
+ .filter((option) => option.value !== '')
191
+ },
192
+
193
+ /** @return {Array<{value: string, label: string}>} The rows, in order. */
194
+ rows() {
195
+ return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
196
+ const key = String(value)
197
+ const choice = this.choices.find((option) => option.value === key)
198
+ return { value: key, label: choice ? choice.label : key }
199
+ })
200
+ },
201
+
202
+ /** @return {Array<{value: string, label: string}>} The choices not in the list yet. */
203
+ available() {
204
+ const taken = new Set(this.rows.map((row) => row.value))
205
+ return this.choices.filter((option) => !taken.has(option.value))
206
+ },
207
+
208
+ /** @return {string} The list's accessible name without a label. */
209
+ listLabel() {
210
+ return t('nextcloud-vue', 'Ordered list')
211
+ },
212
+ },
213
+
214
+ methods: {
215
+ t,
216
+
217
+ /**
218
+ * Emit the new order.
219
+ *
220
+ * @param {Array<string>} values The values.
221
+ */
222
+ emitValue(values) {
223
+ /**
224
+ * @event update:modelValue The list changed. Payload: the values in their order.
225
+ */
226
+ this.$emit('update:modelValue', values)
227
+ },
228
+
229
+ /**
230
+ * Move a row to another place and keep focus on the pressed button
231
+ * where it still makes sense.
232
+ *
233
+ * @param {number} from Its index.
234
+ * @param {number} to The index it moves to.
235
+ */
236
+ move(from, to) {
237
+ if (to < 0 || to >= this.rows.length) {
238
+ return
239
+ }
240
+ const values = this.rows.map((row) => row.value)
241
+ const [value] = values.splice(from, 1)
242
+ values.splice(to, 0, value)
243
+ const row = this.rows[from]
244
+ this.emitValue(values)
245
+ this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
246
+ this.$nextTick(() => {
247
+ const direction = to < from ? 'up' : 'down'
248
+ const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
249
+ const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
250
+ if (target && !target.disabled) {
251
+ target.focus()
252
+ }
253
+ })
254
+ },
255
+
256
+ /**
257
+ * Remove a row.
258
+ *
259
+ * @param {number} index Its index.
260
+ */
261
+ remove(index) {
262
+ const row = this.rows[index]
263
+ this.emitValue(this.rows.filter((_, i) => i !== index).map((r) => r.value))
264
+ this.announcement = t('nextcloud-vue', '{item} removed', { item: row.label })
265
+ },
266
+
267
+ /**
268
+ * Add the picked choice at the end.
269
+ *
270
+ * @param {{value: string, label: string}|null} option The choice.
271
+ */
272
+ add(option) {
273
+ if (!option) {
274
+ return
275
+ }
276
+ this.emitValue([...this.rows.map((row) => row.value), option.value])
277
+ this.announcement = t('nextcloud-vue', '{item} added', { item: option.label })
278
+ },
279
+
280
+ /**
281
+ * Pass the typed text on when the app searches.
282
+ *
283
+ * @param {string} query The text.
284
+ */
285
+ onSearch(query) {
286
+ if (this.searchable) {
287
+ /**
288
+ * @event search The user typed in the picker (with `searchable`). Payload: the text.
289
+ */
290
+ this.$emit('search', query)
291
+ }
292
+ },
293
+ },
294
+ }
295
+ </script>
296
+
297
+ <style scoped>
298
+ .cn-ordered-list-field {
299
+ display: flex;
300
+ flex-direction: column;
301
+ gap: 10px;
302
+ }
303
+
304
+ .cn-ordered-list-field__label {
305
+ margin: 0;
306
+ font-weight: 600;
307
+ }
308
+
309
+ .cn-ordered-list-field__rows {
310
+ display: flex;
311
+ flex-direction: column;
312
+ margin: 0;
313
+ padding: 0;
314
+ border: 1px solid var(--color-border);
315
+ border-radius: var(--border-radius-element, 8px);
316
+ list-style: none;
317
+ }
318
+
319
+ .cn-ordered-list-field__row {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 10px;
323
+ min-height: 44px;
324
+ padding: 2px 6px 2px 12px;
325
+ }
326
+
327
+ .cn-ordered-list-field__row + .cn-ordered-list-field__row {
328
+ border-top: 1px solid var(--color-border);
329
+ }
330
+
331
+ .cn-ordered-list-field__position {
332
+ flex: none;
333
+ min-width: 18px;
334
+ color: var(--color-text-maxcontrast);
335
+ font-size: 13px;
336
+ font-variant-numeric: tabular-nums;
337
+ }
338
+
339
+ .cn-ordered-list-field__text {
340
+ flex: 1 1 auto;
341
+ min-width: 0;
342
+ overflow-wrap: anywhere;
343
+ }
344
+
345
+ .cn-ordered-list-field__actions {
346
+ display: flex;
347
+ flex: none;
348
+ gap: 2px;
349
+ }
350
+
351
+ .cn-ordered-list-field__button {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ width: 40px;
356
+ min-width: 40px;
357
+ height: 40px;
358
+ min-height: 40px;
359
+ margin: 0;
360
+ padding: 0;
361
+ border: 0;
362
+ border-radius: var(--border-radius-element, 8px);
363
+ background: transparent;
364
+ color: var(--color-main-text);
365
+ cursor: pointer;
366
+ }
367
+
368
+ .cn-ordered-list-field__button:hover:not(:disabled) {
369
+ background: var(--color-background-hover);
370
+ }
371
+
372
+ .cn-ordered-list-field__button:focus-visible {
373
+ outline: 2px solid var(--color-primary-element);
374
+ outline-offset: 1px;
375
+ }
376
+
377
+ .cn-ordered-list-field__button:disabled {
378
+ opacity: 0.4;
379
+ cursor: default;
380
+ }
381
+
382
+ .cn-ordered-list-field__empty {
383
+ margin: 0;
384
+ color: var(--color-text-maxcontrast);
385
+ }
386
+ </style>
@@ -0,0 +1,7 @@
1
+ // SPDX-FileCopyrightText: 2026 Conduction B.V.
2
+ // SPDX-License-Identifier: EUPL-1.2
3
+
4
+ import CnOrderedListField from './CnOrderedListField.vue'
5
+
6
+ export default CnOrderedListField
7
+ export { CnOrderedListField }