@conduction/nextcloud-vue 2.108.0 → 2.109.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 (155) hide show
  1. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
  2. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
  3. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
  4. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
  6. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
  8. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
  9. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
  10. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
  13. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
  14. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
  15. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
  16. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
  17. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
  18. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
  19. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
  20. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
  21. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
  22. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
  23. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
  24. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  33. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
  34. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
  35. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
  36. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
  37. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
  38. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
  45. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
  46. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
  47. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
  48. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
  49. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
  50. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
  51. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
  52. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
  53. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
  54. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
  55. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
  56. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
  57. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
  58. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
  59. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
  60. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
  61. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
  64. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
  65. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
  66. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
  67. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
  68. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  69. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  70. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
  71. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  72. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
  73. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
  74. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +246 -0
  75. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
  76. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
  77. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
  78. package/dist/esm/index.js +12 -0
  79. package/dist/esm/index.js.map +1 -1
  80. package/dist/esm/l10n/en.json.js +106 -1
  81. package/dist/esm/l10n/en.json.js.map +1 -1
  82. package/dist/esm/l10n/nl.json.js +105 -1
  83. package/dist/esm/l10n/nl.json.js.map +1 -1
  84. package/dist/esm/library-style.js +1 -1
  85. package/dist/esm/nextcloud-vue.css +495 -61
  86. package/dist/esm/utils/actionsDispatcher.js +6 -1
  87. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  88. package/dist/esm/utils/builtInModals.js +3 -1
  89. package/dist/esm/utils/builtInModals.js.map +1 -1
  90. package/dist/esm/utils/duplicateWarning.js +62 -0
  91. package/dist/esm/utils/duplicateWarning.js.map +1 -0
  92. package/dist/esm/utils/errors.js +4 -0
  93. package/dist/esm/utils/errors.js.map +1 -1
  94. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  95. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  96. package/dist/esm/utils/openDuplicateWarning.js +39 -0
  97. package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
  98. package/dist/esm/utils/optionsFrom.js +69 -2
  99. package/dist/esm/utils/optionsFrom.js.map +1 -1
  100. package/dist/esm/utils/planTree.js +57 -0
  101. package/dist/esm/utils/planTree.js.map +1 -0
  102. package/dist/esm/utils/redactionSegments.js +53 -0
  103. package/dist/esm/utils/redactionSegments.js.map +1 -0
  104. package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
  105. package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
  106. package/dist/esm/utils/searchRefusal.js +62 -0
  107. package/dist/esm/utils/searchRefusal.js.map +1 -0
  108. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  109. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  110. package/dist/nextcloud-vue.cjs.js +17409 -12832
  111. package/dist/nextcloud-vue.cjs.js.map +2 -2
  112. package/dist/nextcloud-vue.css +495 -61
  113. package/l10n/en.json +106 -1
  114. package/l10n/nl.json +105 -1
  115. package/package.json +1 -1
  116. package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
  117. package/src/components/CnAddressSearch/index.js +12 -0
  118. package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
  119. package/src/components/CnAssignDialog/index.js +12 -0
  120. package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
  121. package/src/components/CnDecisionListWidget/index.js +12 -0
  122. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
  123. package/src/components/CnDuplicateWarning/index.js +12 -0
  124. package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
  125. package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
  126. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
  127. package/src/components/CnNcDashboardList/index.js +12 -0
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
  129. package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
  130. package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
  131. package/src/components/CnPlanWidget/index.js +12 -0
  132. package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
  133. package/src/components/CnRedactionEditor/index.js +13 -0
  134. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
  135. package/src/components/CnRetentionWidget/index.js +12 -0
  136. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  137. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
  138. package/src/components/CnWorkQueue/CnWorkQueue.vue +456 -0
  139. package/src/components/CnWorkQueue/index.js +12 -0
  140. package/src/components/index.js +9 -0
  141. package/src/css/index-page.css +24 -0
  142. package/src/index.js +12 -0
  143. package/src/schemas/app-manifest-v2.schema.json +119 -3
  144. package/src/utils/actionsDispatcher.js +6 -1
  145. package/src/utils/builtInModals.js +3 -1
  146. package/src/utils/duplicateWarning.js +59 -0
  147. package/src/utils/errors.js +4 -0
  148. package/src/utils/libraryWidgetKeys.js +4 -0
  149. package/src/utils/openDuplicateWarning.js +35 -0
  150. package/src/utils/optionsFrom.js +68 -1
  151. package/src/utils/planTree.js +54 -0
  152. package/src/utils/redactionSegments.js +50 -0
  153. package/src/utils/registerNcDashboardWidgets.js +57 -0
  154. package/src/utils/searchRefusal.js +59 -0
  155. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,388 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-decision-list" data-testid="cn-decision-list">
