@conduction/nextcloud-vue 2.108.0 → 2.110.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 (161) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +115 -12
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +36 -9
  5. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
  7. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
  8. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
  9. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
  10. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
  11. package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
  12. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
  13. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
  14. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
  15. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
  16. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
  17. package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
  18. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
  19. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
  20. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
  21. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
  22. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
  23. package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
  24. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
  25. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
  26. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
  27. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
  28. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
  29. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
  30. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  38. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
  39. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
  40. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
  41. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
  42. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
  43. package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  45. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
  46. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  49. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
  50. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
  51. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
  52. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
  53. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
  54. package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
  55. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
  56. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
  57. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
  58. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
  59. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
  60. package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
  61. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
  62. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
  63. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
  64. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
  65. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
  66. package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
  67. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
  68. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
  69. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
  70. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
  71. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
  72. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
  73. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  74. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
  76. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  77. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
  78. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
  79. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +253 -0
  80. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
  81. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
  82. package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
  83. package/dist/esm/index.js +12 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +106 -1
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +105 -1
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/library-style.js +1 -1
  90. package/dist/esm/nextcloud-vue.css +510 -76
  91. package/dist/esm/utils/actionsDispatcher.js +11 -3
  92. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  93. package/dist/esm/utils/builtInModals.js +3 -1
  94. package/dist/esm/utils/builtInModals.js.map +1 -1
  95. package/dist/esm/utils/duplicateWarning.js +62 -0
  96. package/dist/esm/utils/duplicateWarning.js.map +1 -0
  97. package/dist/esm/utils/errors.js +4 -0
  98. package/dist/esm/utils/errors.js.map +1 -1
  99. package/dist/esm/utils/libraryWidgetKeys.js +4 -0
  100. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  101. package/dist/esm/utils/openDuplicateWarning.js +39 -0
  102. package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
  103. package/dist/esm/utils/optionsFrom.js +69 -2
  104. package/dist/esm/utils/optionsFrom.js.map +1 -1
  105. package/dist/esm/utils/planTree.js +57 -0
  106. package/dist/esm/utils/planTree.js.map +1 -0
  107. package/dist/esm/utils/redactionSegments.js +53 -0
  108. package/dist/esm/utils/redactionSegments.js.map +1 -0
  109. package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
  110. package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
  111. package/dist/esm/utils/searchRefusal.js +62 -0
  112. package/dist/esm/utils/searchRefusal.js.map +1 -0
  113. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  114. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  115. package/dist/nextcloud-vue.cjs.js +17560 -12844
  116. package/dist/nextcloud-vue.cjs.js.map +2 -2
  117. package/dist/nextcloud-vue.css +510 -76
  118. package/l10n/en.json +106 -1
  119. package/l10n/nl.json +105 -1
  120. package/package.json +1 -1
  121. package/src/components/CnActionDialog/CnActionDialog.vue +134 -13
  122. package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
  123. package/src/components/CnAddressSearch/index.js +12 -0
  124. package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
  125. package/src/components/CnAssignDialog/index.js +12 -0
  126. package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
  127. package/src/components/CnDecisionListWidget/index.js +12 -0
  128. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
  129. package/src/components/CnDuplicateWarning/index.js +12 -0
  130. package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
  131. package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
  132. package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
  133. package/src/components/CnNcDashboardList/index.js +12 -0
  134. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
  135. package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
  136. package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
  137. package/src/components/CnPlanWidget/index.js +12 -0
  138. package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
  139. package/src/components/CnRedactionEditor/index.js +13 -0
  140. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
  141. package/src/components/CnRetentionWidget/index.js +12 -0
  142. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  143. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
  144. package/src/components/CnWorkQueue/CnWorkQueue.vue +463 -0
  145. package/src/components/CnWorkQueue/index.js +12 -0
  146. package/src/components/index.js +9 -0
  147. package/src/css/index-page.css +24 -0
  148. package/src/index.js +12 -0
  149. package/src/schemas/app-manifest-v2.schema.json +119 -3
  150. package/src/utils/actionsDispatcher.js +10 -2
  151. package/src/utils/builtInModals.js +3 -1
  152. package/src/utils/duplicateWarning.js +59 -0
  153. package/src/utils/errors.js +4 -0
  154. package/src/utils/libraryWidgetKeys.js +4 -0
  155. package/src/utils/openDuplicateWarning.js +35 -0
  156. package/src/utils/optionsFrom.js +68 -1
  157. package/src/utils/planTree.js +54 -0
  158. package/src/utils/redactionSegments.js +50 -0
  159. package/src/utils/registerNcDashboardWidgets.js +57 -0
  160. package/src/utils/searchRefusal.js +59 -0
  161. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,525 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <CnDialog
