@conduction/nextcloud-vue 2.147.0 → 2.149.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/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +24 -7
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +12 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +23 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +71 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +64 -8
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -1
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +14 -4
- package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +32 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +10 -2
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +84 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +61 -33
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +49 -2
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +90 -59
- package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +10 -0
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +11 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +28 -21
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +8 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -15
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +123 -115
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +4 -0
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
- package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +3 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +26 -15
- package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -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 +785 -518
- package/dist/esm/utils/builtInFormatters.js +53 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- 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 +2688 -2154
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +785 -518
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +24 -7
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +13 -1
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
- package/src/components/CnDataTable/CnDataTable.vue +34 -1
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +73 -10
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +6 -1
- package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +1 -0
- package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +74 -10
- package/src/components/CnFactsWidget/CnFactsWidget.vue +41 -4
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +131 -3
- package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +103 -5
- package/src/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue +3 -1
- package/src/components/CnNotesCard/CnNotesCard.vue +10 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +16 -0
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
- package/src/components/CnStepper/CnStepper.vue +10 -2
- package/src/css/dialog.css +22 -2
- package/src/css/look-board-detail.css +52 -3
- package/src/css/look-board-index.css +51 -1
- package/src/css/look-board.css +48 -5
- package/src/css/table.css +5 -2
- package/src/dialogs/CnAccessPurposeDialog.vue +27 -12
- package/src/schemas/app-manifest-v2.schema.json +4 -2
- package/src/utils/builtInFormatters.js +52 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json
CHANGED
|
@@ -2397,7 +2397,9 @@
|
|
|
2397
2397
|
"From {date}": "From {date}",
|
|
2398
2398
|
"Until {date}": "Until {date}",
|
|
2399
2399
|
"More actions for {name}": "More actions for {name}",
|
|
2400
|
-
"Unlink contact": "Unlink contact"
|
|
2400
|
+
"Unlink contact": "Unlink contact",
|
|
2401
|
+
"{term}, extendable by {extension}": "{term}, extendable by {extension}",
|
|
2402
|
+
"Move {item}, use the arrow keys": "Move {item}, use the arrow keys"
|
|
2401
2403
|
},
|
|
2402
2404
|
"plurals": {
|
|
2403
2405
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2652,7 +2652,9 @@
|
|
|
2652
2652
|
"From {date}": "Vanaf {date}",
|
|
2653
2653
|
"Until {date}": "Tot {date}",
|
|
2654
2654
|
"More actions for {name}": "Meer acties voor {name}",
|
|
2655
|
-
"Unlink contact": "Contact ontkoppelen"
|
|
2655
|
+
"Unlink contact": "Contact ontkoppelen",
|
|
2656
|
+
"{term}, extendable by {extension}": "{term}, te verdagen met {extension}",
|
|
2657
|
+
"Move {item}, use the arrow keys": "{item} verplaatsen, gebruik de pijltjestoetsen"
|
|
2656
2658
|
},
|
|
2657
2659
|
"plurals": {
|
|
2658
2660
|
"{count} member": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.149.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -184,6 +184,14 @@ export default {
|
|
|
184
184
|
* @spec openspec/changes/audit-trail-exclude-actions/specs/audit-trail/spec.md#requirement-a-history-card-can-leave-out-events-by-action
|
|
185
185
|
*/
|
|
186
186
|
excludeActions: { type: Array, default: () => [] },
|
|
187
|
+
/**
|
|
188
|
+
* Actors the card leaves out by user id (case-insensitive), such as
|
|
189
|
+
* `['System']` for a history of what people did: the bookkeeping a
|
|
190
|
+
* background job writes is not shown.
|
|
191
|
+
*
|
|
192
|
+
* @spec openspec/changes/audit-card-exclude-actors/specs/audit-trail/spec.md#requirement-a-history-card-can-leave-out-entries-by-actor
|
|
193
|
+
*/
|
|
194
|
+
excludeActors: { type: Array, default: () => [] },
|
|
187
195
|
/** Whether the card collapses. */
|
|
188
196
|
collapsible: { type: Boolean, default: false },
|
|
189
197
|
/** Override the card title (defaults to the translated label). */
|
|
@@ -199,7 +207,8 @@ export default {
|
|
|
199
207
|
/**
|
|
200
208
|
* What happened, in words, by action (`claim`) or by action and the
|
|
201
209
|
* state it led to (`complete:completed`, tried first). `{actor}`,
|
|
202
|
-
* `{state}` and `{changed.<key>}` (a value the entry recorded,
|
|
210
|
+
* `{state}` and `{changed.<key>}` (a value the entry recorded, a dotted key
|
|
211
|
+
* reading into it such as `{changed.data.fileName}`, in the
|
|
203
212
|
* words of `valueLabels`) are filled in; a label that names the actor leaves it off
|
|
204
213
|
* the line under it. An action without a label shows as written.
|
|
205
214
|
*
|
|
@@ -262,10 +271,17 @@ export default {
|
|
|
262
271
|
/** @return {Array<object>} The entries without the excluded actions. */
|
|
263
272
|
shownEntries() {
|
|
264
273
|
const excluded = this.excludeActions.map((action) => String(action).toLowerCase())
|
|
265
|
-
|
|
274
|
+
const actors = this.excludeActors.map((actor) => String(actor).toLowerCase())
|
|
275
|
+
if (excluded.length === 0 && actors.length === 0) {
|
|
266
276
|
return this.entries
|
|
267
277
|
}
|
|
268
|
-
return this.entries.filter((entry) => !excluded.includes(String(entry.action || entry.event || '').toLowerCase())
|
|
278
|
+
return this.entries.filter((entry) => !excluded.includes(String(entry.action || entry.event || '').toLowerCase())
|
|
279
|
+
&& !actors.includes(String(entry.user || '').toLowerCase()))
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/** @return {boolean} Whether the card leaves entries out, and so reads more than it shows. */
|
|
283
|
+
filtersEntries() {
|
|
284
|
+
return this.excludeActions.length > 0 || this.excludeActors.length > 0
|
|
269
285
|
},
|
|
270
286
|
|
|
271
287
|
displayedEntries() {
|
|
@@ -301,7 +317,7 @@ export default {
|
|
|
301
317
|
if (this.sourceUrl !== null) {
|
|
302
318
|
return this.sourceUrl
|
|
303
319
|
}
|
|
304
|
-
const limit = this.
|
|
320
|
+
const limit = this.filtersEntries ? Math.min(100, this.maxDisplay * 10) : this.maxDisplay
|
|
305
321
|
const params = new URLSearchParams({ limit: String(limit) })
|
|
306
322
|
if (this.scope === 'app') {
|
|
307
323
|
if (this.register) {
|
|
@@ -381,7 +397,7 @@ export default {
|
|
|
381
397
|
if (this.scope !== 'object' || this.sourceUrl !== null || !this.objectId || !Array.isArray(this.relatedSources)) {
|
|
382
398
|
return []
|
|
383
399
|
}
|
|
384
|
-
const limit = this.
|
|
400
|
+
const limit = this.filtersEntries ? Math.min(100, this.maxDisplay * 10) : this.maxDisplay
|
|
385
401
|
const read = async (url) => {
|
|
386
402
|
const response = await fetch(prefixUrl(url), { headers: buildHeaders() })
|
|
387
403
|
if (!response.ok) {
|
|
@@ -423,7 +439,7 @@ export default {
|
|
|
423
439
|
return label
|
|
424
440
|
.replace('{actor}', this.actorName(entry))
|
|
425
441
|
.replace('{state}', entry.stateAfter || '')
|
|
426
|
-
.replace(/\{changed\.([\w
|
|
442
|
+
.replace(/\{changed\.([\w.-]+)\}/g, (match, key) => this.changedValue(entry, key))
|
|
427
443
|
.replace(/\{object\.([\w.-]+)\}/g, (match, path) => {
|
|
428
444
|
const value = this.object ? readPath(this.object, path) : undefined
|
|
429
445
|
return value === null || value === undefined || typeof value === 'object' ? '' : String(value)
|
|
@@ -444,7 +460,8 @@ export default {
|
|
|
444
460
|
*/
|
|
445
461
|
changedValue(entry, key) {
|
|
446
462
|
const changed = entry && typeof entry.changed === 'object' && entry.changed !== null ? entry.changed : {}
|
|
447
|
-
|
|
463
|
+
// A dotted key reads into what the entry recorded (`data.fileName` on a file action).
|
|
464
|
+
const value = String(key).split('.').reduce((at, part) => newValueOf(at !== null && typeof at === 'object' ? at[part] : undefined), changed)
|
|
448
465
|
if (value === null || value === undefined || typeof value === 'object') {
|
|
449
466
|
return ''
|
|
450
467
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
:maxDisplay="resolvedMaxDisplay"
|
|
19
19
|
:layout="content && typeof content.layout === 'string' ? content.layout : ''"
|
|
20
20
|
:excludeActions="resolvedExcludeActions"
|
|
21
|
+
:excludeActors="resolvedExcludeActors"
|
|
21
22
|
:actionLabels="actionLabels"
|
|
22
23
|
:valueLabels="valueLabels"
|
|
23
24
|
:changeLabels="changeLabels"
|
|
@@ -105,7 +106,7 @@ export default {
|
|
|
105
106
|
excludeActions: { type: Array, default: () => [] },
|
|
106
107
|
/** Stored widget content blob (CnDashboardPage registry branch): `{ title, maxDisplay, register?, schema?, objectId?, source?, endpoint?, emptyText?, excludeActions?, actionLabels?, footnote? }`. */
|
|
107
108
|
/** Stored widget content blob (CnDashboardPage registry branch): `{ title, maxDisplay, register?, schema?, objectId?, source?, endpoint?, emptyText?, excludeActions?, actionLabels?, valueLabels? }`. */
|
|
108
|
-
/** Stored widget content blob (CnDashboardPage registry branch): `{ title, maxDisplay, register?, schema?, objectId?, source?, endpoint?, emptyText?, excludeActions?, actionLabels?, valueLabels?, relatedSources? }`. */
|
|
109
|
+
/** Stored widget content blob (CnDashboardPage registry branch): `{ title, maxDisplay, register?, schema?, objectId?, source?, endpoint?, emptyText?, excludeActions?, actionLabels?, valueLabels?, relatedSources?, excludeActors? }`. */
|
|
109
110
|
content: { type: Object, default: () => ({}) },
|
|
110
111
|
},
|
|
111
112
|
|
|
@@ -279,6 +280,17 @@ export default {
|
|
|
279
280
|
return Array.isArray(fromContent) ? fromContent.filter((action) => typeof action === 'string' && action !== '') : []
|
|
280
281
|
},
|
|
281
282
|
|
|
283
|
+
/**
|
|
284
|
+
* The actors to leave out, from `content.excludeActors`.
|
|
285
|
+
*
|
|
286
|
+
* @spec openspec/changes/audit-card-exclude-actors/specs/audit-trail/spec.md#requirement-a-history-card-can-leave-out-entries-by-actor
|
|
287
|
+
* @return {Array<string>}
|
|
288
|
+
*/
|
|
289
|
+
resolvedExcludeActors() {
|
|
290
|
+
const fromContent = this.content.excludeActors
|
|
291
|
+
return Array.isArray(fromContent) ? fromContent.filter((actor) => typeof actor === 'string' && actor !== '') : []
|
|
292
|
+
},
|
|
293
|
+
|
|
282
294
|
resolvedTitle() {
|
|
283
295
|
return this.title || this.content.title || ''
|
|
284
296
|
},
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
:required="question.required"
|
|
95
95
|
data-testid="cn-checklist-run-text"
|
|
96
96
|
@input="markDirty(question.id)" />
|
|
97
|
-
<label v-if="allowNote" class="cn-checklist-run__note-label">
|
|
97
|
+
<label v-if="allowNote && noteShown(question)" class="cn-checklist-run__note-label">
|
|
98
98
|
{{ noteLabel }}
|
|
99
99
|
<textarea
|
|
100
100
|
v-model="draft[question.id].note"
|
|
@@ -288,6 +288,11 @@ function idOf(object) {
|
|
|
288
288
|
* with no bound record, `record: { register, schema, filter, sort }` picks
|
|
289
289
|
* the open record: the first match, under the reader's rights.
|
|
290
290
|
*
|
|
291
|
+
* `noteWhen: { answers: [...] }` shows a question's note field only once its
|
|
292
|
+
* answer is one of those stored values (the board shows a note under "Nee"),
|
|
293
|
+
* or when the question already holds a note; without it every question shows
|
|
294
|
+
* one (`allowNote: false` hides them all).
|
|
295
|
+
*
|
|
291
296
|
* `startAt: "firstOpen"` opens a run that was started earlier at the page of
|
|
292
297
|
* its first unanswered question (the last page when all are answered), once
|
|
293
298
|
* per record; without it the run opens at the first page.
|
|
@@ -321,7 +326,7 @@ export default {
|
|
|
321
326
|
* The widget config: `{ questions | questionsPath, itemsKey?,
|
|
322
327
|
* groupLabelField?, orderField?, fields?, typeMap?, choiceValues?,
|
|
323
328
|
* answers | answersPath, answerKey?, answerFields?, recordPhotosField?,
|
|
324
|
-
* record?, saveAction?, finishAction?, pageSize?, allowNote?,
|
|
329
|
+
* record?, saveAction?, finishAction?, pageSize?, allowNote?, noteWhen?,
|
|
325
330
|
* allowPhotos?, caption?, emptyText? }`, as in the component
|
|
326
331
|
* description and docs/components/cn-checklist-run-widget.md.
|
|
327
332
|
*
|
|
@@ -612,6 +617,23 @@ export default {
|
|
|
612
617
|
},
|
|
613
618
|
|
|
614
619
|
methods: {
|
|
620
|
+
/**
|
|
621
|
+
* Whether a question shows its note field: always, unless `noteWhen`
|
|
622
|
+
* names the answers that ask for one and this question has none of
|
|
623
|
+
* them and no note yet.
|
|
624
|
+
*
|
|
625
|
+
* @param {object} question The question.
|
|
626
|
+
* @return {boolean}
|
|
627
|
+
*/
|
|
628
|
+
noteShown(question) {
|
|
629
|
+
const answers = this.content?.noteWhen?.answers
|
|
630
|
+
if (!Array.isArray(answers)) {
|
|
631
|
+
return true
|
|
632
|
+
}
|
|
633
|
+
const draft = this.draft[question.id] || {}
|
|
634
|
+
return (draft.note || '') !== '' || answers.map(String).includes(String(draft.value ?? ''))
|
|
635
|
+
},
|
|
636
|
+
|
|
615
637
|
/**
|
|
616
638
|
* The host's translate, or the text unchanged.
|
|
617
639
|
*
|
|
@@ -1194,6 +1216,11 @@ export default {
|
|
|
1194
1216
|
padding: 12px 14px;
|
|
1195
1217
|
}
|
|
1196
1218
|
|
|
1219
|
+
.cn-checklist-run__photos-title {
|
|
1220
|
+
font-size: 15px;
|
|
1221
|
+
line-height: 1.2;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1197
1224
|
.cn-checklist-run__legend {
|
|
1198
1225
|
font-size: 15px;
|
|
1199
1226
|
font-weight: 600;
|
|
@@ -639,6 +639,8 @@ export default {
|
|
|
639
639
|
* `link: true` makes that column the one that names the row: it carries
|
|
640
640
|
* the row link and the title look. Without it the first column does.
|
|
641
641
|
*
|
|
642
|
+
* `emptyValueBy` (`{ field, values }`) names what an empty cell holds by
|
|
643
|
+
* another field of the row, translated; it goes before `emptyValue`.
|
|
642
644
|
* `emptyValue` is what a row without a value holds in that column, so a
|
|
643
645
|
* `badge` column draws a pill on every row.
|
|
644
646
|
*
|
|
@@ -2238,13 +2240,44 @@ export default {
|
|
|
2238
2240
|
return v
|
|
2239
2241
|
}
|
|
2240
2242
|
const value = this.getCellValue(row, this.columnValueField(col))
|
|
2243
|
+
const empty = value === null || value === undefined || value === ''
|
|
2244
|
+
// `emptyValueBy`: what an empty cell means depends on another field
|
|
2245
|
+
// of the row (a retention period left empty on a result that is
|
|
2246
|
+
// kept reads "Permanent", on one that is destroyed it stays empty).
|
|
2247
|
+
if (empty && col && col.emptyValueBy && typeof col.emptyValueBy === 'object') {
|
|
2248
|
+
const words = this.emptyValueFor(row, col.emptyValueBy)
|
|
2249
|
+
if (words !== '') {
|
|
2250
|
+
return words
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2241
2253
|
// `emptyValue`: what a row without a value holds, so a badge column draws one for every row.
|
|
2242
|
-
if (
|
|
2254
|
+
if (empty && col && Object.hasOwn(col, 'emptyValue')) {
|
|
2243
2255
|
return col.emptyValue
|
|
2244
2256
|
}
|
|
2245
2257
|
return value
|
|
2246
2258
|
},
|
|
2247
2259
|
|
|
2260
|
+
/**
|
|
2261
|
+
* The words an empty cell shows, picked by another field of the row:
|
|
2262
|
+
* `{ field, values: { <that field's value>: <words> } }`, translated.
|
|
2263
|
+
*
|
|
2264
|
+
* @param {object} row The row data.
|
|
2265
|
+
* @param {{field: string, values: object}} spec The column's `emptyValueBy`.
|
|
2266
|
+
* @return {string} The words, '' when the row's value has none.
|
|
2267
|
+
*/
|
|
2268
|
+
emptyValueFor(row, spec) {
|
|
2269
|
+
const key = this.getCellValue(row, spec.field)
|
|
2270
|
+
const values = spec.values && typeof spec.values === 'object' ? spec.values : {}
|
|
2271
|
+
if (key === null || key === undefined || !Object.hasOwn(values, String(key))) {
|
|
2272
|
+
return ''
|
|
2273
|
+
}
|
|
2274
|
+
const words = values[String(key)]
|
|
2275
|
+
if (typeof words !== 'string' || words === '') {
|
|
2276
|
+
return ''
|
|
2277
|
+
}
|
|
2278
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(words) : words
|
|
2279
|
+
},
|
|
2280
|
+
|
|
2248
2281
|
/**
|
|
2249
2282
|
* Whether an aggregate cell shows its `zeroLabel` as muted text instead
|
|
2250
2283
|
* of through the column's widget (`aggregate.zeroAs: "text"`): a count
|
|
@@ -438,7 +438,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
438
438
|
| `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
|
|
439
439
|
| `tabsFrom` | String | `''` | A field path, read like a `visibleWhen` field, whose list of tab ids picks the tabs of the page's `tabs` widgets and their order for this record: `@context.caseType.tabs` reads the list a case type keeps (DqAanbesteding: Inschrijvingen, Publicaties, Bestanden, Historie). A tab's id is its `id`, else its `widgetId`, or the string entry. An empty or missing list keeps the declared tabs; edit mode shows them all. |
|
|
440
440
|
| `verbEndpoints` | Object | `null` | App routes that replace a record source's own route for a verb, by verb id (manifest `config.verbEndpoints`), such as `{ "complete": "/apps/dossiq/api/case-tasks/{id}/complete" }` for an app that checks something of its own before the engine completes the task. `{id}` is the record id. Only read with `entitySource`. |
|
|
441
|
-
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }
|
|
441
|
+
| `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, checkUrl?, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label, hint? }`, a `hint` being a line under the option that says what choosing it does; "Other" with a written purpose is always added); `checkUrl` is asked first (GET), and an answer `{ required: false }` opens the record without the dialog, because the app recorded the purpose itself; `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
|
|
442
442
|
| `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
|
|
443
443
|
|
|
444
444
|
## A body widget's own cards in the side column
|
|
@@ -772,7 +772,7 @@
|
|
|
772
772
|
the reader has stated why they open it and the purpose is recorded. -->
|
|
773
773
|
<div v-else-if="contentWithheld" class="cn-detail-page__access-purpose" data-testid="cn-detail-page-access-purpose">
|
|
774
774
|
<CnAccessPurposeDialog
|
|
775
|
-
v-if="accessPurposeAsking"
|
|
775
|
+
v-if="accessPurposeAsking && !accessPurposeChecking"
|
|
776
776
|
:title="accessPurposeText('title')"
|
|
777
777
|
:description="accessPurposeText('description')"
|
|
778
778
|
:options="accessPurposeOptions"
|
|
@@ -781,7 +781,7 @@
|
|
|
781
781
|
@submit="recordAccessPurpose"
|
|
782
782
|
@cancel="accessPurposeAsking = false" />
|
|
783
783
|
<NcEmptyContent
|
|
784
|
-
v-else-if="accessPurposeRequired"
|
|
784
|
+
v-else-if="accessPurposeRequired && !accessPurposeChecking"
|
|
785
785
|
:name="t('nextcloud-vue', 'State a purpose to open this')"
|
|
786
786
|
:description="t('nextcloud-vue', 'This record holds special categories of personal data. It opens once your purpose is recorded.')"
|
|
787
787
|
data-testid="cn-detail-page-access-purpose-refused">
|
|
@@ -3095,6 +3095,12 @@ export default {
|
|
|
3095
3095
|
* browser session. A refusal keeps the record closed. `null` (the
|
|
3096
3096
|
* default) asks nothing.
|
|
3097
3097
|
*
|
|
3098
|
+
* `checkUrl` (GET, `{objectId}` filled in) is asked first: an answer
|
|
3099
|
+
* `{ required: false }` opens the record without the dialog (the app
|
|
3100
|
+
* recorded the purpose itself, as for the case's own handler); any
|
|
3101
|
+
* other answer, or a failure, shows the dialog. An option's `hint`
|
|
3102
|
+
* is a line under it saying what choosing it does besides recording it.
|
|
3103
|
+
*
|
|
3098
3104
|
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
3099
3105
|
* @type {object|null}
|
|
3100
3106
|
*/
|
|
@@ -3413,6 +3419,10 @@ export default {
|
|
|
3413
3419
|
accessPurposeGranted: readGrantedPurposes(),
|
|
3414
3420
|
/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
|
|
3415
3421
|
accessPurposeAsking: true,
|
|
3422
|
+
/** Whether `accessPurpose.checkUrl` is being asked for the record in view. */
|
|
3423
|
+
accessPurposeChecking: false,
|
|
3424
|
+
/** The record the check last answered for, so it is asked once per record. */
|
|
3425
|
+
accessPurposeCheckedKey: '',
|
|
3416
3426
|
/** Whether a purpose is being recorded. */
|
|
3417
3427
|
accessPurposeBusy: false,
|
|
3418
3428
|
/** Why the last purpose was refused. */
|
|
@@ -4129,7 +4139,11 @@ export default {
|
|
|
4129
4139
|
const options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : []
|
|
4130
4140
|
return options
|
|
4131
4141
|
.filter((option) => option && typeof option.value === 'string' && option.value !== '')
|
|
4132
|
-
.map((option) => ({
|
|
4142
|
+
.map((option) => ({
|
|
4143
|
+
value: option.value,
|
|
4144
|
+
label: this.cnTranslate(option.label || option.value),
|
|
4145
|
+
...(typeof option.hint === 'string' && option.hint !== '' ? { hint: this.cnTranslate(option.hint) } : {}),
|
|
4146
|
+
}))
|
|
4133
4147
|
},
|
|
4134
4148
|
|
|
4135
4149
|
/**
|
|
@@ -5832,6 +5846,17 @@ export default {
|
|
|
5832
5846
|
this.accessPurposeError = ''
|
|
5833
5847
|
},
|
|
5834
5848
|
|
|
5849
|
+
/** Once the page knows the record asks a purpose, the app is asked first whether it must (`checkUrl`). */
|
|
5850
|
+
accessPurposeRequired: {
|
|
5851
|
+
handler(required) {
|
|
5852
|
+
if (required && this.contentWithheld) {
|
|
5853
|
+
this.checkAccessPurpose()
|
|
5854
|
+
}
|
|
5855
|
+
},
|
|
5856
|
+
|
|
5857
|
+
immediate: true,
|
|
5858
|
+
},
|
|
5859
|
+
|
|
5835
5860
|
// Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors
|
|
5836
5861
|
// mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer
|
|
5837
5862
|
// forwards never changes. A shallow watcher missed every such edit —
|
|
@@ -6089,6 +6114,50 @@ export default {
|
|
|
6089
6114
|
* @param {{purpose: string, note: string}} answer The chosen purpose.
|
|
6090
6115
|
* @return {Promise<void>}
|
|
6091
6116
|
*/
|
|
6117
|
+
/**
|
|
6118
|
+
* Ask the app whether the reader must state a purpose for this record
|
|
6119
|
+
* (`accessPurpose.checkUrl`); `{ required: false }` opens it at once.
|
|
6120
|
+
*
|
|
6121
|
+
* @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens
|
|
6122
|
+
* @return {Promise<void>}
|
|
6123
|
+
*/
|
|
6124
|
+
async checkAccessPurpose() {
|
|
6125
|
+
const key = this.accessPurposeKey
|
|
6126
|
+
const url = String(this.accessPurpose?.checkUrl || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')))
|
|
6127
|
+
if (url === '' || this.accessPurposeCheckedKey === key) {
|
|
6128
|
+
return
|
|
6129
|
+
}
|
|
6130
|
+
this.accessPurposeCheckedKey = key
|
|
6131
|
+
this.accessPurposeChecking = true
|
|
6132
|
+
try {
|
|
6133
|
+
const { default: axios } = await import('@nextcloud/axios')
|
|
6134
|
+
const { data } = await axios.get(prefixUrl(url))
|
|
6135
|
+
if (data && data.required === false && this.accessPurposeKey === key) {
|
|
6136
|
+
this.grantAccessPurpose(key)
|
|
6137
|
+
}
|
|
6138
|
+
} catch {
|
|
6139
|
+
// The check is a shortcut: without an answer the reader is asked as before.
|
|
6140
|
+
} finally {
|
|
6141
|
+
this.accessPurposeChecking = false
|
|
6142
|
+
}
|
|
6143
|
+
},
|
|
6144
|
+
|
|
6145
|
+
/**
|
|
6146
|
+
* Open the record and remember for this browser session that its
|
|
6147
|
+
* purpose is recorded.
|
|
6148
|
+
*
|
|
6149
|
+
* @param {string} key The record's session key.
|
|
6150
|
+
* @return {void}
|
|
6151
|
+
*/
|
|
6152
|
+
grantAccessPurpose(key) {
|
|
6153
|
+
this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]
|
|
6154
|
+
try {
|
|
6155
|
+
window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))
|
|
6156
|
+
} catch {
|
|
6157
|
+
// Without session storage the purpose is asked again on the next visit.
|
|
6158
|
+
}
|
|
6159
|
+
},
|
|
6160
|
+
|
|
6092
6161
|
async recordAccessPurpose(answer) {
|
|
6093
6162
|
const url = String(this.accessPurpose?.url || '').replace(/\{objectId\}/g, encodeURIComponent(String(this.objectId || '')))
|
|
6094
6163
|
if (url === '') {
|
|
@@ -6100,13 +6169,7 @@ export default {
|
|
|
6100
6169
|
try {
|
|
6101
6170
|
const { default: axios } = await import('@nextcloud/axios')
|
|
6102
6171
|
await axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note })
|
|
6103
|
-
|
|
6104
|
-
this.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]
|
|
6105
|
-
try {
|
|
6106
|
-
window.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))
|
|
6107
|
-
} catch {
|
|
6108
|
-
// Without session storage the purpose is asked again on the next visit.
|
|
6109
|
-
}
|
|
6172
|
+
this.grantAccessPurpose(this.accessPurposeKey)
|
|
6110
6173
|
} catch (error) {
|
|
6111
6174
|
const data = error?.response?.data
|
|
6112
6175
|
this.accessPurposeError = (data && (data.message || data.error)) || t('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.')
|
|
@@ -86,9 +86,14 @@ export default {
|
|
|
86
86
|
font-size: 13px;
|
|
87
87
|
font-weight: 700;
|
|
88
88
|
gap: 6px;
|
|
89
|
-
margin: 0;
|
|
90
89
|
min-height: 32px;
|
|
91
90
|
padding: 0 14px;
|
|
92
91
|
white-space: nowrap;
|
|
93
92
|
}
|
|
93
|
+
|
|
94
|
+
/* server.css gives every button `margin: 3px 3px 3px 0` at (0,2,1); the
|
|
95
|
+
doubled class outranks it. */
|
|
96
|
+
.cn-detail-page__catalog-action.cn-detail-page__catalog-action {
|
|
97
|
+
margin: 0;
|
|
98
|
+
}
|
|
94
99
|
</style>
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
<template>
|
|
6
6
|
<CnDialog
|
|
7
7
|
:name="text(title) || t('nextcloud-vue', 'This may already exist')"
|
|
8
|
+
:eyebrow="text(eyebrow)"
|
|
8
9
|
width="form"
|
|
9
10
|
data-testid="cn-duplicate-warning"
|
|
10
11
|
@closing="close">
|
|
11
12
|
<div class="cn-duplicate-warning">
|
|
12
|
-
<NcNoteCard :type="affordances.blocked ? 'error' : 'warning'" data-testid="cn-duplicate-warning-note">
|
|
13
|
+
<NcNoteCard :type="affordances.blocked && !affordances.mayContinue ? 'error' : 'warning'" data-testid="cn-duplicate-warning-note">
|
|
13
14
|
{{ headlineText }}
|
|
14
15
|
</NcNoteCard>
|
|
15
16
|
|
|
@@ -21,13 +22,14 @@
|
|
|
21
22
|
<a v-if="row.href" :href="row.href" class="cn-duplicate-warning__title">{{ row.title }}</a>
|
|
22
23
|
<span v-else class="cn-duplicate-warning__title">{{ row.title }}</span>
|
|
23
24
|
<span v-if="row.meta" class="cn-duplicate-warning__meta">{{ row.meta }}</span>
|
|
24
|
-
<span class="cn-duplicate-warning__meta">{{ row.
|
|
25
|
+
<span class="cn-duplicate-warning__meta">{{ row.matched }}</span>
|
|
25
26
|
</li>
|
|
26
27
|
</ul>
|
|
27
28
|
|
|
28
29
|
<NcTextField v-if="affordances.needsReason"
|
|
29
30
|
:label="text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')"
|
|
30
31
|
:modelValue="reason"
|
|
32
|
+
:helperText="text(reasonHint)"
|
|
31
33
|
data-testid="cn-duplicate-warning-reason"
|
|
32
34
|
@update:modelValue="reason = $event" />
|
|
33
35
|
|
|
@@ -36,6 +38,12 @@
|
|
|
36
38
|
data-testid="cn-duplicate-warning-blocked">
|
|
37
39
|
{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}
|
|
38
40
|
</p>
|
|
41
|
+
|
|
42
|
+
<p v-if="footnote"
|
|
43
|
+
class="cn-duplicate-warning__hint"
|
|
44
|
+
data-testid="cn-duplicate-warning-footnote">
|
|
45
|
+
{{ text(footnote) }}
|
|
46
|
+
</p>
|
|
39
47
|
</div>
|
|
40
48
|
|
|
41
49
|
<template #actions>
|
|
@@ -58,7 +66,7 @@ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
|
58
66
|
import { generateUrl } from '@nextcloud/router'
|
|
59
67
|
import { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'
|
|
60
68
|
import { duplicateAffordances } from '../../utils/duplicateWarning.js'
|
|
61
|
-
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
69
|
+
import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'
|
|
62
70
|
import { CnDialog } from '../CnDialog/index.js'
|
|
63
71
|
|
|
64
72
|
/**
|
|
@@ -134,12 +142,46 @@ export default {
|
|
|
134
142
|
default: '{title}',
|
|
135
143
|
},
|
|
136
144
|
|
|
137
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* A record's meta line, a template filled from it, or several: the
|
|
147
|
+
* first that fills is shown ("{identifier} · {caseType.title}", then
|
|
148
|
+
* "{identifier}" for a record that lacks the type).
|
|
149
|
+
*
|
|
150
|
+
* @type {string|Array<string>}
|
|
151
|
+
*/
|
|
138
152
|
rowMeta: {
|
|
139
|
-
type: String,
|
|
153
|
+
type: [String, Array],
|
|
140
154
|
default: '{identifier}',
|
|
141
155
|
},
|
|
142
156
|
|
|
157
|
+
/**
|
|
158
|
+
* What a match was found on, `{fields}` (joined as a list: "Aanvrager
|
|
159
|
+
* en Onderwerp") and `{percentage}` filled, through the host
|
|
160
|
+
* translate. Empty: "Aanvrager, Onderwerp (82%)".
|
|
161
|
+
*/
|
|
162
|
+
matchedText: {
|
|
163
|
+
type: String,
|
|
164
|
+
default: '',
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
/** A line above the title, as the board dialogs draw one. */
|
|
168
|
+
eyebrow: {
|
|
169
|
+
type: String,
|
|
170
|
+
default: '',
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/** The help line under the reason field. */
|
|
174
|
+
reasonHint: {
|
|
175
|
+
type: String,
|
|
176
|
+
default: '',
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/** A muted note under the matches and the reason, for what Cancel and a block mean. */
|
|
180
|
+
footnote: {
|
|
181
|
+
type: String,
|
|
182
|
+
default: '',
|
|
183
|
+
},
|
|
184
|
+
|
|
143
185
|
/** A record's page, a template with `{uuid}`; empty draws no link. */
|
|
144
186
|
recordUrl: {
|
|
145
187
|
type: String,
|
|
@@ -233,15 +275,14 @@ export default {
|
|
|
233
275
|
const record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)
|
|
234
276
|
const fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []
|
|
235
277
|
const score = Number(match && match.score)
|
|
278
|
+
const words = fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field))
|
|
279
|
+
const percentage = Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0
|
|
236
280
|
return {
|
|
237
281
|
uuid,
|
|
238
282
|
title: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),
|
|
239
|
-
meta: record ?
|
|
283
|
+
meta: record ? firstFilled(this.rowMeta, record, (key) => key).trim() : '',
|
|
240
284
|
href: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',
|
|
241
|
-
|
|
242
|
-
? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')
|
|
243
|
-
: t('nextcloud-vue', 'Matched on the score, not on a single field'),
|
|
244
|
-
percentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,
|
|
285
|
+
matched: this.matchedLine(words, percentage),
|
|
245
286
|
}
|
|
246
287
|
})
|
|
247
288
|
},
|
|
@@ -260,6 +301,29 @@ export default {
|
|
|
260
301
|
return value ? this.cnTranslate(value) : ''
|
|
261
302
|
},
|
|
262
303
|
|
|
304
|
+
/**
|
|
305
|
+
* What a match was found on, and how strongly.
|
|
306
|
+
*
|
|
307
|
+
* @param {Array<string>} words The matched fields, in the host's words.
|
|
308
|
+
* @param {number} percentage The score as a percentage.
|
|
309
|
+
* @return {string}
|
|
310
|
+
*/
|
|
311
|
+
matchedLine(words, percentage) {
|
|
312
|
+
if (words.length === 0) {
|
|
313
|
+
return `${t('nextcloud-vue', 'Matched on the score, not on a single field')} (${percentage}%)`
|
|
314
|
+
}
|
|
315
|
+
if (!this.matchedText) {
|
|
316
|
+
return `${words.join(', ')} (${percentage}%)`
|
|
317
|
+
}
|
|
318
|
+
let fields = words.join(', ')
|
|
319
|
+
try {
|
|
320
|
+
fields = new Intl.ListFormat(document.documentElement.lang || undefined, { type: 'conjunction' }).format(words)
|
|
321
|
+
} catch {
|
|
322
|
+
// An engine without Intl.ListFormat keeps the comma list.
|
|
323
|
+
}
|
|
324
|
+
return fillTemplate(this.cnTranslate(this.matchedText), { fields, percentage: String(percentage) })
|
|
325
|
+
},
|
|
326
|
+
|
|
263
327
|
/** File it over the warning, carrying the reason and what it was filed over. */
|
|
264
328
|
fileAnyway() {
|
|
265
329
|
if (!this.canFile) {
|