@conduction/nextcloud-vue 2.56.0-beta.62 → 2.56.0-beta.63

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,225 @@
1
+ import { translatePlural, translate } from '@nextcloud/l10n';
2
+ import { generateUrl } from '@nextcloud/router';
3
+ import { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue';
4
+ import { duplicateAffordances } from '../../utils/duplicateWarning.js';
5
+ import { fillTemplate } from '../../utils/fillTemplate.js';
6
+ import __sfc_main from '../CnDialog/CnDialog.vue.js';
7
+
8
+ /**
9
+ * CnDuplicateWarning — "this may already exist", before a record is filed.
10
+ * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),
11
+ * never scoring anything itself, with each matched record's title, a meta
12
+ * line, what it matched on and how strongly. Under a `warn` policy anyone may
13
+ * file it anyway; under `block` only a user who `mayOverride`, and they say
14
+ * why. A match the user may not read is reported without disclosing it.
15
+ * dossiq's case intake is the first consumer; pipelinq contacts share it.
16
+ *
17
+ * Open it in a template, or from code with `openDuplicateWarning(props)`,
18
+ * which resolves with `{ reason, over }` or null.
19
+ *
20
+ * ```vue
21
+ * <CnDuplicateWarning
22
+ * :matches="matches"
23
+ * :records="readableCases"
24
+ * policy="block"
25
+ * :may-override="isCoordinator"
26
+ * record-url="/apps/dossiq/cases/{uuid}"
27
+ * :field-labels="{ requester: 'Requester', title: 'Subject' }"
28
+ * @file="save"
29
+ * @close="cancel" />
30
+ * ```
31
+ */
32
+ var script = {
33
+ name: 'CnDuplicateWarning',
34
+
35
+ components: { CnDialog: __sfc_main, NcButton, NcNoteCard, NcTextField },
36
+
37
+ inject: {
38
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
39
+ cnTranslate: { default: () => (key) => key },
40
+ },
41
+
42
+ props: {
43
+ /**
44
+ * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.
45
+ *
46
+ * @type {Array<object>}
47
+ */
48
+ matches: {
49
+ type: Array,
50
+ default: () => [],
51
+ },
52
+
53
+ /**
54
+ * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.
55
+ *
56
+ * @type {Array<object>}
57
+ */
58
+ records: {
59
+ type: Array,
60
+ default: () => [],
61
+ },
62
+
63
+ /** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */
64
+ policy: {
65
+ type: String,
66
+ default: 'warn',
67
+ },
68
+
69
+ /** Whether this user may file over a block (and must say why). */
70
+ mayOverride: {
71
+ type: Boolean,
72
+ default: false,
73
+ },
74
+
75
+ /** A record's title, a template filled from it. */
76
+ rowTitle: {
77
+ type: String,
78
+ default: '{title}',
79
+ },
80
+
81
+ /** A record's meta line, a template filled from it. */
82
+ rowMeta: {
83
+ type: String,
84
+ default: '{identifier}',
85
+ },
86
+
87
+ /** A record's page, a template with `{uuid}`; empty draws no link. */
88
+ recordUrl: {
89
+ type: String,
90
+ default: '',
91
+ },
92
+
93
+ /**
94
+ * Words for the matched fields (`{ requester: "Requester" }`), through the host translate.
95
+ *
96
+ * @type {object}
97
+ */
98
+ fieldLabels: {
99
+ type: Object,
100
+ default: () => ({}),
101
+ },
102
+
103
+ /** The dialog title; defaults to "This may already exist". */
104
+ title: {
105
+ type: String,
106
+ default: '',
107
+ },
108
+
109
+ /** The note's sentence, `{count}` filled; defaults to "%n existing record looks like this one.". */
110
+ headline: {
111
+ type: String,
112
+ default: '',
113
+ },
114
+
115
+ /** The sentence under a block the user may not pass. */
116
+ blockedText: {
117
+ type: String,
118
+ default: '',
119
+ },
120
+
121
+ /** The reason field's label. */
122
+ reasonLabel: {
123
+ type: String,
124
+ default: '',
125
+ },
126
+
127
+ /** The continue button's label; defaults to "File it anyway". */
128
+ continueLabel: {
129
+ type: String,
130
+ default: '',
131
+ },
132
+ },
133
+
134
+ emits: ['close', 'file'],
135
+
136
+ data() {
137
+ return {
138
+ reason: '',
139
+ }
140
+ },
141
+
142
+ computed: {
143
+ /**
144
+ * What to draw and offer.
145
+ *
146
+ * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
147
+ * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}
148
+ */
149
+ affordances() {
150
+ return duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })
151
+ },
152
+
153
+ /** @return {boolean} Whether "file it anyway" may be pressed. */
154
+ canFile() {
155
+ return this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')
156
+ },
157
+
158
+ /** @return {string} The note's sentence. */
159
+ headlineText() {
160
+ const count = this.matches.length;
161
+ if (this.headline) {
162
+ return fillTemplate(this.cnTranslate(this.headline), { count })
163
+ }
164
+ return translatePlural('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)
165
+ },
166
+
167
+ /**
168
+ * One row per match: the readable record's title and meta and link, or
169
+ * a neutral name for one the user may not read.
170
+ *
171
+ * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
172
+ * @return {Array<object>}
173
+ */
174
+ rows() {
175
+ return this.matches.map((match) => {
176
+ const uuid = String((match && match.uuid) || '');
177
+ const record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid);
178
+ const fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : [];
179
+ const score = Number(match && match.score);
180
+ return {
181
+ uuid,
182
+ title: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : translate('nextcloud-vue', 'A record you may not open'),
183
+ meta: record ? fillTemplate(this.rowMeta, record).trim() : '',
184
+ href: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',
185
+ matchedOn: fields.length
186
+ ? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')
187
+ : translate('nextcloud-vue', 'Matched on the score, not on a single field'),
188
+ percentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,
189
+ }
190
+ })
191
+ },
192
+ },
193
+
194
+ methods: {
195
+ t: translate,
196
+
197
+ /**
198
+ * A prop text through the host translate.
199
+ *
200
+ * @param {string} value The text.
201
+ * @return {string}
202
+ */
203
+ text(value) {
204
+ return value ? this.cnTranslate(value) : ''
205
+ },
206
+
207
+ /** File it over the warning, carrying the reason and what it was filed over. */
208
+ fileAnyway() {
209
+ if (!this.canFile) {
210
+ return
211
+ }
212
+ /** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */
213
+ this.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) });
214
+ },
215
+
216
+ /** Go back without filing. */
217
+ close() {
218
+ /** @event close The user went back without filing. */
219
+ this.$emit('close');
220
+ },
221
+ },
222
+ };
223
+
224
+ export { script as default };
225
+ //# sourceMappingURL=CnDuplicateWarning.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnDuplicateWarning.vue2.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matchedOn }} ({{ row.percentage }}%)</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/** A record's meta line, a template filled from it. */\n\t\trowMeta: {\n\t\t\ttype: String,\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? fillTemplate(this.rowMeta, record).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatchedOn: fields.length\n\t\t\t\t\t\t? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Matched on the score, not on a single field'),\n\t\t\t\t\tpercentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["CnDialog","n","t"],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import { resolveComponent, openBlock, createBlock, withCtx, createVNode, createTextVNode, toDisplayString, createCommentVNode, createElementVNode, createElementBlock, Fragment, renderList } from 'vue';
2
+
3
+ const _hoisted_1 = { class: "cn-duplicate-warning" };
4
+ const _hoisted_2 = { class: "cn-duplicate-warning__matches" };
5
+ const _hoisted_3 = ["href"];
6
+ const _hoisted_4 = {
7
+ key: 1,
8
+ class: "cn-duplicate-warning__title"
9
+ };
10
+ const _hoisted_5 = {
11
+ key: 2,
12
+ class: "cn-duplicate-warning__meta"
13
+ };
14
+ const _hoisted_6 = { class: "cn-duplicate-warning__meta" };
15
+ const _hoisted_7 = {
16
+ key: 1,
17
+ class: "cn-duplicate-warning__hint",
18
+ "data-testid": "cn-duplicate-warning-blocked"
19
+ };
20
+
21
+ function render(_ctx, _cache, $props, $setup, $data, $options) {
22
+ const _component_NcNoteCard = resolveComponent("NcNoteCard");
23
+ const _component_NcTextField = resolveComponent("NcTextField");
24
+ const _component_NcButton = resolveComponent("NcButton");
25
+ const _component_CnDialog = resolveComponent("CnDialog");
26
+
27
+ return (openBlock(), createBlock(_component_CnDialog, {
28
+ name: $options.text($props.title) || $options.t('nextcloud-vue', 'This may already exist'),
29
+ width: "form",
30
+ "data-testid": "cn-duplicate-warning",
31
+ onClosing: $options.close
32
+ }, {
33
+ actions: withCtx(() => [
34
+ createVNode(_component_NcButton, {
35
+ variant: "tertiary",
36
+ "data-testid": "cn-duplicate-warning-cancel",
37
+ onClick: $options.close
38
+ }, {
39
+ default: withCtx(() => [
40
+ createTextVNode(toDisplayString($options.t('nextcloud-vue', 'Cancel')), 1 /* TEXT */)
41
+ ]),
42
+ _: 1 /* STABLE */
43
+ }, 8 /* PROPS */, ["onClick"]),
44
+ ($options.affordances.mayContinue)
45
+ ? (openBlock(), createBlock(_component_NcButton, {
46
+ key: 0,
47
+ variant: "primary",
48
+ disabled: !$options.canFile,
49
+ "data-testid": "cn-duplicate-warning-continue",
50
+ onClick: $options.fileAnyway
51
+ }, {
52
+ default: withCtx(() => [
53
+ createTextVNode(toDisplayString($options.text($props.continueLabel) || $options.t('nextcloud-vue', 'File it anyway')), 1 /* TEXT */)
54
+ ]),
55
+ _: 1 /* STABLE */
56
+ }, 8 /* PROPS */, ["disabled", "onClick"]))
57
+ : createCommentVNode("v-if", true)
58
+ ]),
59
+ default: withCtx(() => [
60
+ createElementVNode("div", _hoisted_1, [
61
+ createVNode(_component_NcNoteCard, {
62
+ type: $options.affordances.blocked ? 'error' : 'warning',
63
+ "data-testid": "cn-duplicate-warning-note"
64
+ }, {
65
+ default: withCtx(() => [
66
+ createTextVNode(toDisplayString($options.headlineText), 1 /* TEXT */)
67
+ ]),
68
+ _: 1 /* STABLE */
69
+ }, 8 /* PROPS */, ["type"]),
70
+ createElementVNode("ul", _hoisted_2, [
71
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.rows, (row) => {
72
+ return (openBlock(), createElementBlock("li", {
73
+ key: row.uuid,
74
+ class: "cn-duplicate-warning__match",
75
+ "data-testid": "cn-duplicate-warning-match"
76
+ }, [
77
+ (row.href)
78
+ ? (openBlock(), createElementBlock("a", {
79
+ key: 0,
80
+ href: row.href,
81
+ class: "cn-duplicate-warning__title"
82
+ }, toDisplayString(row.title), 9 /* TEXT, PROPS */, _hoisted_3))
83
+ : (openBlock(), createElementBlock("span", _hoisted_4, toDisplayString(row.title), 1 /* TEXT */)),
84
+ (row.meta)
85
+ ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(row.meta), 1 /* TEXT */))
86
+ : createCommentVNode("v-if", true),
87
+ createElementVNode("span", _hoisted_6, toDisplayString(row.matchedOn) + " (" + toDisplayString(row.percentage) + "%)", 1 /* TEXT */)
88
+ ]))
89
+ }), 128 /* KEYED_FRAGMENT */))
90
+ ]),
91
+ ($options.affordances.needsReason)
92
+ ? (openBlock(), createBlock(_component_NcTextField, {
93
+ key: 0,
94
+ label: $options.text($props.reasonLabel) || $options.t('nextcloud-vue', 'Why file it anyway'),
95
+ modelValue: $data.reason,
96
+ "data-testid": "cn-duplicate-warning-reason",
97
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ($data.reason = $event))
98
+ }, null, 8 /* PROPS */, ["label", "modelValue"]))
99
+ : createCommentVNode("v-if", true),
100
+ ($options.affordances.blocked && !$options.affordances.mayContinue)
101
+ ? (openBlock(), createElementBlock("p", _hoisted_7, toDisplayString($options.text($props.blockedText) || $options.t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.')), 1 /* TEXT */))
102
+ : createCommentVNode("v-if", true)
103
+ ])
104
+ ]),
105
+ _: 1 /* STABLE */
106
+ }, 8 /* PROPS */, ["name", "onClosing"]))
107
+ }
108
+
109
+ export { render };
110
+ //# sourceMappingURL=CnDuplicateWarning.vue3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnDuplicateWarning.vue3.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue?vue&type=template&id=7cb31ec5&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matchedOn }} ({{ row.percentage }}%)</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/** A record's meta line, a template filled from it. */\n\t\trowMeta: {\n\t\t\ttype: String,\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? fillTemplate(this.rowMeta, record).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatchedOn: fields.length\n\t\t\t\t\t\t? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Matched on the score, not on a single field'),\n\t\t\t\t\tpercentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["_createBlock","_createVNode","_createElementVNode","_createElementBlock","_Fragment","_renderList","_toDisplayString","_openBlock"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnFormDialog.vue2.js';
2
2
  import { render } from './CnFormDialog.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-3f3ee5db";
