@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,655 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <section class="cn-redaction" data-testid="cn-redaction-editor">
7
+ <header class="cn-redaction__head">
8
+ <div>
9
+ <h1 class="cn-redaction__title">
10
+ {{ heading }}
11
+ </h1>
12
+ <!-- @slot Under the title: a breadcrumb back to the record. -->
13
+ <slot name="crumbs" />
14
+ </div>
15
+ <div v-if="documents.length > 0" class="cn-redaction__buttons">
16
+ <NcButton data-testid="cn-redaction-previous" :disabled="index === 0 || busy" @click="open(index - 1)">
17
+ {{ t('nextcloud-vue', 'Previous document') }}
18
+ </NcButton>
19
+ <NcButton data-testid="cn-redaction-next" :disabled="index >= documents.length - 1 || busy" @click="open(index + 1)">
20
+ {{ t('nextcloud-vue', 'Next document') }}
21
+ </NcButton>
22
+ <NcButton variant="primary"
23
+ data-testid="cn-redaction-record"
24
+ :disabled="!canRecord"
25
+ @click="record">
26
+ {{ t('nextcloud-vue', 'Record redaction') }}
27
+ </NcButton>
28
+ </div>
29
+ </header>
30
+
31
+ <p v-if="loadError"
32
+ class="cn-redaction__error"
33
+ role="alert"
34
+ data-testid="cn-redaction-load-error">
35
+ {{ loadError }}
36
+ </p>
37
+ <p v-else-if="loading" role="status">
38
+ {{ t('nextcloud-vue', 'Loading…') }}
39
+ </p>
40
+ <p v-else-if="documents.length === 0"
41
+ role="status"
42
+ class="cn-redaction__muted"
43
+ data-testid="cn-redaction-empty">
44
+ {{ text(config.emptyText) || t('nextcloud-vue', 'No document here needs redacting.') }}
45
+ </p>
46
+ <template v-else>
47
+ <div class="cn-redaction__bar">
48
+ <span class="cn-redaction__muted" data-testid="cn-redaction-position">
49
+ {{ t('nextcloud-vue', 'Document {index} of {count}', { index: index + 1, count: documents.length }) }}
50
+ <span v-if="current.redactedDocumentRef" class="cn-redaction__pill cn-redaction__pill--accept" data-testid="cn-redaction-done">
51
+ {{ t('nextcloud-vue', 'Redacted copy recorded') }}
52
+ </span>
53
+ </span>
54
+ <template v-if="config.urls && config.urls.upload">
55
+ <NcButton data-testid="cn-redaction-upload" :disabled="busy" @click="$refs.upload.click()">
56
+ {{ t('nextcloud-vue', 'Upload redacted version') }}
57
+ </NcButton>
58
+ <input ref="upload"
59
+ type="file"
60
+ class="hidden-visually"
61
+ data-testid="cn-redaction-upload-input"
62
+ :aria-label="t('nextcloud-vue', 'Upload redacted version')"
63
+ @change="upload($event.target.files)">
64
+ </template>
65
+ </div>
66
+ <p v-if="error"
67
+ class="cn-redaction__error"
68
+ role="alert"
69
+ data-testid="cn-redaction-error">
70
+ {{ error }}
71
+ </p>
72
+ <p v-if="notice" role="status" data-testid="cn-redaction-notice">
73
+ {{ notice }}
74
+ </p>
75
+
76
+ <div class="cn-redaction__columns">
77
+ <div class="cn-redaction__document" data-testid="cn-redaction-text">
78
+ <strong>{{ current.title || t('nextcloud-vue', 'Document without a title') }}</strong>
79
+ <p v-if="detecting" role="status">
80
+ {{ t('nextcloud-vue', 'Finding the passages to redact…') }}
81
+ </p>
82
+ <p v-else-if="documentText === ''" class="cn-redaction__muted">
83
+ {{ t('nextcloud-vue', 'No text was extracted from this document.') }}
84
+ </p>
85
+ <p v-else class="cn-redaction__body">
86
+ <template v-for="(segment, at) in segments" :key="at">
87
+ <span v-if="segment.verdict" :class="`cn-redaction__mark cn-redaction__mark--${segment.verdict}`">{{ segment.text }}</span>
88
+ <template v-else>
89
+ {{ segment.text }}
90
+ </template>
91
+ </template>
92
+ </p>
93
+ </div>
94
+
95
+ <aside class="cn-redaction__proposal">
96
+ <h2>{{ t('nextcloud-vue', 'Proposal') }}</h2>
97
+ <span class="cn-redaction__muted" data-testid="cn-redaction-count">
98
+ {{ n('nextcloud-vue', '%n passage found. Review each one.', '%n passages found. Review each one.', passages.length) }}
99
+ </span>
100
+ <ul class="cn-redaction__list">
101
+ <li v-for="(passage, at) in passages"
102
+ :key="at"
103
+ class="cn-redaction__passage"
104
+ :data-testid="`cn-redaction-passage-${at}`">
105
+ <span class="cn-redaction__line">
106
+ <strong>{{ labelOf(passage.type) }}: "{{ passage.value }}"</strong>
107
+ <span v-if="passage.verdict" :class="`cn-redaction__pill cn-redaction__pill--${passage.verdict}`">{{ verdictLabel(passage.verdict) }}</span>
108
+ </span>
109
+ <span v-if="passage.locked" class="cn-redaction__muted">{{ t('nextcloud-vue', 'Always redacted by rule') }}</span>
110
+ <span v-else-if="grounds" class="cn-redaction__muted">{{ grounds }}</span>
111
+ <div v-if="!passage.locked"
112
+ class="cn-redaction__verdicts"
113
+ role="group"
114
+ :aria-label="t('nextcloud-vue', 'Verdict for {passage}', { passage: passage.value })">
115
+ <NcButton v-for="verdict in VERDICTS"
116
+ :key="verdict"
117
+ size="small"
118
+ :pressed="passage.verdict === verdict"
119
+ :data-testid="`cn-redaction-verdict-${at}-${verdict}`"
120
+ @update:pressed="setVerdict(at, verdict)">
121
+ {{ verdictLabel(verdict) }}
122
+ </NcButton>
123
+ </div>
124
+ <NcSelect v-if="passage.entity.source === 'officer'"
125
+ :modelValue="typeOptions.find((option) => option.id === passage.type) || null"
126
+ :options="typeOptions"
127
+ :inputLabel="t('nextcloud-vue', 'Passage type')"
128
+ :clearable="false"
129
+ :data-testid="`cn-redaction-type-${at}`"
130
+ @update:modelValue="retype(at, $event)" />
131
+ <NcTextField v-if="passage.verdict === 'adjust'"
132
+ :modelValue="passage.value"
133
+ :label="t('nextcloud-vue', 'Passage text')"
134
+ :data-testid="`cn-redaction-value-${at}`"
135
+ @update:modelValue="adjust(at, $event)" />
136
+ </li>
137
+ </ul>
138
+ <NcButton data-testid="cn-redaction-add" :disabled="busy || detecting || Boolean(current.redactedDocumentRef)" @click="addPassage">
139
+ {{ t('nextcloud-vue', 'Add passage') }}
140
+ </NcButton>
141
+ </aside>
142
+ </div>
143
+ </template>
144
+ </section>
145
+ </template>
146
+
147
+ <script>
148
+ import axios from '@nextcloud/axios'
149
+ import { emit } from '@nextcloud/event-bus'
150
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
151
+ import { generateUrl } from '@nextcloud/router'
152
+ import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue'
153
+ import { readPath } from '../../utils/readPath.js'
154
+ import { redactionSegments } from '../../utils/redactionSegments.js'
155
+
156
+ /** The page refresh channel the built-in widgets listen on. */
157
+ const PAGE_REFRESH = 'cn:page:refresh'
158
+
159
+ /** The verdicts a reviewer gives a passage. */
160
+ const VERDICTS = ['accept', 'adjust', 'skip']
161
+
162
+ /**
163
+ * The server's sentence for a failure, or the fallback.
164
+ *
165
+ * @param {unknown} failure The rejection.
166
+ * @param {string} fallback What to say when the server said nothing.
167
+ * @return {string}
168
+ */
169
+ function sentenceOf(failure, fallback) {
170
+ const data = failure && failure.response && failure.response.data
171
+ return String((data && (data.message || data.error)) || fallback)
172
+ }
173
+
174
+ /**
175
+ * CnRedactionEditor — redact the documents of a record, one at a time
176
+ * (board DqLakken): the document text with the passages marked in place,
177
+ * and beside it the proposal, a verdict per passage (agreed, adjust, do not
178
+ * redact), a passage the detection missed added by hand, and Record
179
+ * redaction. It names no procedure: the routes and the passage type labels
180
+ * come from `config`, so dossiq's Woo redaction and filinq's anonymisation
181
+ * use the same editor.
182
+ *
183
+ * The check is the reviewer's: Record redaction is enabled only once every
184
+ * passage has a verdict, and it first records the check of the passages to
185
+ * redact, in the reviewer's name, and only then asks for the redacted copy.
186
+ * A passage marked `locked` (a rule that always applies) starts agreed and
187
+ * cannot be changed. A refusal is shown in the server's words and the
188
+ * upload of a version redacted elsewhere stays offered.
189
+ *
190
+ * `config`:
191
+ * - `urls`: `{ list, detect, check, redact, upload? }`, templates with
192
+ * `{recordId}` and `{documentRef}`. `list` answers `{ documents: [{
193
+ * documentRef, title, redactedDocumentRef?, redaction? }] }`; `detect`
194
+ * answers `{ text, redaction: { entities } }`; `check` receives `{ entities
195
+ * }`; `redact` and `upload` (a `file` form field) answer `{
196
+ * redactedDocumentRef, redaction }`.
197
+ * - `typeLabels`: what a passage type is called (`{ PERSON: "Name" }`).
198
+ * - `groundsPath`: a document's grounds (a list) shown under each passage.
199
+ * - `title`, `emptyText`: texts. All through the host translate.
200
+ */
201
+ export default {
202
+ name: 'CnRedactionEditor',
203
+
204
+ components: { NcButton, NcSelect, NcTextField },
205
+
206
+ inject: {
207
+ /** The host app's translate (CnAppRoot `cnTranslate`). */
208
+ cnTranslate: { default: () => (key) => key },
209
+ },
210
+
211
+ props: {
212
+ /** The record whose documents are redacted, for `{recordId}`. */
213
+ recordId: {
214
+ type: [String, Number],
215
+ default: '',
216
+ },
217
+
218
+ /**
219
+ * The editor's configuration (see the component docs).
220
+ *
221
+ * @type {object}
222
+ */
223
+ config: {
224
+ type: Object,
225
+ default: () => ({}),
226
+ },
227
+ },
228
+
229
+ emits: ['recorded'],
230
+
231
+ data() {
232
+ return {
233
+ VERDICTS,
234
+ loading: true,
235
+ loadError: '',
236
+ documents: [],
237
+ index: 0,
238
+ documentText: '',
239
+ passages: [],
240
+ detecting: false,
241
+ busy: false,
242
+ error: '',
243
+ notice: '',
244
+ }
245
+ },
246
+
247
+ computed: {
248
+ /** @return {object} The open document. */
249
+ current() {
250
+ return this.documents[this.index] || {}
251
+ },
252
+
253
+ /** @return {string} The heading. */
254
+ heading() {
255
+ const own = this.text(this.config.title)
256
+ if (own) {
257
+ return own
258
+ }
259
+ return this.current.title ? t('nextcloud-vue', 'Redact: {title}', { title: this.current.title }) : t('nextcloud-vue', 'Redact documents')
260
+ },
261
+
262
+ /** @return {Array<{id: string, label: string}>} The passage types. */
263
+ typeOptions() {
264
+ return Object.keys(this.config.typeLabels || {}).map((type) => ({ id: type, label: this.labelOf(type) }))
265
+ },
266
+
267
+ /** @return {string} The open document's grounds. */
268
+ grounds() {
269
+ const list = this.config.groundsPath ? readPath(this.current, this.config.groundsPath) : null
270
+ return Array.isArray(list) ? list.join(', ') : ''
271
+ },
272
+
273
+ /** @return {Array<object>} The entities to redact: agreed or adjusted, with text. */
274
+ toRedact() {
275
+ return this.passages
276
+ .filter((p) => p.verdict === 'accept' || p.verdict === 'adjust')
277
+ .map((p) => ({ ...p.entity, value: p.value.trim() }))
278
+ .filter((entity) => entity.value !== '')
279
+ },
280
+
281
+ /**
282
+ * Whether the redaction may be recorded: every passage judged, at least one to redact.
283
+ *
284
+ * @spec openspec/changes/redaction-editor/specs/redaction-editor/spec.md#requirement-a-reviewer-redacts-a-records-documents
285
+ * @return {boolean}
286
+ */
287
+ canRecord() {
288
+ return !this.busy && !this.detecting && !this.current.redactedDocumentRef
289
+ && this.passages.length > 0
290
+ && this.passages.every((p) => p.verdict !== null)
291
+ && this.toRedact.length > 0
292
+ },
293
+
294
+ /** @return {Array<{text: string, verdict: (string|null)}>} The text with the passages marked. */
295
+ segments() {
296
+ return redactionSegments(this.documentText, this.passages)
297
+ },
298
+ },
299
+
300
+ mounted() {
301
+ this.load()
302
+ },
303
+
304
+ methods: {
305
+ t,
306
+ n,
307
+
308
+ /**
309
+ * A config text through the host translate.
310
+ *
311
+ * @param {unknown} value The text.
312
+ * @return {string}
313
+ */
314
+ text(value) {
315
+ return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
316
+ },
317
+
318
+ /**
319
+ * A configured url with the record and the open document filled in.
320
+ *
321
+ * @param {string} key The `urls` key.
322
+ * @return {string}
323
+ */
324
+ urlOf(key) {
325
+ const template = String((this.config.urls || {})[key] || '')
326
+ return generateUrl(template
327
+ .replace('{recordId}', encodeURIComponent(String(this.recordId)))
328
+ .replace('{documentRef}', encodeURIComponent(String(this.current.documentRef || ''))))
329
+ },
330
+
331
+ /**
332
+ * What a passage type is called.
333
+ *
334
+ * @param {string} type The type.
335
+ * @return {string}
336
+ */
337
+ labelOf(type) {
338
+ const label = (this.config.typeLabels || {})[type]
339
+ return label ? this.cnTranslate(String(label)) : String(type || '')
340
+ },
341
+
342
+ /**
343
+ * What a verdict is called.
344
+ *
345
+ * @param {string} verdict The verdict.
346
+ * @return {string}
347
+ */
348
+ verdictLabel(verdict) {
349
+ return ({ accept: t('nextcloud-vue', 'Agreed'), adjust: t('nextcloud-vue', 'Adjust'), skip: t('nextcloud-vue', 'Do not redact') })[verdict] || ''
350
+ },
351
+
352
+ /**
353
+ * Read the documents and open the first.
354
+ *
355
+ * @spec openspec/changes/redaction-editor/specs/redaction-editor/spec.md#requirement-a-reviewer-redacts-a-records-documents
356
+ * @return {Promise<void>}
357
+ */
358
+ async load() {
359
+ this.loading = true
360
+ this.loadError = ''
361
+ try {
362
+ const { data } = await axios.get(this.urlOf('list'))
363
+ this.documents = Array.isArray(data && data.documents) ? data.documents : []
364
+ } catch (failure) {
365
+ this.documents = []
366
+ this.loadError = sentenceOf(failure, t('nextcloud-vue', 'The documents could not be read.'))
367
+ } finally {
368
+ this.loading = false
369
+ }
370
+ if (this.documents.length > 0) {
371
+ await this.open(Math.min(this.index, this.documents.length - 1))
372
+ }
373
+ },
374
+
375
+ /**
376
+ * Open one document: its recorded redaction, or the passages detected in it.
377
+ *
378
+ * @spec openspec/changes/redaction-editor/specs/redaction-editor/spec.md#requirement-a-reviewer-redacts-a-records-documents
379
+ * @param {number} at The document's position.
380
+ * @return {Promise<void>}
381
+ */
382
+ async open(at) {
383
+ this.index = at
384
+ this.error = ''
385
+ this.notice = ''
386
+ this.documentText = ''
387
+ this.passages = []
388
+ if (this.current.redactedDocumentRef) {
389
+ this.passages = this.passagesOf((this.current.redaction && this.current.redaction.entities) || [], 'accept')
390
+ return
391
+ }
392
+ this.detecting = true
393
+ try {
394
+ const { data } = await axios.post(this.urlOf('detect'))
395
+ this.documentText = String((data && data.text) || '')
396
+ this.passages = this.passagesOf((data && data.redaction && data.redaction.entities) || [], null)
397
+ } catch (failure) {
398
+ this.error = sentenceOf(failure, t('nextcloud-vue', 'The passages of this document could not be found. Upload a redacted version instead.'))
399
+ } finally {
400
+ this.detecting = false
401
+ }
402
+ },
403
+
404
+ /**
405
+ * Passages from entities; a locked entity starts agreed.
406
+ *
407
+ * @param {Array<object>} entities The entities.
408
+ * @param {string|null} verdict The verdict the others start with.
409
+ * @return {Array<object>}
410
+ */
411
+ passagesOf(entities, verdict) {
412
+ return entities.map((entity) => ({
413
+ entity,
414
+ type: String((entity && entity.type) || ''),
415
+ value: String((entity && entity.value) || ''),
416
+ locked: Boolean(entity && entity.locked),
417
+ verdict: entity && entity.locked ? 'accept' : verdict,
418
+ }))
419
+ },
420
+
421
+ /**
422
+ * Give a passage a verdict.
423
+ *
424
+ * @param {number} at The passage.
425
+ * @param {string} verdict The verdict.
426
+ */
427
+ setVerdict(at, verdict) {
428
+ if (!this.passages[at].locked) {
429
+ this.passages[at] = { ...this.passages[at], verdict }
430
+ }
431
+ },
432
+
433
+ /**
434
+ * Change an adjusted passage's text.
435
+ *
436
+ * @param {number} at The passage.
437
+ * @param {string} value The text.
438
+ */
439
+ adjust(at, value) {
440
+ this.passages[at] = { ...this.passages[at], value: String(value ?? '') }
441
+ },
442
+
443
+ /**
444
+ * Give a hand-added passage its type.
445
+ *
446
+ * @param {number} at The passage.
447
+ * @param {{id: string}|null} option The type.
448
+ */
449
+ retype(at, option) {
450
+ const type = String((option && option.id) || 'UNKNOWN')
451
+ const passage = this.passages[at]
452
+ this.passages[at] = { ...passage, type, entity: { ...passage.entity, type } }
453
+ },
454
+
455
+ /** Add a passage the detection missed. */
456
+ addPassage() {
457
+ this.passages.push({ entity: { type: 'UNKNOWN', source: 'officer' }, type: 'UNKNOWN', value: '', locked: false, verdict: 'adjust' })
458
+ },
459
+
460
+ /**
461
+ * Record the reviewer's check of the passages, then ask for the redacted copy.
462
+ *
463
+ * @spec openspec/changes/redaction-editor/specs/redaction-editor/spec.md#requirement-a-reviewer-redacts-a-records-documents
464
+ * @return {Promise<void>}
465
+ */
466
+ async record() {
467
+ if (!this.canRecord) {
468
+ return
469
+ }
470
+ this.busy = true
471
+ this.error = ''
472
+ this.notice = ''
473
+ try {
474
+ await axios.post(this.urlOf('check'), { entities: this.toRedact })
475
+ const { data } = await axios.post(this.urlOf('redact'))
476
+ this.recorded(data)
477
+ } catch (failure) {
478
+ this.error = sentenceOf(failure, t('nextcloud-vue', 'The redacted copy could not be made. Upload a redacted version instead.'))
479
+ } finally {
480
+ this.busy = false
481
+ }
482
+ },
483
+
484
+ /**
485
+ * Upload a version redacted elsewhere.
486
+ *
487
+ * @param {FileList|null} files The chosen file.
488
+ * @return {Promise<void>}
489
+ */
490
+ async upload(files) {
491
+ const file = files && files[0]
492
+ if (!file) {
493
+ return
494
+ }
495
+ const form = new FormData()
496
+ form.append('file', file)
497
+ this.busy = true
498
+ this.error = ''
499
+ this.notice = ''
500
+ try {
501
+ const { data } = await axios.post(this.urlOf('upload'), form)
502
+ this.recorded(data)
503
+ } catch (failure) {
504
+ this.error = sentenceOf(failure, t('nextcloud-vue', 'The redacted version could not be stored. Try again.'))
505
+ } finally {
506
+ this.busy = false
507
+ if (this.$refs.upload) {
508
+ this.$refs.upload.value = ''
509
+ }
510
+ }
511
+ },
512
+
513
+ /**
514
+ * Show the recorded copy and refresh the page.
515
+ *
516
+ * @param {object} data The server's answer.
517
+ */
518
+ recorded(data) {
519
+ this.documents[this.index] = { ...this.current, redactedDocumentRef: String((data && data.redactedDocumentRef) || ''), redaction: (data && data.redaction) || {} }
520
+ this.notice = t('nextcloud-vue', 'The redacted copy is recorded. Only this copy will be published.')
521
+ emit(PAGE_REFRESH, { reason: 'redaction' })
522
+ /** @event recorded A redacted copy was recorded. Payload: `{ documentRef, redactedDocumentRef }`. */
523
+ this.$emit('recorded', { documentRef: this.current.documentRef, redactedDocumentRef: this.current.redactedDocumentRef })
524
+ },
525
+ },
526
+ }
527
+ </script>
528
+
529
+ <style scoped>
530
+ .cn-redaction {
531
+ display: flex;
532
+ flex-direction: column;
533
+ gap: 16px;
534
+ }
535
+
536
+ .cn-redaction__head,
537
+ .cn-redaction__bar {
538
+ display: flex;
539
+ flex-wrap: wrap;
540
+ align-items: center;
541
+ justify-content: space-between;
542
+ gap: 12px;
543
+ }
544
+
545
+ .cn-redaction__title {
546
+ margin: 0;
547
+ font-size: 28px;
548
+ font-weight: 700;
549
+ }
550
+
551
+ .cn-redaction__buttons,
552
+ .cn-redaction__verdicts {
553
+ display: flex;
554
+ flex-wrap: wrap;
555
+ gap: 8px;
556
+ }
557
+
558
+ .cn-redaction__muted {
559
+ color: var(--color-text-maxcontrast);
560
+ }
561
+
562
+ .cn-redaction__error {
563
+ margin: 0;
564
+ color: var(--color-error-text, var(--color-error));
565
+ }
566
+
567
+ .cn-redaction__columns {
568
+ display: flex;
569
+ flex-wrap: wrap;
570
+ align-items: flex-start;
571
+ gap: 20px;
572
+ }
573
+
574
+ .cn-redaction__document,
575
+ .cn-redaction__proposal {
576
+ display: flex;
577
+ flex-direction: column;
578
+ min-width: 0;
579
+ border: 1px solid var(--color-border);
580
+ border-radius: var(--border-radius-large, 8px);
581
+ background: var(--color-main-background);
582
+ }
583
+
584
+ .cn-redaction__document {
585
+ flex: 999 1 520px;
586
+ gap: 12px;
587
+ padding: 28px 32px;
588
+ }
589
+
590
+ .cn-redaction__proposal {
591
+ flex: 1 1 300px;
592
+ gap: 10px;
593
+ padding: 20px 22px;
594
+ }
595
+
596
+ .cn-redaction__body {
597
+ white-space: pre-wrap;
598
+ line-height: 1.6;
599
+ }
600
+
601
+ .cn-redaction__mark--accept {
602
+ background: var(--color-main-text);
603
+ color: var(--color-main-text);
604
+ }
605
+
606
+ .cn-redaction__mark--adjust,
607
+ .cn-redaction__mark--open {
608
+ outline: 2px solid var(--color-warning-text, var(--color-warning));
609
+ background: var(--color-background-hover);
610
+ }
611
+
612
+ .cn-redaction__mark--skip {
613
+ text-decoration: underline dotted;
614
+ }
615
+
616
+ .cn-redaction__list {
617
+ display: flex;
618
+ flex-direction: column;
619
+ gap: 14px;
620
+ margin: 0;
621
+ padding: 0;
622
+ list-style: none;
623
+ }
624
+
625
+ .cn-redaction__passage {
626
+ display: flex;
627
+ flex-direction: column;
628
+ gap: 6px;
629
+ }
630
+
631
+ .cn-redaction__line {
632
+ display: flex;
633
+ flex-wrap: wrap;
634
+ align-items: center;
635
+ gap: 8px;
636
+ }
637
+
638
+ .cn-redaction__pill {
639
+ display: inline-block;
640
+ padding: 3px 10px;
641
+ border-radius: 12px;
642
+ background: var(--color-background-dark);
643
+ font-size: 13px;
644
+ font-weight: 600;
645
+ white-space: nowrap;
646
+ }
647
+
648
+ .cn-redaction__pill--accept {
649
+ color: var(--color-success-text, var(--color-success));
650
+ }
651
+
652
+ .cn-redaction__pill--adjust {
653
+ color: var(--color-warning-text, var(--color-warning));
654
+ }
655
+ </style>
@@ -0,0 +1,13 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnRedactionEditor — redact a record's documents: the text with the
6
+ * passages marked and a verdict per passage, then the reviewer's check and
7
+ * the redacted copy.
8
+ */
9
+
10
+ import CnRedactionEditor from './CnRedactionEditor.vue'
11
+
12
+ export { CnRedactionEditor }
13
+ export default CnRedactionEditor