@conduction/nextcloud-vue 2.148.0 → 2.150.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/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +12 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.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/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 +26 -3
- 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/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/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -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 +761 -520
- package/dist/esm/utils/fillTemplate.js +19 -2
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1368 -945
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +761 -520
- package/l10n/en.json +2 -1
- package/l10n/nl.json +2 -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/CnChartWidget/CnChartWidget.md +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +12 -0
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
- package/src/components/CnDataTable/CnDataTable.vue +34 -1
- 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 +35 -3
- 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/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
- package/src/css/board.css +2 -0
- package/src/css/dialog.css +17 -0
- package/src/css/look-board-detail.css +52 -3
- package/src/css/look-board-index.css +42 -2
- package/src/css/look-board.css +45 -5
- package/src/css/table.css +5 -2
- package/src/utils/fillTemplate.js +19 -2
package/l10n/en.json
CHANGED
|
@@ -2398,7 +2398,8 @@
|
|
|
2398
2398
|
"Until {date}": "Until {date}",
|
|
2399
2399
|
"More actions for {name}": "More actions for {name}",
|
|
2400
2400
|
"Unlink contact": "Unlink contact",
|
|
2401
|
-
"{term}, extendable by {extension}": "{term}, extendable by {extension}"
|
|
2401
|
+
"{term}, extendable by {extension}": "{term}, extendable by {extension}",
|
|
2402
|
+
"Move {item}, use the arrow keys": "Move {item}, use the arrow keys"
|
|
2402
2403
|
},
|
|
2403
2404
|
"plurals": {
|
|
2404
2405
|
"{count} member": [
|
package/l10n/nl.json
CHANGED
|
@@ -2653,7 +2653,8 @@
|
|
|
2653
2653
|
"Until {date}": "Tot {date}",
|
|
2654
2654
|
"More actions for {name}": "Meer acties voor {name}",
|
|
2655
2655
|
"Unlink contact": "Contact ontkoppelen",
|
|
2656
|
-
"{term}, extendable by {extension}": "{term}, te verdagen met {extension}"
|
|
2656
|
+
"{term}, extendable by {extension}": "{term}, te verdagen met {extension}",
|
|
2657
|
+
"Move {item}, use the arrow keys": "{item} verplaatsen, gebruik de pijltjestoetsen"
|
|
2657
2658
|
},
|
|
2658
2659
|
"plurals": {
|
|
2659
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.150.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
|
},
|
|
@@ -109,7 +109,7 @@ Fallback slot — shown when ApexCharts is not available:
|
|
|
109
109
|
| `barMax` | Number | `null` | `bar-list`: the value a full track stands for (100 for percentages); null takes the largest value |
|
|
110
110
|
| `matrixLabelWidth` | Number | `null` | `matrix`: the row-label column's width in px; a long label wraps (DqProcesanalyse "Dagen per fase": 240). Null sizes the column to its longest label, at least 120px |
|
|
111
111
|
| `footnote` | String | `''` | A line under the chart, through the host's translate: under the rows of a `bar-list` or `matrix`, under the plot of a `line`, `area` or `bar` chart |
|
|
112
|
-
| `notice` | Object | `null` | A toned note card under the chart: `{ lead?, text?, values?, labels?, tone? }`. `{name}` in `lead` (bold) and `text` is filled from `values[name]`, a literal
|
|
112
|
+
| `notice` | Object | `null` | A toned note card under the chart: `{ lead?, text?, values?, labels?, tone? }`. `{name}` in `lead` (bold) and `text` is filled from `values[name]`, a literal, a record token (`@object.<field>`) or a field of the record a `cellSource` with `cellsPath` read its cells from (`@source.<field>`, such as the matrix's name); `labels[name]` (`"rows"`, `"columns"`, `"values"` or a map) turns a key into words. `tone`: info, success, warning, error. Hidden while a value is missing |
|
|
113
113
|
| `referenceLines` | Array | `[]` | Dashed horizontal reference lines on the value axis of a line, area or bar chart, such as a norm: `[{ value, label?, color? }]`. The label goes through the host's translate; the colour defaults to the theme's error colour (board DqDoorlooptijd). `valuePath` reads the value from the endpoint answer instead, `{value}` in the label writes it, and a line without a value is not drawn. |
|
|
114
114
|
| `categoryFormat` | String | `''` | How date categories read on the x-axis: `month` ("jan") or `month-year` ("jan 2026"), in the user's language, or `week` for ISO week keys ("2026-W40" as "w40"). Categories that are not dates are left as is; a bare year key stays a year. |
|
|
115
115
|
| `emptyCellLabel` | String | `''` | `matrix`: the text of a cell without a value ("geen"), through the host's translate. Empty leaves the cell blank |
|
|
@@ -1020,6 +1020,9 @@ export default {
|
|
|
1020
1020
|
aggregateData: null,
|
|
1021
1021
|
// The `cellSource` matrix, laid out (`{ series, categories, columnKeys }`), or null.
|
|
1022
1022
|
cellData: null,
|
|
1023
|
+
// The record a `cellSource` with `cellsPath` read its cells from, for
|
|
1024
|
+
// `@source.<field>` in the notice.
|
|
1025
|
+
cellRecord: null,
|
|
1023
1026
|
// Active `views[]` switcher key (null = the first view). Local
|
|
1024
1027
|
// UI state only — never persisted.
|
|
1025
1028
|
activeViewKey: null,
|
|
@@ -1664,6 +1667,14 @@ export default {
|
|
|
1664
1667
|
}
|
|
1665
1668
|
const declared = def.values && typeof def.values === 'object' ? def.values : {}
|
|
1666
1669
|
const resolved = resolveFilterTokens(declared, this.chartTokenCtx())
|
|
1670
|
+
// `@source.<field>`: a field of the record the matrix read its cells
|
|
1671
|
+
// from (the matrix's name and version on an enforcement advice).
|
|
1672
|
+
for (const [name, token] of Object.entries(declared)) {
|
|
1673
|
+
if (typeof token === 'string' && token.startsWith('@source.')) {
|
|
1674
|
+
const value = token.slice('@source.'.length).split('.').reduce((at, key) => (at && typeof at === 'object' ? at[key] : undefined), this.cellRecord)
|
|
1675
|
+
resolved[name] = value === undefined ? null : value
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1667
1678
|
const source = this.cellSource && typeof this.cellSource === 'object' ? this.cellSource : {}
|
|
1668
1679
|
const maps = { rows: source.rowLabels, columns: source.columnLabels, values: source.valueLabels }
|
|
1669
1680
|
const words = {}
|
|
@@ -2392,6 +2403,7 @@ export default {
|
|
|
2392
2403
|
if (this._cellRequest !== key) {
|
|
2393
2404
|
return
|
|
2394
2405
|
}
|
|
2406
|
+
this.cellRecord = this.cellSource.cellsPath ? (records[0] || null) : null
|
|
2395
2407
|
if (this.cellSource.cellsPath) {
|
|
2396
2408
|
const { cells, config } = cellsFromRecord(records[0] || null, this.cellSource, cellFilter)
|
|
2397
2409
|
this.cellData = pivotCells(cells, config)
|
|
@@ -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
|
|
@@ -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) {
|
|
@@ -227,6 +227,9 @@
|
|
|
227
227
|
<CnIcon v-if="link.icon" :name="link.icon" :size="18" />
|
|
228
228
|
{{ link.label }}
|
|
229
229
|
</component>
|
|
230
|
+
<p v-if="link && link.note && record" class="cn-facts-widget__link-note" data-testid="cn-facts-link-note">
|
|
231
|
+
{{ link.note }}
|
|
232
|
+
</p>
|
|
230
233
|
<button
|
|
231
234
|
v-if="action && record"
|
|
232
235
|
type="button"
|
|
@@ -867,18 +870,19 @@ export default {
|
|
|
867
870
|
if (!link || typeof link !== 'object' || !link.label) {
|
|
868
871
|
return null
|
|
869
872
|
}
|
|
873
|
+
const linkView = this.linkWords(link)
|
|
870
874
|
if (typeof link.sidebarTab === 'string' && link.sidebarTab !== '') {
|
|
871
875
|
return this.objectSidebarState
|
|
872
|
-
? {
|
|
876
|
+
? { ...linkView, sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }
|
|
873
877
|
: null
|
|
874
878
|
}
|
|
875
879
|
if (link.route) {
|
|
876
880
|
const to = this.resolveRoute(link.route, this.record)
|
|
877
|
-
return to ? {
|
|
881
|
+
return to ? { ...linkView, to } : null
|
|
878
882
|
}
|
|
879
883
|
if (typeof link.href === 'string' && link.href !== '') {
|
|
880
884
|
const href = this.linkHref(link, this.record)
|
|
881
|
-
return href ? {
|
|
885
|
+
return href ? { ...linkView, href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : null
|
|
882
886
|
}
|
|
883
887
|
return null
|
|
884
888
|
},
|
|
@@ -1017,6 +1021,23 @@ export default {
|
|
|
1017
1021
|
},
|
|
1018
1022
|
|
|
1019
1023
|
methods: {
|
|
1024
|
+
/**
|
|
1025
|
+
* A link's label and the muted note under it, their `{path}` and
|
|
1026
|
+
* `{path:format}` placeholders filled from the record ("{page:last-segment}").
|
|
1027
|
+
*
|
|
1028
|
+
* @param {object} link The configured link.
|
|
1029
|
+
* @return {{label: string, note: string}}
|
|
1030
|
+
* @spec openspec/changes/facts-link-label-from-record/specs/facts-widget/spec.md#requirement-a-facts-link-can-take-its-words-from-the-record
|
|
1031
|
+
*/
|
|
1032
|
+
linkWords(link) {
|
|
1033
|
+
const fill = (text) => (text.includes('{') && this.record ? this.templateValue(this.record, text) : this.tr(text))
|
|
1034
|
+
const label = fill(String(link.label))
|
|
1035
|
+
return {
|
|
1036
|
+
label: label || this.tr(String(link.label)),
|
|
1037
|
+
note: typeof link.note === 'string' && link.note !== '' ? fill(link.note) : '',
|
|
1038
|
+
}
|
|
1039
|
+
},
|
|
1040
|
+
|
|
1020
1041
|
/**
|
|
1021
1042
|
* Open the page's object sidebar on one of its tabs.
|
|
1022
1043
|
*
|
|
@@ -1899,6 +1920,11 @@ export default {
|
|
|
1899
1920
|
return formatTermDuration(raw, typeof spec.extendBy === 'string' && record ? readPath(record, spec.extendBy) : null)
|
|
1900
1921
|
}
|
|
1901
1922
|
// The month written out, the year only outside this year: "3 oktober".
|
|
1923
|
+
// One segment of a path ("Team/Page"): its first or its last.
|
|
1924
|
+
if (format === 'first-segment' || format === 'last-segment') {
|
|
1925
|
+
const segments = String(raw).split('/').map((part) => part.trim()).filter((part) => part !== '')
|
|
1926
|
+
return (format === 'first-segment' ? segments[0] : segments[segments.length - 1]) || ''
|
|
1927
|
+
}
|
|
1902
1928
|
if (format === 'longdate') {
|
|
1903
1929
|
return formatLongDate(raw)
|
|
1904
1930
|
}
|
|
@@ -2315,6 +2341,12 @@ export default {
|
|
|
2315
2341
|
text-decoration: underline;
|
|
2316
2342
|
}
|
|
2317
2343
|
|
|
2344
|
+
.cn-facts-widget__link-note {
|
|
2345
|
+
margin: 0;
|
|
2346
|
+
color: var(--color-text-maxcontrast);
|
|
2347
|
+
font-size: 13px;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2318
2350
|
.cn-facts-widget__link--button {
|
|
2319
2351
|
display: inline-flex;
|
|
2320
2352
|
align-items: center;
|