7
+ <NcLoadingIcon v-if="loading" :size="24" />
8
+ <NcEmptyContent v-else-if="error"
9
+ :name="text(content.errorTitle) || t('nextcloud-vue', 'This list could not be read')"
10
+ :description="error"
11
+ data-testid="cn-decision-list-error">
12
+ <template #action>
13
+ <NcButton data-testid="cn-decision-list-retry" @click="load">
14
+ {{ t('nextcloud-vue', 'Try again') }}
15
+ </NcButton>
16
+ </template>
17
+ </NcEmptyContent>
18
+ <p v-else-if="rows.length === 0" class="cn-decision-list__empty" data-testid="cn-decision-list-empty">
19
+ {{ text(content.emptyText) || t('nextcloud-vue', 'Nothing to decide right now.') }}
20
+ </p>
21
+ <ul v-else class="cn-decision-list__rows">
22
+ <li v-for="row in rows"
23
+ :key="row.id"
24
+ class="cn-decision-list__row"
25
+ :data-testid="`cn-decision-list-row-${row.id}`">
26
+ <p class="cn-decision-list__title">
27
+ {{ row.title }}
28
+ </p>
29
+ <p v-if="row.meta" class="cn-decision-list__meta">
30
+ {{ row.meta }}
31
+ </p>
32
+ <div class="cn-decision-list__fields">
33
+ <NcSelect :modelValue="answerOption(row.id)"
34
+ :options="answerOptions"
35
+ :inputLabel="text(content.answerLabel) || t('nextcloud-vue', 'Your answer')"
36
+ :clearable="false"
37
+ :data-testid="`cn-decision-list-answer-${row.id}`"
38
+ @update:modelValue="(v) => patch(row.id, { answer: v ? v.id : '' })" />
39
+ <NcTextField v-if="reasonConfig"
40
+ :modelValue="draft(row.id).reason"
41
+ :label="text(reasonConfig.label) || t('nextcloud-vue', 'Why?')"
42
+ :data-testid="`cn-decision-list-reason-${row.id}`"
43
+ @update:modelValue="(v) => patch(row.id, { reason: String(v ?? '') })" />
44
+ <template v-for="field in extraFields(row.id)" :key="field.field">
45
+ <NcDateTimePickerNative v-if="field.type === 'date'"
46
+ :modelValue="draft(row.id).extra[field.field] || null"
47
+ type="date"
48
+ :label="text(field.label) || field.field"
49
+ :data-testid="`cn-decision-list-${field.field}-${row.id}`"
50
+ @update:modelValue="(v) => setExtra(row.id, field.field, v)" />
51
+ <NcTextField v-else
52
+ :modelValue="draft(row.id).extra[field.field] || ''"
53
+ :label="text(field.label) || field.field"
54
+ :data-testid="`cn-decision-list-${field.field}-${row.id}`"
55
+ @update:modelValue="(v) => setExtra(row.id, field.field, v)" />
56
+ </template>
57
+ </div>
58
+ <p v-if="refusals[row.id]" class="cn-decision-list__refusal" role="alert">
59
+ {{ refusals[row.id] }}
60
+ </p>
61
+ <NcButton :disabled="!canSubmit(row.id)"
62
+ :data-testid="`cn-decision-list-submit-${row.id}`"
63
+ @click="submit(row)">
64
+ {{ text(submitConfig.label) || t('nextcloud-vue', 'Record this decision') }}
65
+ </NcButton>
66
+ </li>
67
+ </ul>
68
+ </div>
69
+ </template>
70
+
71
+ <script>
72
+ import axios from '@nextcloud/axios'
73
+ import { translate as t } from '@nextcloud/l10n'
74
+ import { generateUrl } from '@nextcloud/router'
75
+ import { NcButton, NcDateTimePickerNative, NcEmptyContent, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
76
+ import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
77
+ import { fillTemplate } from '../../utils/fillTemplate.js'
78
+ import { readPath } from '../../utils/readPath.js'
79
+
80
+ /**
81
+ * A date picker value as `YYYY-MM-DD`, or the value as a string.
82
+ *
83
+ * @param {unknown} value The value.
84
+ * @return {string}
85
+ */
86
+ function dateValue(value) {
87
+ if (value instanceof Date && !Number.isNaN(value.getTime())) {
88
+ const pad = (n) => String(n).padStart(2, '0')
89
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`
90
+ }
91
+ return value === null || value === undefined ? '' : String(value)
92
+ }
93
+
94
+ /**
95
+ * CnDecisionListWidget — the `decision-list` widget: a worklist where each
96
+ * row takes one answer from a closed set, a reason, and any fields that
97
+ * answer needs, recorded per row (board DqMijnWerk "archival reviews": a
98
+ * verdict, why, and a new date when retained). The row leaves the list once
99
+ * its answer lands, so the user keeps their place. A failed read shows an
100
+ * error with a retry, never an empty list; a refusal stays on its row in the
101
+ * server's words. hermiq approvals, learniq review queues and integriq
102
+ * approvals use the same widget.
103
+ *
104
+ * `content`:
105
+ * - `source`: an endpointSource for the rows (`responsePath`), or `itemsPath`
106
+ * to read them out of the response.
107
+ * - `idField` (default `id`), `title` and `meta`: templates filled from a row.
108
+ * - `answers`: `[{ value, label, fields? }]`; `fields` are `[{ field, type?:
109
+ * "date"|"text", label?, required? }]` that answer asks for.
110
+ * - `reason`: `{ label?, required?, field? }`, required by default and sent
111
+ * as `field` (default `reason`); `false` drops it.
112
+ * - `submit`: `{ label?, request: { method?, url, body? } }`: `url` and `body`
113
+ * templates filled from the row; the answer goes as `answerField` (default
114
+ * `answer`), with the reason and the answer's fields.
115
+ * - `emptyText`, `errorTitle`, `answerLabel`: texts.
116
+ *
117
+ * Texts go through the host translate.
118
+ */
119
+ export default {
120
+ name: 'CnDecisionListWidget',
121
+
122
+ components: { NcButton, NcDateTimePickerNative, NcEmptyContent, NcLoadingIcon, NcSelect, NcTextField },
123
+
124
+ inject: {
125
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
126
+ cnTranslate: { default: () => (key) => key },
127
+ },
128
+
129
+ props: {
130
+ /**
131
+ * The widget's configuration (see the component docs).
132
+ *
133
+ * @type {object}
134
+ */
135
+ content: {
136
+ type: Object,
137
+ default: () => ({}),
138
+ },
139
+ },
140
+
141
+ emits: ['decided'],
142
+
143
+ data() {
144
+ return {
145
+ loading: true,
146
+ error: '',
147
+ items: [],
148
+ drafts: {},
149
+ refusals: {},
150
+ busy: {},
151
+ }
152
+ },
153
+
154
+ computed: {
155
+ /** @return {Array<{value: string, label: string, fields?: Array<object>}>} The answers with a value. */
156
+ answers() {
157
+ return (Array.isArray(this.content.answers) ? this.content.answers : []).filter((a) => a && a.value !== undefined && a.value !== '')
158
+ },
159
+
160
+ /** @return {Array<{id: string, label: string}>} The answers as select options. */
161
+ answerOptions() {
162
+ return this.answers.map((a) => ({ id: String(a.value), label: this.text(a.label) || String(a.value) }))
163
+ },
164
+
165
+ /** @return {object|null} The reason config, null when dropped. */
166
+ reasonConfig() {
167
+ if (this.content.reason === false) {
168
+ return null
169
+ }
170
+ return this.content.reason && typeof this.content.reason === 'object' ? this.content.reason : {}
171
+ },
172
+
173
+ /** @return {object} The submit config. */
174
+ submitConfig() {
175
+ return this.content.submit && typeof this.content.submit === 'object' ? this.content.submit : {}
176
+ },
177
+
178
+ /**
179
+ * The rows: id, title and meta filled from each item.
180
+ *
181
+ * @spec openspec/changes/decision-list-widget/specs/decision-list-widget/spec.md#requirement-a-worklist-of-per-row-decisions
182
+ * @return {Array<{id: string, title: string, meta: string, item: object}>}
183
+ */
184
+ rows() {
185
+ const idField = this.content.idField || 'id'
186
+ return this.items.map((item) => {
187
+ const id = String(readPath(item, idField) ?? '')
188
+ return {
189
+ id,
190
+ title: (this.content.title ? fillTemplate(this.cnTranslate(this.content.title), item).trim() : '') || String(item.title || id),
191
+ meta: this.content.meta ? fillTemplate(this.cnTranslate(this.content.meta), item).trim() : '',
192
+ item,
193
+ }
194
+ }).filter((row) => row.id !== '')
195
+ },
196
+ },
197
+
198
+ mounted() {
199
+ this.load()
200
+ },
201
+
202
+ methods: {
203
+ t,
204
+
205
+ /**
206
+ * A config text through the host translate.
207
+ *
208
+ * @param {unknown} value The text.
209
+ * @return {string}
210
+ */
211
+ text(value) {
212
+ return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
213
+ },
214
+
215
+ /**
216
+ * Read the rows; a failure is an error with a retry, never an empty list.
217
+ *
218
+ * @spec openspec/changes/decision-list-widget/specs/decision-list-widget/spec.md#requirement-a-worklist-of-per-row-decisions
219
+ * @return {Promise<void>}
220
+ */
221
+ async load() {
222
+ this.loading = true
223
+ this.error = ''
224
+ try {
225
+ const data = await fetchEndpointSource(this.content.source, {}, { force: true })
226
+ const list = this.content.itemsPath ? readPath(data, this.content.itemsPath) : data
227
+ this.items = Array.isArray(list) ? list.filter((item) => item && typeof item === 'object') : []
228
+ } catch (failure) {
229
+ const body = failure && failure.response && failure.response.data
230
+ this.error = String((body && (body.message || body.error)) || (failure && failure.message) || failure)
231
+ } finally {
232
+ this.loading = false
233
+ }
234
+ },
235
+
236
+ /**
237
+ * A row's draft.
238
+ *
239
+ * @param {string} id The row id.
240
+ * @return {{answer: string, reason: string, extra: object}}
241
+ */
242
+ draft(id) {
243
+ return this.drafts[id] || { answer: '', reason: '', extra: {} }
244
+ },
245
+
246
+ /**
247
+ * Change a row's draft.
248
+ *
249
+ * @param {string} id The row id.
250
+ * @param {object} change The fields to change.
251
+ */
252
+ patch(id, change) {
253
+ this.drafts = { ...this.drafts, [id]: { ...this.draft(id), ...change } }
254
+ },
255
+
256
+ /**
257
+ * Set one of an answer's fields on a row.
258
+ *
259
+ * @param {string} id The row id.
260
+ * @param {string} field The field.
261
+ * @param {unknown} value The value.
262
+ */
263
+ setExtra(id, field, value) {
264
+ this.patch(id, { extra: { ...this.draft(id).extra, [field]: dateValue(value) } })
265
+ },
266
+
267
+ /**
268
+ * @param {string} id The row id.
269
+ * @return {object|null} The chosen answer as a select option.
270
+ */
271
+ answerOption(id) {
272
+ return this.answerOptions.find((o) => o.id === this.draft(id).answer) || null
273
+ },
274
+
275
+ /**
276
+ * The fields the chosen answer asks for.
277
+ *
278
+ * @param {string} id The row id.
279
+ * @return {Array<object>}
280
+ */
281
+ extraFields(id) {
282
+ const answer = this.answers.find((a) => String(a.value) === this.draft(id).answer)
283
+ return answer && Array.isArray(answer.fields) ? answer.fields.filter((f) => f && f.field) : []
284
+ },
285
+
286
+ /**
287
+ * Whether a row may be sent: an answer, a needed reason, its required fields.
288
+ *
289
+ * @spec openspec/changes/decision-list-widget/specs/decision-list-widget/spec.md#requirement-a-worklist-of-per-row-decisions
290
+ * @param {string} id The row id.
291
+ * @return {boolean}
292
+ */
293
+ canSubmit(id) {
294
+ const draft = this.draft(id)
295
+ if (this.busy[id] || !this.answerOptions.some((o) => o.id === draft.answer)) {
296
+ return false
297
+ }
298
+ if (this.reasonConfig && this.reasonConfig.required !== false && draft.reason.trim() === '') {
299
+ return false
300
+ }
301
+ return this.extraFields(id).every((f) => f.required === false || String(draft.extra[f.field] || '').trim() !== '')
302
+ },
303
+
304
+ /**
305
+ * Record one row's decision and take it off the list.
306
+ *
307
+ * @spec openspec/changes/decision-list-widget/specs/decision-list-widget/spec.md#requirement-a-worklist-of-per-row-decisions
308
+ * @param {{id: string, item: object}} row The row.
309
+ * @return {Promise<void>}
310
+ */
311
+ async submit(row) {
312
+ if (!this.canSubmit(row.id)) {
313
+ return
314
+ }
315
+ const draft = this.draft(row.id)
316
+ const request = this.submitConfig.request || {}
317
+ const body = {}
318
+ for (const [key, value] of Object.entries(request.body || {})) {
319
+ body[key] = typeof value === 'string' ? fillTemplate(value, row.item) : value
320
+ }
321
+ body[this.content.answerField || 'answer'] = draft.answer
322
+ if (this.reasonConfig) {
323
+ body[this.reasonConfig.field || 'reason'] = draft.reason.trim()
324
+ }
325
+ for (const field of this.extraFields(row.id)) {
326
+ if (String(draft.extra[field.field] || '') !== '') {
327
+ body[field.field] = draft.extra[field.field]
328
+ }
329
+ }
330
+ this.busy = { ...this.busy, [row.id]: true }
331
+ this.refusals = { ...this.refusals, [row.id]: '' }
332
+ try {
333
+ if (request.url) {
334
+ const url = fillTemplate(request.url, row.item)
335
+ const method = String(request.method || 'POST').toLowerCase()
336
+ await axios[['post', 'put', 'patch'].includes(method) ? method : 'post'](/^https?:\/\//i.test(url) ? url : generateUrl(url), body)
337
+ }
338
+ this.items = this.items.filter((item) => item !== row.item)
339
+ /** @event decided A row's decision landed. Payload: `{ id, item, body }`. */
340
+ this.$emit('decided', { id: row.id, item: row.item, body })
341
+ } catch (failure) {
342
+ const data = failure && failure.response && failure.response.data
343
+ this.refusals = { ...this.refusals, [row.id]: String((data && (data.message || data.error)) || t('nextcloud-vue', 'This decision was not recorded.')) }
344
+ } finally {
345
+ this.busy = { ...this.busy, [row.id]: false }
346
+ }
347
+ },
348
+ },
349
+ }
350
+ </script>
351
+
352
+ <style scoped>
353
+ .cn-decision-list__empty,
354
+ .cn-decision-list__meta {
355
+ margin: 0;
356
+ color: var(--color-text-maxcontrast);
357
+ }
358
+
359
+ .cn-decision-list__rows {
360
+ margin: 0;
361
+ padding: 0;
362
+ list-style: none;
363
+ }
364
+
365
+ .cn-decision-list__row {
366
+ display: flex;
367
+ flex-direction: column;
368
+ gap: 6px;
369
+ padding-block: 10px;
370
+ border-block-end: 1px solid var(--color-border);
371
+ }
372
+
373
+ .cn-decision-list__title {
374
+ margin: 0;
375
+ font-weight: 600;
376
+ }
377
+
378
+ .cn-decision-list__fields {
379
+ display: grid;
380
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
381
+ gap: 8px;
382
+ }
383
+
384
+ .cn-decision-list__refusal {
385
+ margin: 0;
386
+ color: var(--color-error-text, var(--color-error));
387
+ }
388
+ </style>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnDecisionListWidget — the `decision-list` widget: a worklist where each
6
+ * row takes an answer, a reason and that answer's fields, recorded per row.
7
+ */
8
+
9
+ import CnDecisionListWidget from './CnDecisionListWidget.vue'
10
+
11
+ export { CnDecisionListWidget }
12
+ export default CnDecisionListWidget