@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
@@ -0,0 +1,449 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <div class="cn-settings-field" :data-testid="`cn-settings-field-${field.key}`">
8
+ <NcCheckboxRadioSwitch
9
+ v-if="kind === 'switch' || kind === 'boolean'"
10
+ :type="kind === 'switch' ? 'switch' : 'checkbox'"
11
+ :modelValue="!!modelValue"
12
+ :disabled="field.readOnly"
13
+ :aria-describedby="describedBy"
14
+ @update:modelValue="emitValue($event)">
15
+ {{ label }}
16
+ </NcCheckboxRadioSwitch>
17
+ <div v-else-if="kind === 'image'"
18
+ class="cn-settings-field__image"
19
+ role="group"
20
+ :aria-labelledby="`${inputId}-label`">
21
+ <span :id="`${inputId}-label`" class="cn-settings-field__image-label">{{ label }}</span>
22
+ <div class="cn-settings-field__image-row">
23
+ <span class="cn-settings-field__image-tile" :data-testid="`cn-settings-field-image-${field.key}`">
24
+ <img v-if="stringValue" :src="stringValue" :alt="t('nextcloud-vue', 'Current {name}', { name: label })">
25
+ <span v-else class="cn-settings-field__image-initials" aria-hidden="true">{{ field.initials || '' }}</span>
26
+ </span>
27
+ <input
28
+ :id="inputId"
29
+ ref="file"
30
+ class="hidden-visually"
31
+ type="file"
32
+ :accept="field.accept || 'image/*'"
33
+ tabindex="-1"
34
+ :aria-labelledby="`${inputId}-label`"
35
+ :aria-describedby="describedBy"
36
+ @change="upload($event.target.files && $event.target.files[0])">
37
+ <NcButton
38
+ v-if="!field.readOnly && field.uploadUrl"
39
+ :disabled="uploading"
40
+ :aria-describedby="describedBy"
41
+ :data-testid="`cn-settings-field-upload-${field.key}`"
42
+ @click="$refs.file && $refs.file.click()">
43
+ <template v-if="uploading" #icon>
44
+ <NcLoadingIcon :size="20" />
45
+ </template>
46
+ {{ field.uploadLabel || t('nextcloud-vue', 'Upload {name}', { name: label }) }}
47
+ </NcButton>
48
+ </div>
49
+ </div>
50
+ <NcSelect
51
+ v-else-if="kind === 'select'"
52
+ :inputId="inputId"
53
+ :modelValue="selectedOption"
54
+ :options="normalizedOptions"
55
+ :inputLabel="label"
56
+ :placeholder="field.placeholder || ''"
57
+ :loading="optionsLoading"
58
+ :disabled="field.readOnly"
59
+ :clearable="!field.required"
60
+ label="label"
61
+ :aria-describedby="describedBy"
62
+ @update:modelValue="emitValue(optionValue($event))" />
63
+ <NcTextArea
64
+ v-else-if="kind === 'textarea'"
65
+ :id="inputId"
66
+ :label="label"
67
+ :modelValue="stringValue"
68
+ :rows="field.rows || 4"
69
+ :placeholder="field.placeholder || ''"
70
+ :readonly="field.readOnly"
71
+ :error="!!error"
72
+ :aria-describedby="describedBy"
73
+ @update:modelValue="emitValue($event)" />
74
+ <NcTextField
75
+ v-else-if="kind === 'secret'"
76
+ :id="inputId"
77
+ :label="label"
78
+ type="password"
79
+ autocomplete="new-password"
80
+ :modelValue="stringValue"
81
+ :placeholder="field.secretSet ? secretSetPlaceholder : (field.placeholder || '')"
82
+ :readonly="field.readOnly"
83
+ :error="!!error"
84
+ :aria-describedby="describedBy"
85
+ @update:modelValue="emitValue($event)" />
86
+ <NcTextField
87
+ v-else-if="kind === 'number'"
88
+ :id="inputId"
89
+ :label="label"
90
+ type="number"
91
+ :min="field.min"
92
+ :max="field.max"
93
+ :modelValue="stringValue"
94
+ :readonly="field.readOnly"
95
+ :error="!!error"
96
+ :aria-describedby="describedBy"
97
+ @update:modelValue="emitValue($event === '' ? null : Number($event))" />
98
+ <NcTextField
99
+ v-else
100
+ :id="inputId"
101
+ :label="label"
102
+ :type="kind === 'password' ? 'password' : 'text'"
103
+ :modelValue="stringValue"
104
+ :placeholder="field.placeholder || ''"
105
+ :readonly="field.readOnly"
106
+ :error="!!error"
107
+ :aria-describedby="describedBy"
108
+ @update:modelValue="emitValue($event)" />
109
+ <p v-if="shownError"
110
+ :id="`${inputId}-error`"
111
+ class="cn-settings-field__error"
112
+ :data-testid="`cn-settings-field-error-${field.key}`">
113
+ {{ shownError }}
114
+ </p>
115
+ <p v-else-if="hint" :id="`${inputId}-hint`" class="cn-settings-field__hint">
116
+ {{ hint }}
117
+ </p>
118
+ </div>
119
+ </template>
120
+
121
+ <script>
122
+ import axios from '@nextcloud/axios'
123
+ import { translate as t } from '@nextcloud/l10n'
124
+ import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
125
+ import { buildHeaders, prefixUrl } from '../../utils/headers.js'
126
+ import { readPath } from '../../utils/readPath.js'
127
+ import { serverErrorMessage } from '../../utils/serverErrorMessage.js'
128
+
129
+ let fieldUid = 0
130
+
131
+ /** Field types and the names CnSettingsPage's manifest uses for them. */
132
+ const KIND_ALIASES = { enum: 'select', string: 'text' }
133
+
134
+ /**
135
+ * CnSettingsField - one settings field: the control for its type with its
136
+ * label, and under it an error or a hint. Shared by CnSettingsPage and
137
+ * CnSettingsFieldsSection.
138
+ *
139
+ * Types: `text` (default), `textarea`, `number`, `switch`, `boolean` (a checkbox),
140
+ * `select` (`enum`) with static `options` or options read from `optionsUrl`,
141
+ * `password` (a masked input that shows its value back) and `secret`
142
+ * (replace-only: the input stays empty, and with `secretSet` says a value is
143
+ * stored; the host sends it only when something is typed), and `image` (a
144
+ * preview tile, the initials without an image, and an upload button that
145
+ * posts the file to `uploadUrl` at once and emits `uploaded`).
146
+ *
147
+ * ```vue
148
+ * <CnSettingsField
149
+ * :field="{ key: 'apiKey', label: 'API key', type: 'secret', secretSet: true }"
150
+ * v-model="form.apiKey" />
151
+ * ```
152
+ */
153
+ export default {
154
+ name: 'CnSettingsField',
155
+
156
+ components: { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextArea, NcTextField },
157
+
158
+ props: {
159
+ /**
160
+ * The field: `{ key, label, type?, options?, optionsUrl?, optionsPath?,
161
+ * optionLabel?, optionValue?, placeholder?, rows?, min?, max?, required?,
162
+ * readOnly?, secretSet? }`. `label` arrives translated.
163
+ *
164
+ * @type {object}
165
+ */
166
+ field: {
167
+ type: Object,
168
+ required: true,
169
+ },
170
+
171
+ /**
172
+ * The value: text, a number, a boolean or a select value. Any type,
173
+ * because a Boolean in a type list would read an empty string as true.
174
+ */
175
+ modelValue: {
176
+ type: null,
177
+ default: null,
178
+ },
179
+
180
+ /** A validation message, drawn under the control and tied to it. */
181
+ error: {
182
+ type: String,
183
+ default: '',
184
+ },
185
+
186
+ /** A hint under the control (not drawn while there is an error). */
187
+ hint: {
188
+ type: String,
189
+ default: '',
190
+ },
191
+ },
192
+
193
+ emits: ['update:modelValue', 'uploaded'],
194
+
195
+ data() {
196
+ fieldUid += 1
197
+ return {
198
+ uid: fieldUid,
199
+ fetchedOptions: null,
200
+ optionsLoading: false,
201
+ uploading: false,
202
+ uploadError: '',
203
+ }
204
+ },
205
+
206
+ computed: {
207
+ /** @return {string} The control kind. */
208
+ kind() {
209
+ const type = String(this.field.type || 'text')
210
+ return KIND_ALIASES[type] || type
211
+ },
212
+
213
+ /** @return {string} The label. */
214
+ label() {
215
+ return String(this.field.label || this.field.key || '')
216
+ },
217
+
218
+ /** @return {string} The control's id. */
219
+ inputId() {
220
+ return `cn-settings-field-${this.uid}-${String(this.field.key).replace(/[^A-Za-z0-9_-]/g, '-')}`
221
+ },
222
+
223
+ /** @return {string} The message shown under the control: an upload failure, else `error`. */
224
+ shownError() {
225
+ return this.uploadError || this.error
226
+ },
227
+
228
+ /** @return {string|null} The id of the error or hint the control is described by. */
229
+ describedBy() {
230
+ if (this.shownError) {
231
+ return `${this.inputId}-error`
232
+ }
233
+ return this.hint ? `${this.inputId}-hint` : null
234
+ },
235
+
236
+ /** @return {string} The value as text for a text control. */
237
+ stringValue() {
238
+ return this.modelValue === null || this.modelValue === undefined ? '' : String(this.modelValue)
239
+ },
240
+
241
+ /** @return {string} The placeholder of a secret that is stored. */
242
+ secretSetPlaceholder() {
243
+ return t('nextcloud-vue', 'Stored. Type a new value to replace it.')
244
+ },
245
+
246
+ /**
247
+ * The select options as `{ label, value }`: the static `options` (strings,
248
+ * numbers or objects), or those read from `optionsUrl`.
249
+ *
250
+ * @return {Array<{label: string, value: (string|number|boolean)}>}
251
+ */
252
+ normalizedOptions() {
253
+ const raw = Array.isArray(this.field.options) ? this.field.options : (this.fetchedOptions || [])
254
+ return raw.map((option) => (option && typeof option === 'object'
255
+ ? { ...option, label: String(option.label ?? option.value ?? ''), value: option.value }
256
+ : { label: String(option), value: option }))
257
+ },
258
+
259
+ /** @return {object|null} The option the value selects, for NcSelect. */
260
+ selectedOption() {
261
+ if (this.modelValue === null || this.modelValue === undefined || this.modelValue === '') {
262
+ return null
263
+ }
264
+ return this.normalizedOptions.find((option) => option.value === this.modelValue)
265
+ || { label: String(this.modelValue), value: this.modelValue }
266
+ },
267
+ },
268
+
269
+ watch: {
270
+ 'field.optionsUrl': {
271
+ handler() {
272
+ this.loadOptions()
273
+ },
274
+
275
+ immediate: true,
276
+ },
277
+ },
278
+
279
+ methods: {
280
+ t,
281
+
282
+ /**
283
+ * Upload a chosen image to `uploadUrl` as multipart form data (the
284
+ * file under `uploadField`, default `image`, plus `uploadParams`), then
285
+ * take the new image url from the answer at `uploadValuePath` (or a
286
+ * cache-busted `previewUrl`).
287
+ *
288
+ * @param {File|undefined} file The chosen file.
289
+ * @return {Promise<void>}
290
+ * @spec openspec/changes/settings-fields-section/specs/settings-fields-section/spec.md#requirement-an-image-field-uploads-at-once
291
+ */
292
+ async upload(file) {
293
+ if (!file || !this.field.uploadUrl) {
294
+ return
295
+ }
296
+ const form = new FormData()
297
+ for (const [name, value] of Object.entries(this.field.uploadParams || {})) {
298
+ form.append(name, value)
299
+ }
300
+ form.append(this.field.uploadField || 'image', file)
301
+ this.uploading = true
302
+ this.uploadError = ''
303
+ try {
304
+ const response = await axios.post(prefixUrl(this.field.uploadUrl), form)
305
+ const fromAnswer = this.field.uploadValuePath ? readPath(response?.data, this.field.uploadValuePath) : ''
306
+ const next = fromAnswer || (this.field.previewUrl ? `${prefixUrl(this.field.previewUrl)}${this.field.previewUrl.includes('?') ? '&' : '?'}v=${Date.now()}` : '')
307
+ /**
308
+ * @event uploaded An image was uploaded. Payload: its new url.
309
+ */
310
+ this.$emit('uploaded', next)
311
+ } catch (error) {
312
+ this.uploadError = t('nextcloud-vue', 'The image could not be uploaded: {reason}', { reason: serverErrorMessage(error) })
313
+ } finally {
314
+ this.uploading = false
315
+ if (this.$refs.file) {
316
+ this.$refs.file.value = ''
317
+ }
318
+ }
319
+ },
320
+
321
+ /**
322
+ * Report a new value.
323
+ *
324
+ * @param {unknown} value The value.
325
+ * @return {void}
326
+ */
327
+ emitValue(value) {
328
+ /**
329
+ * @event update:modelValue The value changed. Payload: the new value.
330
+ */
331
+ this.$emit('update:modelValue', value)
332
+ },
333
+
334
+ /**
335
+ * The storable value of what NcSelect emits.
336
+ *
337
+ * @param {object|string|number|null} emitted An option, or null on clear.
338
+ * @return {string|number|boolean|null}
339
+ */
340
+ optionValue(emitted) {
341
+ if (emitted === null || emitted === undefined) {
342
+ return null
343
+ }
344
+ return typeof emitted === 'object' && 'value' in emitted ? emitted.value : emitted
345
+ },
346
+
347
+ /**
348
+ * Read a sourced select's options: a list (or the list at
349
+ * `optionsPath`, or under `results`) mapped through `optionLabel` and
350
+ * `optionValue` (dotted paths; default `label`/`title`/`name` and `value`/`id`).
351
+ *
352
+ * @return {Promise<void>}
353
+ */
354
+ async loadOptions() {
355
+ const url = this.kind === 'select' && !Array.isArray(this.field.options) ? this.field.optionsUrl : ''
356
+ if (!url) {
357
+ return
358
+ }
359
+ this.optionsLoading = true
360
+ try {
361
+ const response = await fetch(prefixUrl(url), { headers: buildHeaders() })
362
+ if (!response.ok) {
363
+ this.fetchedOptions = []
364
+ return
365
+ }
366
+ const data = await response.json()
367
+ const list = this.field.optionsPath ? readPath(data, this.field.optionsPath) : (Array.isArray(data) ? data : data?.results)
368
+ this.fetchedOptions = (Array.isArray(list) ? list : []).map((item) => {
369
+ if (!item || typeof item !== 'object') {
370
+ return { label: String(item), value: item }
371
+ }
372
+ const value = this.field.optionValue ? readPath(item, this.field.optionValue) : (item.value ?? item.id)
373
+ const label = this.field.optionLabel ? readPath(item, this.field.optionLabel) : (item.label ?? item.title ?? item.name ?? value)
374
+ return { label: String(label ?? ''), value }
375
+ })
376
+ } catch {
377
+ this.fetchedOptions = []
378
+ } finally {
379
+ this.optionsLoading = false
380
+ }
381
+ },
382
+ },
383
+ }
384
+ </script>
385
+
386
+ <style scoped>
387
+ .cn-settings-field {
388
+ display: flex;
389
+ flex-direction: column;
390
+ gap: 4px;
391
+ min-width: 0;
392
+ }
393
+
394
+ .cn-settings-field__hint,
395
+ .cn-settings-field__error {
396
+ margin: 0;
397
+ font-size: 13px;
398
+ line-height: 1.4;
399
+ }
400
+
401
+ .cn-settings-field__image {
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 6px;
405
+ }
406
+
407
+ .cn-settings-field__image-label {
408
+ font-weight: 600;
409
+ }
410
+
411
+ .cn-settings-field__image-row {
412
+ display: flex;
413
+ flex-wrap: wrap;
414
+ align-items: center;
415
+ gap: 12px;
416
+ }
417
+
418
+ .cn-settings-field__image-tile {
419
+ display: inline-flex;
420
+ align-items: center;
421
+ justify-content: center;
422
+ width: 64px;
423
+ height: 64px;
424
+ overflow: hidden;
425
+ border: 1px solid var(--color-border);
426
+ border-radius: var(--border-radius-large);
427
+ background: var(--color-primary-element-light);
428
+ }
429
+
430
+ .cn-settings-field__image-tile img {
431
+ max-width: 100%;
432
+ max-height: 100%;
433
+ object-fit: contain;
434
+ }
435
+
436
+ .cn-settings-field__image-initials {
437
+ font-size: 20px;
438
+ font-weight: 700;
439
+ color: var(--color-primary-element-light-text, var(--color-primary-element));
440
+ }
441
+
442
+ .cn-settings-field__hint {
443
+ color: var(--color-text-maxcontrast);
444
+ }
445
+
446
+ .cn-settings-field__error {
447
+ color: var(--color-error-text, var(--color-error));
448
+ }
449
+ </style>
@@ -0,0 +1,4 @@
1
+ import CnSettingsField from './CnSettingsField.vue'
2
+
3
+ export default CnSettingsField
4
+ export { CnSettingsField }