5
+ script.__scopeId = "data-v-699550cc";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -39,7 +39,7 @@ import { computeAssignments } from '../../utils/formAssign.js';
39
39
  import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js';
40
40
  import { durationSeconds } from '../../utils/isoDuration.js';
41
41
  import { objectDisplayName } from '../../utils/objectName.js';
42
- import { fieldWithOptionsFrom, loadOptionsFrom, filterOptions, isOptionsFrom } from '../../utils/optionsFrom.js';
42
+ import { fieldWithOptionsFrom, resolveFormTokens, loadOptionsFrom, filterOptions, isOptionsFrom, formTokenKeys } from '../../utils/optionsFrom.js';
43
43
  import { resolveDefaultToken, languageOptions, timezoneOptions } from '../../utils/pickerOptions.js';
44
44
  import { planPropertySourceFill, getDotted } from '../../utils/propertySourceFill.js';
45
45
  import { pruneRows } from '../../utils/pruneRows.js';
@@ -650,6 +650,8 @@ var script = {
650
650
  sourcedOptions: {},
651
651
  // Whether a field's `optionsFrom` failed or gave no options, by field key: it falls back to text.
652
652
  sourcedFailed: {},
653
+ // The resolved source each field's options were read from, by field key.
654
+ sourcedSignatures: {},
653
655
  // Set while a save that passed its uniqueness check goes on.
654
656
  uniqueChecked: false,
655
657
  // The sections folded away (`sectionCollapsed` on one of their fields opens them folded).
@@ -1015,6 +1017,9 @@ var script = {
1015
1017
  visibleFields() {
1016
1018
  return this.resolvedFields
1017
1019
  .filter((field) => shouldShow(field, this.formData))
1020
+ // `optionsFrom.hideWhenEmpty`: a select whose source gives nothing
1021
+ // for the current values is left out (a type without formats).
1022
+ .filter((field) => !(field.optionsFrom && field.optionsFrom.hideWhenEmpty === true && Array.isArray(this.sourcedOptions[field.key]) && this.sourcedOptions[field.key].length === 0))
1018
1023
  // `requiredWhen`: required while its condition holds, so the
1019
1024
  // asterisk, the board look's aria-required and validation follow.
1020
1025
  .map((field) => (field.requiredWhen ? { ...field, required: conditionHolds(field.requiredWhen, this.formData, field.key) } : field))
@@ -1741,13 +1746,18 @@ var script = {
1741
1746
  * @spec openspec/changes/form-field-options-from/specs/form-dialog/spec.md#requirement-a-select-field-can-read-its-options-from-a-source
1742
1747
  */
1743
1748
  async fetchSourcedOptions(field, query) {
1744
- let all = this.sourcedOptions[field.key];
1749
+ // A source that reads another field (`@answer.<key>`) is read again
1750
+ // when that value changes: the cache is kept per resolved source.
1751
+ const from = resolveFormTokens(field.optionsFrom, this.formData);
1752
+ const signature = JSON.stringify(from);
1753
+ let all = this.sourcedSignatures[field.key] === signature ? this.sourcedOptions[field.key] : null;
1745
1754
  if (!all) {
1746
1755
  try {
1747
- all = await loadOptionsFrom(field.optionsFrom, resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext) || {});
1756
+ all = await loadOptionsFrom(from, resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext) || {});
1748
1757
  } catch {
1749
1758
  all = [];
1750
1759
  }
1760
+ this.sourcedSignatures = { ...this.sourcedSignatures, [field.key]: signature };
1751
1761
  this.sourcedOptions = { ...this.sourcedOptions, [field.key]: all };
1752
1762
  this.sourcedFailed = { ...this.sourcedFailed, [field.key]: all.length === 0 };
1753
1763
  const labels = {};
@@ -2019,6 +2029,12 @@ var script = {
2019
2029
  // A hand edit ends this field's own rules; other fields may follow it.
2020
2030
  delete this.assignedFrom[key];
2021
2031
  this.runAssignments([key]);
2032
+ // A select whose source reads this field (`@answer.<key>`) reads it again.
2033
+ for (const field of this.resolvedFields) {
2034
+ if (isOptionsFrom(field.optionsFrom) && formTokenKeys(field.optionsFrom).includes(key)) {
2035
+ this.loadAsyncOptions(field, '');
2036
+ }
2037
+ }
2022
2038
  },
2023
2039
 
2024
2040
  /**