7
+ :name="text(config.title) || t('nextcloud-vue', 'Assign')"
8
+ :eyebrow="fill(config.eyebrow)"
9
+ :subtitle="text(config.subtitle)"
10
+ width="form"
11
+ :noClose="busy"
12
+ data-testid="cn-assign-dialog"
13
+ @closing="close">
14
+ <div v-if="!done" class="cn-assign-dialog">
15
+ <div class="cn-assign-dialog__field">
16
+ <CnSearchSelect
17
+ :modelValue="picked ? picked.id : null"
18
+ :inputId="assigneeId"
19
+ :inputLabel="assigneeLabel"
20
+ :labelOutside="true"
21
+ :options="shownOptions"
22
+ :loading="searching"
23
+ :filterable="false"
24
+ :placeholder="text(assigneeConfig.placeholder)"
25
+ data-testid="cn-assign-dialog-assignee"
26
+ @search="onSearch"
27
+ @update:modelValue="onPick" />
28
+ <span v-if="assigneeHint" class="cn-assign-dialog__hint">{{ assigneeHint }}</span>
29
+ </div>
30
+
31
+ <div v-if="config.note !== false" class="cn-assign-dialog__field">
32
+ <NcTextArea
33
+ :id="noteId"
34
+ v-model="note"
35
+ :label="noteLabel"
36
+ :labelOutside="false"
37
+ resize="vertical"
38
+ data-testid="cn-assign-dialog-note" />
39
+ <span v-if="noteConfig.hint" class="cn-assign-dialog__hint">{{ text(noteConfig.hint) }}</span>
40
+ </div>
41
+
42
+ <div v-for="toggle in toggles"
43
+ :key="toggle.field"
44
+ class="cn-assign-dialog__toggle"
45
+ :class="{ 'cn-assign-dialog__toggle--on': flags[toggle.field] }"
46
+ :data-testid="`cn-assign-dialog-toggle-${toggle.field}`">
47
+ <NcCheckboxRadioSwitch :modelValue="flags[toggle.field] === true" @update:modelValue="flags[toggle.field] = $event">
48
+ <span class="cn-assign-dialog__toggle-title">{{ text(toggle.label) }}</span>
49
+ </NcCheckboxRadioSwitch>
50
+ <span v-if="toggle.note" class="cn-assign-dialog__hint cn-assign-dialog__toggle-note">{{ text(toggle.note) }}</span>
51
+ </div>
52
+
53
+ <p v-if="refusal"
54
+ class="cn-assign-dialog__error"
55
+ role="alert"
56
+ data-testid="cn-assign-dialog-refusal">
57
+ {{ refusal }}
58
+ </p>
59
+ </div>
60
+
61
+ <p v-else role="status" data-testid="cn-assign-dialog-done">
62
+ {{ text(config.doneText) || t('nextcloud-vue', 'Done') }}
63
+ </p>
64
+
65
+ <template #actions>
66
+ <NcButton :disabled="busy" data-testid="cn-assign-dialog-cancel" @click="close">
67
+ {{ done ? t('nextcloud-vue', 'Close') : t('nextcloud-vue', 'Cancel') }}
68
+ </NcButton>
69
+ <NcButton v-if="!done"
70
+ variant="primary"
71
+ :disabled="!canConfirm"
72
+ data-testid="cn-assign-dialog-confirm"
73
+ @click="confirm">
74
+ <template #icon>
75
+ <CnIcon :name="confirmConfig.icon || 'SwapHorizontal'" :size="20" />
76
+ </template>
77
+ {{ text(confirmConfig.label) || t('nextcloud-vue', 'Assign') }}
78
+ </NcButton>
79
+ </template>
80
+ </CnDialog>
81
+ </template>
82
+
83
+ <script>
84
+ import axios from '@nextcloud/axios'
85
+ import { emit } from '@nextcloud/event-bus'
86
+ import { translate as t } from '@nextcloud/l10n'
87
+ import { generateUrl } from '@nextcloud/router'
88
+ import { NcButton, NcCheckboxRadioSwitch, NcTextArea } from '@nextcloud/vue'
89
+ import { fetchEndpointSource, interpolateUrlTokens } from '../../composables/useEndpointSource.js'
90
+ import { fillTemplate } from '../../utils/fillTemplate.js'
91
+ import { readPath } from '../../utils/readPath.js'
92
+ import { searchNextcloudGroups, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
93
+ import { CnDialog } from '../CnDialog/index.js'
94
+ import { CnIcon } from '../CnIcon/index.js'
95
+ import { CnSearchSelect } from '../CnSearchSelect/index.js'
96
+
97
+ /** The page refresh channel the built-in widgets listen on. */
98
+ const PAGE_REFRESH = 'cn:page:refresh'
99
+
100
+ /** How long typing rests before a search runs, in ms. */
101
+ const SEARCH_DELAY = 250
102
+
103
+ /** Gives each dialog's fields their own ids. */
104
+ let fieldSeq = 0
105
+
106
+ /**
107
+ * The found rows, with what was typed offered as an assignee of its own when
108
+ * no row carries that id or name: where sharing is restricted the search
109
+ * answers nothing, and an endpoint may still take the name.
110
+ *
111
+ * @param {Array<object>} found The rows found.
112
+ * @param {string} typed What was typed.
113
+ * @param {string} label The typed row's label.
114
+ * @return {Array<object>} The rows.
115
+ */
116
+ export function withTypedAssignee(found, typed, label) {
117
+ const lower = typed.toLowerCase()
118
+ if (typed === '' || found.some((row) => String(row.id) === typed || String(row.label).toLowerCase() === lower)) {
119
+ return found
120
+ }
121
+ return [...found, { id: typed, label, icon: 'AccountGroupOutline', kind: 'typed' }]
122
+ }
123
+
124
+ /**
125
+ * CnAssignDialog — hand a record to another user or team, with a note and
126
+ * optional switches, configured from the manifest (an `open-modal` action's
127
+ * props). dossiq's case handover (board DqOverdragen: new team, why it moves,
128
+ * "this is a doorzending (Awb 2:3)") is the first; pipelinq tickets and bulk
129
+ * reassignment use the same dialog.
130
+ *
131
+ * `config`:
132
+ * - `title`, `subtitle`, `doneText`: texts, through the host translate;
133
+ * `eyebrow` a template filled from the record.
134
+ * - `source`: an endpointSource to read the record from, else `object`.
135
+ * - `assignee`: `{ kinds?, label?, placeholder?, hint?, currentPath?,
136
+ * currentHint?, currentAs?, allowTyped?, field?, kindField? }`. `kinds` is `["group"]`
137
+ * (default), `["user"]` or both; `currentPath` reads who has it now from the
138
+ * record and `currentHint` ("Now with {current}.", the placeholder named
139
+ * by `currentAs`) says so, in place of `hint`; `allowTyped`
140
+ * offers what was typed as a choice; the id is sent as `field` (default
141
+ * `assignee`) and its kind as `kindField` when set.
142
+ * - `note`: `{ label?, hint?, required?, field? }`, required by default and
143
+ * sent as `field` (default `note`); `false` drops it.
144
+ * - `toggles`: `[{ field, label, note? }]`: switches that start off and are
145
+ * sent as booleans.
146
+ * - `confirm`: `{ label?, icon?, request?: { method?, url, body? }, action? }`.
147
+ * `request` sends the choice itself (`body` templates filled from the
148
+ * record); `action` dispatches a manifest action with the choice as its
149
+ * last argument.
150
+ *
151
+ * Tokens (`@objectId`, `@object.<field>`) resolve in every url. A refusal is
152
+ * shown in the server's own words and the dialog stays open.
153
+ */
154
+ export default {
155
+ name: 'CnAssignDialog',
156
+
157
+ components: { CnDialog, CnIcon, CnSearchSelect, NcButton, NcCheckboxRadioSwitch, NcTextArea },
158
+
159
+ inject: {
160
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
161
+ cnTranslate: { default: () => (key) => key },
162
+ /** The page's pre-bound action dispatcher (CnPageRenderer), for `confirm.action`. */
163
+ cnDispatchAction: { default: null },
164
+ },
165
+
166
+ props: {
167
+ /**
168
+ * The dialog's configuration (see the component docs).
169
+ *
170
+ * @type {object}
171
+ */
172
+ config: {
173
+ type: Object,
174
+ default: () => ({}),
175
+ },
176
+
177
+ /** The record's id, for the `@objectId` token. */
178
+ objectId: {
179
+ type: [String, Number],
180
+ default: '',
181
+ },
182
+
183
+ /**
184
+ * The record, when the host has it.
185
+ *
186
+ * @type {object|null}
187
+ */
188
+ object: {
189
+ type: Object,
190
+ default: null,
191
+ },
192
+ },
193
+
194
+ emits: ['close', 'done'],
195
+
196
+ data() {
197
+ const seq = ++fieldSeq
198
+ const flags = {}
199
+ for (const toggle of Array.isArray(this.config.toggles) ? this.config.toggles : []) {
200
+ if (toggle && toggle.field) {
201
+ flags[toggle.field] = false
202
+ }
203
+ }
204
+ return {
205
+ record: this.object,
206
+ options: [],
207
+ picked: null,
208
+ searching: false,
209
+ timer: null,
210
+ searchSeq: 0,
211
+ note: '',
212
+ flags,
213
+ busy: false,
214
+ refusal: '',
215
+ done: false,
216
+ assigneeId: `cn-assign-dialog-assignee-${seq}`,
217
+ noteId: `cn-assign-dialog-note-${seq}`,
218
+ }
219
+ },
220
+
221
+ computed: {
222
+ /** @return {object} The token context. */
223
+ tokenCtx() {
224
+ return { objectId: this.recordId, object: this.record || {} }
225
+ },
226
+
227
+ /**
228
+ * The record's id: `objectId`, or the route's `id` when the prop is
229
+ * empty or a token the opener could not resolve.
230
+ *
231
+ * @return {string|number}
232
+ */
233
+ recordId() {
234
+ const given = this.objectId
235
+ if (given !== '' && given !== null && given !== undefined && !String(given).startsWith('@')) {
236
+ return given
237
+ }
238
+ return (this.$route && this.$route.params && this.$route.params.id) || ''
239
+ },
240
+
241
+ /** @return {object} The assignee config. */
242
+ assigneeConfig() {
243
+ return this.config.assignee && typeof this.config.assignee === 'object' ? this.config.assignee : {}
244
+ },
245
+
246
+ /** @return {object} The note config. */
247
+ noteConfig() {
248
+ return this.config.note && typeof this.config.note === 'object' ? this.config.note : {}
249
+ },
250
+
251
+ /** @return {object} The confirm config. */
252
+ confirmConfig() {
253
+ return this.config.confirm && typeof this.config.confirm === 'object' ? this.config.confirm : {}
254
+ },
255
+
256
+ /** @return {Array<object>} The switches with a field. */
257
+ toggles() {
258
+ return (Array.isArray(this.config.toggles) ? this.config.toggles : []).filter((toggle) => toggle && toggle.field)
259
+ },
260
+
261
+ /** @return {string[]} The kinds searched: `group`, `user`. */
262
+ kinds() {
263
+ const kinds = Array.isArray(this.assigneeConfig.kinds) ? this.assigneeConfig.kinds.filter((k) => k === 'group' || k === 'user') : []
264
+ return kinds.length ? kinds : ['group']
265
+ },
266
+
267
+ /** @return {string} The assignee field's label. */
268
+ assigneeLabel() {
269
+ if (this.assigneeConfig.label) {
270
+ return this.text(this.assigneeConfig.label)
271
+ }
272
+ return this.kinds.length === 1 && this.kinds[0] === 'user' ? t('nextcloud-vue', 'New handler') : t('nextcloud-vue', 'New team')
273
+ },
274
+
275
+ /** @return {string} Who has the record now, read from it. */
276
+ current() {
277
+ const path = this.assigneeConfig.currentPath
278
+ const value = path && this.record ? readPath(this.record, path) : ''
279
+ return value === undefined || value === null ? '' : String(value)
280
+ },
281
+
282
+ /**
283
+ * The hint under the assignee field: who has it now, else the configured hint.
284
+ *
285
+ * @spec openspec/changes/assign-dialog/specs/assign-dialog/spec.md#requirement-a-manifest-configures-an-assign-dialog
286
+ * @return {string}
287
+ */
288
+ assigneeHint() {
289
+ const name = this.assigneeConfig.currentAs || 'current'
290
+ const now = this.current && this.assigneeConfig.currentHint ? fillTemplate(this.cnTranslate(this.assigneeConfig.currentHint), { [name]: this.current }) : ''
291
+ return now || this.text(this.assigneeConfig.hint)
292
+ },
293
+
294
+ /** @return {Array<object>} The rows, keeping the picked one listed. */
295
+ shownOptions() {
296
+ if (this.picked && !this.options.some((o) => String(o.id) === String(this.picked.id))) {
297
+ return [this.picked, ...this.options]
298
+ }
299
+ return this.options
300
+ },
301
+
302
+ /** @return {boolean} Whether a note is asked and needed. */
303
+ noteNeeded() {
304
+ return this.config.note !== false && this.noteConfig.required !== false
305
+ },
306
+
307
+ /** @return {string} The note field's label. */
308
+ noteLabel() {
309
+ return this.text(this.noteConfig.label) || t('nextcloud-vue', 'Why it is moving')
310
+ },
311
+
312
+ /**
313
+ * Whether the assignment may run: an assignee, a note when needed, nothing in flight.
314
+ *
315
+ * @spec openspec/changes/assign-dialog/specs/assign-dialog/spec.md#requirement-a-manifest-configures-an-assign-dialog
316
+ * @return {boolean}
317
+ */
318
+ canConfirm() {
319
+ return !this.busy && this.picked !== null && (!this.noteNeeded || this.note.trim() !== '')
320
+ },
321
+ },
322
+
323
+ async mounted() {
324
+ const source = this.config.source
325
+ if (source && source.url) {
326
+ try {
327
+ const data = await fetchEndpointSource(source, this.tokenCtx, { fresh: true })
328
+ this.record = data && typeof data === 'object' ? data : this.record
329
+ } catch {
330
+ // The hint stays without the current assignee; the dialog still works.
331
+ }
332
+ }
333
+ },
334
+
335
+ beforeUnmount() {
336
+ clearTimeout(this.timer)
337
+ },
338
+
339
+ methods: {
340
+ t,
341
+
342
+ /**
343
+ * A config text through the host translate, '' for anything else.
344
+ *
345
+ * @param {unknown} value The text.
346
+ * @return {string}
347
+ */
348
+ text(value) {
349
+ return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
350
+ },
351
+
352
+ /**
353
+ * A template filled from the record, translated first.
354
+ *
355
+ * @param {unknown} template The template.
356
+ * @return {string}
357
+ */
358
+ fill(template) {
359
+ if (typeof template !== 'string' || template === '') {
360
+ return ''
361
+ }
362
+ return fillTemplate(this.cnTranslate(template), this.record || {}).trim()
363
+ },
364
+
365
+ /**
366
+ * Search after typing rests.
367
+ *
368
+ * @param {string} query What was typed.
369
+ */
370
+ onSearch(query) {
371
+ clearTimeout(this.timer)
372
+ const typed = String(query ?? '').trim()
373
+ if (typed === '') {
374
+ this.options = []
375
+ return
376
+ }
377
+ this.timer = setTimeout(() => this.search(typed), SEARCH_DELAY)
378
+ },
379
+
380
+ /**
381
+ * Search the configured kinds; offer what was typed when allowed.
382
+ *
383
+ * @spec openspec/changes/assign-dialog/specs/assign-dialog/spec.md#requirement-a-manifest-configures-an-assign-dialog
384
+ * @param {string} typed What was typed.
385
+ * @return {Promise<void>}
386
+ */
387
+ async search(typed) {
388
+ const seq = ++this.searchSeq
389
+ this.searching = true
390
+ const [groups, users] = await Promise.all([
391
+ this.kinds.includes('group') ? searchNextcloudGroups(typed, { limit: 20 }) : [],
392
+ this.kinds.includes('user') ? searchNextcloudUsers(typed, { limit: 20 }) : [],
393
+ ])
394
+ if (seq !== this.searchSeq) {
395
+ return
396
+ }
397
+ const found = [
398
+ ...groups.map((g) => ({ id: g.id, label: g.label, secondary: g.subline || '', icon: 'AccountGroupOutline', kind: 'group' })),
399
+ ...users.map((u) => ({ id: u.id, label: u.label, secondary: u.subline || '', avatar: true, kind: 'user' })),
400
+ ]
401
+ this.options = this.assigneeConfig.allowTyped ? withTypedAssignee(found, typed, t('nextcloud-vue', 'Use "{name}"', { name: typed })) : found
402
+ this.searching = false
403
+ },
404
+
405
+ /**
406
+ * Take the picked row.
407
+ *
408
+ * @param {string|null} id The picked id.
409
+ */
410
+ onPick(id) {
411
+ this.picked = id === null ? null : (this.shownOptions.find((o) => String(o.id) === String(id)) || null)
412
+ },
413
+
414
+ /**
415
+ * The choice as sent: assignee, its kind, the note and the switches.
416
+ *
417
+ * @return {object}
418
+ */
419
+ choice() {
420
+ const out = { [this.assigneeConfig.field || 'assignee']: String(this.picked.id) }
421
+ if (this.assigneeConfig.kindField) {
422
+ out[this.assigneeConfig.kindField] = this.picked.kind === 'typed' ? this.kinds[0] : this.picked.kind
423
+ }
424
+ if (this.config.note !== false) {
425
+ out[this.noteConfig.field || 'note'] = this.note.trim()
426
+ }
427
+ return { ...out, ...this.flags }
428
+ },
429
+
430
+ /**
431
+ * Assign once: send `confirm.request`, or dispatch `confirm.action`,
432
+ * then show the done text and refresh the page.
433
+ *
434
+ * @spec openspec/changes/assign-dialog/specs/assign-dialog/spec.md#requirement-a-manifest-configures-an-assign-dialog
435
+ * @return {Promise<void>}
436
+ */
437
+ async confirm() {
438
+ if (!this.canConfirm) {
439
+ return
440
+ }
441
+ this.busy = true
442
+ this.refusal = ''
443
+ const choice = this.choice()
444
+ try {
445
+ const request = this.confirmConfig.request
446
+ if (request && request.url) {
447
+ const url = interpolateUrlTokens(request.url, this.tokenCtx)
448
+ const body = {}
449
+ for (const [key, value] of Object.entries(request.body || {})) {
450
+ body[key] = typeof value === 'string' ? fillTemplate(value, this.record || {}) : value
451
+ }
452
+ const method = String(request.method || 'POST').toLowerCase()
453
+ await axios[['post', 'put', 'patch'].includes(method) ? method : 'post'](/^https?:\/\//i.test(url) ? url : generateUrl(url), { ...body, ...choice })
454
+ } else if (this.confirmConfig.action && typeof this.cnDispatchAction === 'function') {
455
+ await this.cnDispatchAction({ ...this.confirmConfig.action, args: [...(this.confirmConfig.action.args || []), choice] })
456
+ }
457
+ this.done = true
458
+ emit(PAGE_REFRESH, {})
459
+ /** @event done The assignment ran. Payload: the choice as sent. */
460
+ this.$emit('done', choice)
461
+ } catch (failure) {
462
+ const data = failure && failure.response && failure.response.data
463
+ this.refusal = String((data && (data.message || data.error)) || t('nextcloud-vue', 'It was not handed over.'))
464
+ } finally {
465
+ this.busy = false
466
+ }
467
+ },
468
+
469
+ /** Close the dialog, unless the assignment is running. */
470
+ close() {
471
+ if (this.busy) {
472
+ return
473
+ }
474
+ /** @event close The dialog was closed. */
475
+ this.$emit('close')
476
+ },
477
+ },
478
+ }
479
+ </script>
480
+
481
+ <style scoped>
482
+ .cn-assign-dialog {
483
+ display: flex;
484
+ flex-direction: column;
485
+ gap: 18px;
486
+ }
487
+
488
+ .cn-assign-dialog__field {
489
+ display: flex;
490
+ flex-direction: column;
491
+ gap: 6px;
492
+ }
493
+
494
+ .cn-assign-dialog__hint {
495
+ font-size: 13px;
496
+ color: var(--color-text-maxcontrast);
497
+ }
498
+
499
+ .cn-assign-dialog__toggle {
500
+ display: flex;
501
+ flex-direction: column;
502
+ gap: 2px;
503
+ padding: 12px 14px;
504
+ border: 1px solid var(--color-border-dark);
505
+ border-radius: var(--border-radius-large, 8px);
506
+ }
507
+
508
+ .cn-assign-dialog__toggle--on {
509
+ border: 2px solid var(--color-primary-element);
510
+ background: var(--color-primary-element-light);
511
+ }
512
+
513
+ .cn-assign-dialog__toggle-title {
514
+ font-weight: 600;
515
+ }
516
+
517
+ .cn-assign-dialog__toggle-note {
518
+ padding-inline-start: 44px;
519
+ }
520
+
521
+ .cn-assign-dialog__error {
522
+ margin: 0;
523
+ color: var(--color-error-text, var(--color-error));
524
+ }
525
+ </style>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnAssignDialog — hand a record to another user or team, with a note and
6
+ * optional switches, configured from the manifest.
7
+ */
8
+
9
+ import CnAssignDialog from './CnAssignDialog.vue'
10
+
11
+ export { CnAssignDialog }
12
+ export default CnAssignDialog