@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.
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +115 -12
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +36 -9
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js +10 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js +305 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue2.js.map +1 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js +38 -0
- package/dist/esm/components/CnAddressSearch/CnAddressSearch.vue3.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +10 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +398 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +155 -0
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js +9 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js +281 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js +137 -0
- package/dist/esm/components/CnDecisionListWidget/CnDecisionListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +9 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +225 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +110 -0
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +19 -3
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +513 -481
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +9 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +181 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js +50 -0
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +102 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -6
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js +190 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js +91 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js +9 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js +64 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue2.js.map +1 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js +49 -0
- package/dist/esm/components/CnPlanWidget/CnPlanWidgetRow.vue3.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js +9 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js +382 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js +285 -0
- package/dist/esm/components/CnRedactionEditor/CnRedactionEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +9 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js +277 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js +183 -0
- package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +53 -22
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js +9 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js +253 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js +197 -0
- package/dist/esm/components/CnWorkQueue/CnWorkQueue.vue3.js.map +1 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +106 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +105 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +510 -76
- package/dist/esm/utils/actionsDispatcher.js +11 -3
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/builtInModals.js +3 -1
- package/dist/esm/utils/builtInModals.js.map +1 -1
- package/dist/esm/utils/duplicateWarning.js +62 -0
- package/dist/esm/utils/duplicateWarning.js.map +1 -0
- package/dist/esm/utils/errors.js +4 -0
- package/dist/esm/utils/errors.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/openDuplicateWarning.js +39 -0
- package/dist/esm/utils/openDuplicateWarning.js.map +1 -0
- package/dist/esm/utils/optionsFrom.js +69 -2
- package/dist/esm/utils/optionsFrom.js.map +1 -1
- package/dist/esm/utils/planTree.js +57 -0
- package/dist/esm/utils/planTree.js.map +1 -0
- package/dist/esm/utils/redactionSegments.js +53 -0
- package/dist/esm/utils/redactionSegments.js.map +1 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js +61 -0
- package/dist/esm/utils/registerNcDashboardWidgets.js.map +1 -0
- package/dist/esm/utils/searchRefusal.js +62 -0
- package/dist/esm/utils/searchRefusal.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17560 -12844
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +510 -76
- package/l10n/en.json +106 -1
- package/l10n/nl.json +105 -1
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +134 -13
- package/src/components/CnAddressSearch/CnAddressSearch.vue +343 -0
- package/src/components/CnAddressSearch/index.js +12 -0
- package/src/components/CnAssignDialog/CnAssignDialog.vue +525 -0
- package/src/components/CnAssignDialog/index.js +12 -0
- package/src/components/CnDecisionListWidget/CnDecisionListWidget.vue +388 -0
- package/src/components/CnDecisionListWidget/index.js +12 -0
- package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +318 -0
- package/src/components/CnDuplicateWarning/index.js +12 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +19 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +52 -0
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +218 -0
- package/src/components/CnNcDashboardList/index.js +12 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +107 -6
- package/src/components/CnPlanWidget/CnPlanWidget.vue +246 -0
- package/src/components/CnPlanWidget/CnPlanWidgetRow.vue +117 -0
- package/src/components/CnPlanWidget/index.js +12 -0
- package/src/components/CnRedactionEditor/CnRedactionEditor.vue +655 -0
- package/src/components/CnRedactionEditor/index.js +13 -0
- package/src/components/CnRetentionWidget/CnRetentionWidget.vue +413 -0
- package/src/components/CnRetentionWidget/index.js +12 -0
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +31 -0
- package/src/components/CnWorkQueue/CnWorkQueue.vue +463 -0
- package/src/components/CnWorkQueue/index.js +12 -0
- package/src/components/index.js +9 -0
- package/src/css/index-page.css +24 -0
- package/src/index.js +12 -0
- package/src/schemas/app-manifest-v2.schema.json +119 -3
- package/src/utils/actionsDispatcher.js +10 -2
- package/src/utils/builtInModals.js +3 -1
- package/src/utils/duplicateWarning.js +59 -0
- package/src/utils/errors.js +4 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/openDuplicateWarning.js +35 -0
- package/src/utils/optionsFrom.js +68 -1
- package/src/utils/planTree.js +54 -0
- package/src/utils/redactionSegments.js +50 -0
- package/src/utils/registerNcDashboardWidgets.js +57 -0
- package/src/utils/searchRefusal.js +59 -0
- 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
|