@conduction/nextcloud-vue 2.56.0-beta.62 → 2.56.0-beta.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +246 -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 +495 -61
- package/dist/esm/utils/actionsDispatcher.js +6 -1
- 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 +17409 -12832
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +495 -61
- package/l10n/en.json +106 -1
- package/l10n/nl.json +105 -1
- package/package.json +1 -1
- 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 +456 -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 +6 -1
- 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,318 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<CnDialog
|
|
7
|
+
:name="text(title) || t('nextcloud-vue', 'This may already exist')"
|
|
8
|
+
width="form"
|
|
9
|
+
data-testid="cn-duplicate-warning"
|
|
10
|
+
@closing="close">
|
|
11
|
+
<div class="cn-duplicate-warning">
|
|
12
|
+
<NcNoteCard :type="affordances.blocked ? 'error' : 'warning'" data-testid="cn-duplicate-warning-note">
|
|
13
|
+
{{ headlineText }}
|
|
14
|
+
</NcNoteCard>
|
|
15
|
+
|
|
16
|
+
<ul class="cn-duplicate-warning__matches">
|
|
17
|
+
<li v-for="row in rows"
|
|
18
|
+
:key="row.uuid"
|
|
19
|
+
class="cn-duplicate-warning__match"
|
|
20
|
+
data-testid="cn-duplicate-warning-match">
|
|
21
|
+
<a v-if="row.href" :href="row.href" class="cn-duplicate-warning__title">{{ row.title }}</a>
|
|
22
|
+
<span v-else class="cn-duplicate-warning__title">{{ row.title }}</span>
|
|
23
|
+
<span v-if="row.meta" class="cn-duplicate-warning__meta">{{ row.meta }}</span>
|
|
24
|
+
<span class="cn-duplicate-warning__meta">{{ row.matchedOn }} ({{ row.percentage }}%)</span>
|
|
25
|
+
</li>
|
|
26
|
+
</ul>
|
|
27
|
+
|
|
28
|
+
<NcTextField v-if="affordances.needsReason"
|
|
29
|
+
:label="text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')"
|
|
30
|
+
:modelValue="reason"
|
|
31
|
+
data-testid="cn-duplicate-warning-reason"
|
|
32
|
+
@update:modelValue="reason = $event" />
|
|
33
|
+
|
|
34
|
+
<p v-if="affordances.blocked && !affordances.mayContinue"
|
|
35
|
+
class="cn-duplicate-warning__hint"
|
|
36
|
+
data-testid="cn-duplicate-warning-blocked">
|
|
37
|
+
{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<template #actions>
|
|
42
|
+
<NcButton variant="tertiary" data-testid="cn-duplicate-warning-cancel" @click="close">
|
|
43
|
+
{{ t('nextcloud-vue', 'Cancel') }}
|
|
44
|
+
</NcButton>
|
|
45
|
+
<NcButton v-if="affordances.mayContinue"
|
|
46
|
+
variant="primary"
|
|
47
|
+
:disabled="!canFile"
|
|
48
|
+
data-testid="cn-duplicate-warning-continue"
|
|
49
|
+
@click="fileAnyway">
|
|
50
|
+
{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}
|
|
51
|
+
</NcButton>
|
|
52
|
+
</template>
|
|
53
|
+
</CnDialog>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<script>
|
|
57
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
58
|
+
import { generateUrl } from '@nextcloud/router'
|
|
59
|
+
import { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'
|
|
60
|
+
import { duplicateAffordances } from '../../utils/duplicateWarning.js'
|
|
61
|
+
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
62
|
+
import { CnDialog } from '../CnDialog/index.js'
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* CnDuplicateWarning — "this may already exist", before a record is filed.
|
|
66
|
+
* It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),
|
|
67
|
+
* never scoring anything itself, with each matched record's title, a meta
|
|
68
|
+
* line, what it matched on and how strongly. Under a `warn` policy anyone may
|
|
69
|
+
* file it anyway; under `block` only a user who `mayOverride`, and they say
|
|
70
|
+
* why. A match the user may not read is reported without disclosing it.
|
|
71
|
+
* dossiq's case intake is the first consumer; pipelinq contacts share it.
|
|
72
|
+
*
|
|
73
|
+
* Open it in a template, or from code with `openDuplicateWarning(props)`,
|
|
74
|
+
* which resolves with `{ reason, over }` or null.
|
|
75
|
+
*
|
|
76
|
+
* ```vue
|
|
77
|
+
* <CnDuplicateWarning
|
|
78
|
+
* :matches="matches"
|
|
79
|
+
* :records="readableCases"
|
|
80
|
+
* policy="block"
|
|
81
|
+
* :may-override="isCoordinator"
|
|
82
|
+
* record-url="/apps/dossiq/cases/{uuid}"
|
|
83
|
+
* :field-labels="{ requester: 'Requester', title: 'Subject' }"
|
|
84
|
+
* @file="save"
|
|
85
|
+
* @close="cancel" />
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export default {
|
|
89
|
+
name: 'CnDuplicateWarning',
|
|
90
|
+
|
|
91
|
+
components: { CnDialog, NcButton, NcNoteCard, NcTextField },
|
|
92
|
+
|
|
93
|
+
inject: {
|
|
94
|
+
/** The host app's translate (CnAppRoot `cnTranslate`). */
|
|
95
|
+
cnTranslate: { default: () => (key) => key },
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
props: {
|
|
99
|
+
/**
|
|
100
|
+
* The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.
|
|
101
|
+
*
|
|
102
|
+
* @type {Array<object>}
|
|
103
|
+
*/
|
|
104
|
+
matches: {
|
|
105
|
+
type: Array,
|
|
106
|
+
default: () => [],
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The matched records the user may read, matched by `id`, `uuid` or `@self.id`.
|
|
111
|
+
*
|
|
112
|
+
* @type {Array<object>}
|
|
113
|
+
*/
|
|
114
|
+
records: {
|
|
115
|
+
type: Array,
|
|
116
|
+
default: () => [],
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */
|
|
120
|
+
policy: {
|
|
121
|
+
type: String,
|
|
122
|
+
default: 'warn',
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/** Whether this user may file over a block (and must say why). */
|
|
126
|
+
mayOverride: {
|
|
127
|
+
type: Boolean,
|
|
128
|
+
default: false,
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/** A record's title, a template filled from it. */
|
|
132
|
+
rowTitle: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: '{title}',
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/** A record's meta line, a template filled from it. */
|
|
138
|
+
rowMeta: {
|
|
139
|
+
type: String,
|
|
140
|
+
default: '{identifier}',
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/** A record's page, a template with `{uuid}`; empty draws no link. */
|
|
144
|
+
recordUrl: {
|
|
145
|
+
type: String,
|
|
146
|
+
default: '',
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Words for the matched fields (`{ requester: "Requester" }`), through the host translate.
|
|
151
|
+
*
|
|
152
|
+
* @type {object}
|
|
153
|
+
*/
|
|
154
|
+
fieldLabels: {
|
|
155
|
+
type: Object,
|
|
156
|
+
default: () => ({}),
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/** The dialog title; defaults to "This may already exist". */
|
|
160
|
+
title: {
|
|
161
|
+
type: String,
|
|
162
|
+
default: '',
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/** The note's sentence, `{count}` filled; defaults to "%n existing record looks like this one.". */
|
|
166
|
+
headline: {
|
|
167
|
+
type: String,
|
|
168
|
+
default: '',
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/** The sentence under a block the user may not pass. */
|
|
172
|
+
blockedText: {
|
|
173
|
+
type: String,
|
|
174
|
+
default: '',
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/** The reason field's label. */
|
|
178
|
+
reasonLabel: {
|
|
179
|
+
type: String,
|
|
180
|
+
default: '',
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/** The continue button's label; defaults to "File it anyway". */
|
|
184
|
+
continueLabel: {
|
|
185
|
+
type: String,
|
|
186
|
+
default: '',
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
emits: ['close', 'file'],
|
|
191
|
+
|
|
192
|
+
data() {
|
|
193
|
+
return {
|
|
194
|
+
reason: '',
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
computed: {
|
|
199
|
+
/**
|
|
200
|
+
* What to draw and offer.
|
|
201
|
+
*
|
|
202
|
+
* @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
|
|
203
|
+
* @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}
|
|
204
|
+
*/
|
|
205
|
+
affordances() {
|
|
206
|
+
return duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/** @return {boolean} Whether "file it anyway" may be pressed. */
|
|
210
|
+
canFile() {
|
|
211
|
+
return this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/** @return {string} The note's sentence. */
|
|
215
|
+
headlineText() {
|
|
216
|
+
const count = this.matches.length
|
|
217
|
+
if (this.headline) {
|
|
218
|
+
return fillTemplate(this.cnTranslate(this.headline), { count })
|
|
219
|
+
}
|
|
220
|
+
return n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One row per match: the readable record's title and meta and link, or
|
|
225
|
+
* a neutral name for one the user may not read.
|
|
226
|
+
*
|
|
227
|
+
* @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning
|
|
228
|
+
* @return {Array<object>}
|
|
229
|
+
*/
|
|
230
|
+
rows() {
|
|
231
|
+
return this.matches.map((match) => {
|
|
232
|
+
const uuid = String((match && match.uuid) || '')
|
|
233
|
+
const record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)
|
|
234
|
+
const fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []
|
|
235
|
+
const score = Number(match && match.score)
|
|
236
|
+
return {
|
|
237
|
+
uuid,
|
|
238
|
+
title: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),
|
|
239
|
+
meta: record ? fillTemplate(this.rowMeta, record).trim() : '',
|
|
240
|
+
href: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',
|
|
241
|
+
matchedOn: fields.length
|
|
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,
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
methods: {
|
|
251
|
+
t,
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* A prop text through the host translate.
|
|
255
|
+
*
|
|
256
|
+
* @param {string} value The text.
|
|
257
|
+
* @return {string}
|
|
258
|
+
*/
|
|
259
|
+
text(value) {
|
|
260
|
+
return value ? this.cnTranslate(value) : ''
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/** File it over the warning, carrying the reason and what it was filed over. */
|
|
264
|
+
fileAnyway() {
|
|
265
|
+
if (!this.canFile) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */
|
|
269
|
+
this.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
/** Go back without filing. */
|
|
273
|
+
close() {
|
|
274
|
+
/** @event close The user went back without filing. */
|
|
275
|
+
this.$emit('close')
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
</script>
|
|
280
|
+
|
|
281
|
+
<style scoped>
|
|
282
|
+
.cn-duplicate-warning {
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
gap: 12px;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.cn-duplicate-warning__matches {
|
|
289
|
+
display: flex;
|
|
290
|
+
flex-direction: column;
|
|
291
|
+
gap: 8px;
|
|
292
|
+
margin: 0;
|
|
293
|
+
padding: 0;
|
|
294
|
+
list-style: none;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.cn-duplicate-warning__match {
|
|
298
|
+
display: flex;
|
|
299
|
+
flex-wrap: wrap;
|
|
300
|
+
gap: 4px 12px;
|
|
301
|
+
padding: 8px 12px;
|
|
302
|
+
border: 1px solid var(--color-border);
|
|
303
|
+
border-radius: var(--border-radius-large, 8px);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.cn-duplicate-warning__title {
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.cn-duplicate-warning__meta,
|
|
311
|
+
.cn-duplicate-warning__hint {
|
|
312
|
+
color: var(--color-text-maxcontrast);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.cn-duplicate-warning__hint {
|
|
316
|
+
margin: 0;
|
|
317
|
+
}
|
|
318
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnDuplicateWarning — "this may already exist", before a record is filed,
|
|
6
|
+
* from OpenRegister's scored matches.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import CnDuplicateWarning from './CnDuplicateWarning.vue'
|
|
10
|
+
|
|
11
|
+
export { CnDuplicateWarning }
|
|
12
|
+
export default CnDuplicateWarning
|
|
@@ -961,7 +961,7 @@ import { computeAssignments } from '../../utils/formAssign.js'
|
|
|
961
961
|
import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
962
962
|
import { durationSeconds } from '../../utils/isoDuration.js'
|
|
963
963
|
import { objectDisplayName } from '../../utils/objectName.js'
|
|
964
|
-
import { fieldWithOptionsFrom, filterOptions, isOptionsFrom, loadOptionsFrom } from '../../utils/optionsFrom.js'
|
|
964
|
+
import { fieldWithOptionsFrom, filterOptions, formTokenKeys, isOptionsFrom, loadOptionsFrom, resolveFormTokens } from '../../utils/optionsFrom.js'
|
|
965
965
|
import { languageOptions, resolveDefaultToken, timezoneOptions } from '../../utils/pickerOptions.js'
|
|
966
966
|
import { getDotted, planPropertySourceFill } from '../../utils/propertySourceFill.js'
|
|
967
967
|
import { pruneRows } from '../../utils/pruneRows.js'
|
|
@@ -1573,6 +1573,8 @@ export default {
|
|
|
1573
1573
|
sourcedOptions: {},
|
|
1574
1574
|
// Whether a field's `optionsFrom` failed or gave no options, by field key: it falls back to text.
|
|
1575
1575
|
sourcedFailed: {},
|
|
1576
|
+
// The resolved source each field's options were read from, by field key.
|
|
1577
|
+
sourcedSignatures: {},
|
|
1576
1578
|
// Set while a save that passed its uniqueness check goes on.
|
|
1577
1579
|
uniqueChecked: false,
|
|
1578
1580
|
// The sections folded away (`sectionCollapsed` on one of their fields opens them folded).
|
|
@@ -1938,6 +1940,9 @@ export default {
|
|
|
1938
1940
|
visibleFields() {
|
|
1939
1941
|
return this.resolvedFields
|
|
1940
1942
|
.filter((field) => shouldShow(field, this.formData))
|
|
1943
|
+
// `optionsFrom.hideWhenEmpty`: a select whose source gives nothing
|
|
1944
|
+
// for the current values is left out (a type without formats).
|
|
1945
|
+
.filter((field) => !(field.optionsFrom && field.optionsFrom.hideWhenEmpty === true && Array.isArray(this.sourcedOptions[field.key]) && this.sourcedOptions[field.key].length === 0))
|
|
1941
1946
|
// `requiredWhen`: required while its condition holds, so the
|
|
1942
1947
|
// asterisk, the board look's aria-required and validation follow.
|
|
1943
1948
|
.map((field) => (field.requiredWhen ? { ...field, required: conditionHolds(field.requiredWhen, this.formData, field.key) } : field))
|
|
@@ -2664,13 +2669,18 @@ export default {
|
|
|
2664
2669
|
* @spec openspec/changes/form-field-options-from/specs/form-dialog/spec.md#requirement-a-select-field-can-read-its-options-from-a-source
|
|
2665
2670
|
*/
|
|
2666
2671
|
async fetchSourcedOptions(field, query) {
|
|
2667
|
-
|
|
2672
|
+
// A source that reads another field (`@answer.<key>`) is read again
|
|
2673
|
+
// when that value changes: the cache is kept per resolved source.
|
|
2674
|
+
const from = resolveFormTokens(field.optionsFrom, this.formData)
|
|
2675
|
+
const signature = JSON.stringify(from)
|
|
2676
|
+
let all = this.sourcedSignatures[field.key] === signature ? this.sourcedOptions[field.key] : null
|
|
2668
2677
|
if (!all) {
|
|
2669
2678
|
try {
|
|
2670
|
-
all = await loadOptionsFrom(
|
|
2679
|
+
all = await loadOptionsFrom(from, resolveObjectTokenContext(this.cnObjectContext, this.cnDetailObjectContext) || {})
|
|
2671
2680
|
} catch {
|
|
2672
2681
|
all = []
|
|
2673
2682
|
}
|
|
2683
|
+
this.sourcedSignatures = { ...this.sourcedSignatures, [field.key]: signature }
|
|
2674
2684
|
this.sourcedOptions = { ...this.sourcedOptions, [field.key]: all }
|
|
2675
2685
|
this.sourcedFailed = { ...this.sourcedFailed, [field.key]: all.length === 0 }
|
|
2676
2686
|
const labels = {}
|
|
@@ -2942,6 +2952,12 @@ export default {
|
|
|
2942
2952
|
// A hand edit ends this field's own rules; other fields may follow it.
|
|
2943
2953
|
delete this.assignedFrom[key]
|
|
2944
2954
|
this.runAssignments([key])
|
|
2955
|
+
// A select whose source reads this field (`@answer.<key>`) reads it again.
|
|
2956
|
+
for (const field of this.resolvedFields) {
|
|
2957
|
+
if (isOptionsFrom(field.optionsFrom) && formTokenKeys(field.optionsFrom).includes(key)) {
|
|
2958
|
+
this.loadAsyncOptions(field, '')
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2945
2961
|
},
|
|
2946
2962
|
|
|
2947
2963
|
/**
|
|
@@ -744,6 +744,28 @@
|
|
|
744
744
|
<NcLoadingIcon :size="32" :name="loadingText" />
|
|
745
745
|
</div>
|
|
746
746
|
|
|
747
|
+
<!-- A search the server refused for its grammar: the term with the
|
|
748
|
+
character it stopped at, the server's reason, and what to do. -->
|
|
749
|
+
<div v-else-if="searchRefusal"
|
|
750
|
+
class="cn-index-page__search-refusal"
|
|
751
|
+
role="status"
|
|
752
|
+
data-testid="cn-index-page-search-refusal">
|
|
753
|
+
<NcNoteCard type="warning">
|
|
754
|
+
<p class="cn-index-page__search-refusal-headline">
|
|
755
|
+
{{ t('nextcloud-vue', 'This search could not be read from character {position}.', { position: searchRefusal.position }) }}
|
|
756
|
+
</p>
|
|
757
|
+
<p class="cn-index-page__search-refusal-term" data-testid="cn-index-page-search-refusal-term">
|
|
758
|
+
<span>{{ searchRefusal.before }}</span><mark v-if="searchRefusal.at">{{ searchRefusal.at }}</mark><span>{{ searchRefusal.after }}</span>
|
|
759
|
+
</p>
|
|
760
|
+
<p class="cn-index-page__search-refusal-muted">
|
|
761
|
+
{{ searchRefusal.message }}
|
|
762
|
+
</p>
|
|
763
|
+
<p class="cn-index-page__search-refusal-muted">
|
|
764
|
+
{{ searchRefusalHintText }}
|
|
765
|
+
</p>
|
|
766
|
+
</NcNoteCard>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
747
769
|
<!-- Error state: the latest self-fetch failed (e.g. a search term the server rejects) -->
|
|
748
770
|
<div v-else-if="selfFetchFailed"
|
|
749
771
|
class="cn-index-page__empty"
|
|
@@ -1361,6 +1383,7 @@ import { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterView
|
|
|
1361
1383
|
import { savedViewBadge, savedViewOverrides } from '../../utils/savedViewOverrides.js'
|
|
1362
1384
|
import { columnsFromSchema, fieldsFromSchema } from '../../utils/schema.js'
|
|
1363
1385
|
import { resolveScopeLayout, resolveScopePresentation } from '../../utils/scopeListLayout.js'
|
|
1386
|
+
import { readSearchRefusal } from '../../utils/searchRefusal.js'
|
|
1364
1387
|
import { orderToSortKeys } from '../../utils/sortOrder.js'
|
|
1365
1388
|
import { annotateRows, filterSourceRows, pageSourceRows, rowsFromSourceBody } from '../../utils/sortSourceRows.js'
|
|
1366
1389
|
import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
|
|
@@ -2825,6 +2848,17 @@ export default {
|
|
|
2825
2848
|
default: null,
|
|
2826
2849
|
},
|
|
2827
2850
|
|
|
2851
|
+
/**
|
|
2852
|
+
* What to do after a refused search, through the app's translate. The
|
|
2853
|
+
* default explains AND, OR, NOT and quotes.
|
|
2854
|
+
*
|
|
2855
|
+
* @spec openspec/changes/index-search-refusal/specs/index-search-refusal/spec.md#requirement-a-refused-search-is-said-where-it-was-typed
|
|
2856
|
+
*/
|
|
2857
|
+
searchRefusalHint: {
|
|
2858
|
+
type: String,
|
|
2859
|
+
default: '',
|
|
2860
|
+
},
|
|
2861
|
+
|
|
2828
2862
|
/** Accessible label for the loading spinner (NcLoadingIcon aria-label) */
|
|
2829
2863
|
loadingText: {
|
|
2830
2864
|
type: String,
|
|
@@ -4417,6 +4451,24 @@ export default {
|
|
|
4417
4451
|
return this.isSelfFetchMode && !!this.list.error?.value
|
|
4418
4452
|
},
|
|
4419
4453
|
|
|
4454
|
+
/**
|
|
4455
|
+
* The failed fetch as a refused search, or null when it is not one.
|
|
4456
|
+
*
|
|
4457
|
+
* @spec openspec/changes/index-search-refusal/specs/index-search-refusal/spec.md#requirement-a-refused-search-is-said-where-it-was-typed
|
|
4458
|
+
* @return {object|null}
|
|
4459
|
+
*/
|
|
4460
|
+
searchRefusal() {
|
|
4461
|
+
return this.selfFetchFailed ? readSearchRefusal(this.list.error.value, this.effectiveSearchValue) : null
|
|
4462
|
+
},
|
|
4463
|
+
|
|
4464
|
+
/** @return {string} What to do after a refused search. */
|
|
4465
|
+
searchRefusalHintText() {
|
|
4466
|
+
if (this.searchRefusalHint) {
|
|
4467
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(this.searchRefusalHint) : this.searchRefusalHint
|
|
4468
|
+
}
|
|
4469
|
+
return t('nextcloud-vue', 'Correct that character, or search for plain words. AND, OR and NOT work in capitals, and quotes make a phrase.')
|
|
4470
|
+
},
|
|
4471
|
+
|
|
4420
4472
|
/** Rows: store collection in self-fetch mode, else the `objects` prop. */
|
|
4421
4473
|
effectiveObjects() {
|
|
4422
4474
|
if (this.isNamedSource) {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div class="cn-nc-dashboard-list" data-testid="cn-nc-dashboard-list">
|
|
7
|
+
<p v-if="error"
|
|
8
|
+
class="cn-nc-dashboard-list__error"
|
|
9
|
+
role="status"
|
|
10
|
+
data-testid="cn-nc-dashboard-list-error">
|
|
11
|
+
{{ t('nextcloud-vue', 'This list could not be read.') }}
|
|
12
|
+
</p>
|
|
13
|
+
<CnDataTable v-else
|
|
14
|
+
:rows="rows"
|
|
15
|
+
:columns="columns"
|
|
16
|
+
:loading="loading"
|
|
17
|
+
hideHeader
|
|
18
|
+
borderless
|
|
19
|
+
:emptyText="text(config.emptyText) || t('nextcloud-vue', 'Nothing to show')"
|
|
20
|
+
@rowClick="onRowClick"
|
|
21
|
+
@rowAuxClick="onRowClick">
|
|
22
|
+
<template v-if="viewAllHref" #footer>
|
|
23
|
+
<a class="cn-data-table__view-all" :href="viewAllHref" data-testid="cn-nc-dashboard-list-view-all">
|
|
24
|
+
{{ text(config.viewAll.label) || t('nextcloud-vue', 'View all') }} →
|
|
25
|
+
</a>
|
|
26
|
+
</template>
|
|
27
|
+
</CnDataTable>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<script>
|
|
32
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
33
|
+
import { generateUrl } from '@nextcloud/router'
|
|
34
|
+
import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
|
|
35
|
+
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
36
|
+
import { openRowTarget } from '../../utils/linkNavigation.js'
|
|
37
|
+
import { readPath } from '../../utils/readPath.js'
|
|
38
|
+
import { resolveDeepTokens } from '../../utils/resolveFilterTokens.js'
|
|
39
|
+
import { CnDataTable } from '../CnDataTable/index.js'
|
|
40
|
+
|
|
41
|
+
/** A name and a muted, right-aligned trailing line. */
|
|
42
|
+
const COLUMNS = [
|
|
43
|
+
{ key: 'mainText', cellClass: 'cn-cell--strong' },
|
|
44
|
+
{ key: 'subText', cellClass: 'cn-cell--muted cn-cell--end' },
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A url for an anchor: absolute and app-root paths as given, else through `generateUrl`.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} url The url.
|
|
51
|
+
* @return {string}
|
|
52
|
+
*/
|
|
53
|
+
function appUrl(url) {
|
|
54
|
+
return /^(https?:)?\/\//i.test(url) || url.startsWith('/index.php') ? url : generateUrl(url)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CnNcDashboardList — the compact list a Nextcloud Dashboard widget shows:
|
|
59
|
+
* a bold name, a muted trailing line, and a "View all" link, read from
|
|
60
|
+
* OpenRegister or any endpoint. Each row opens its record in place (a
|
|
61
|
+
* modifier or middle click opens a new tab), because the Nextcloud Dashboard
|
|
62
|
+
* has no app router. `registerNcDashboardWidgets` mounts it per widget id, so
|
|
63
|
+
* an app's dashboard widgets are configuration rather than one component each.
|
|
64
|
+
*
|
|
65
|
+
* `config`:
|
|
66
|
+
* - `source`: `{ register, schema, params? }` (OpenRegister objects, rows in
|
|
67
|
+
* `results`) or an endpointSource `{ url, params?, responsePath? }`. Filter
|
|
68
|
+
* tokens (`@me`, `@today`, `@today-7d`) resolve in `params`, and an operator
|
|
69
|
+
* map (`{ deadline: { lt: '@today' } }`) goes out as `deadline[lt]=…`.
|
|
70
|
+
* - `itemsPath`: the rows' path in the response, when not `results`.
|
|
71
|
+
* - `limit`: the most rows shown (default 7).
|
|
72
|
+
* - `title`, `subtitle`: templates filled from a row (default `{title}`, none).
|
|
73
|
+
* - `href`: a template for a row's page (`/apps/dossiq/cases/{id}`).
|
|
74
|
+
* - `viewAll`: `{ href, label? }`.
|
|
75
|
+
* - `emptyText`: text for an empty list.
|
|
76
|
+
*
|
|
77
|
+
* Texts go through the host translate.
|
|
78
|
+
*/
|
|
79
|
+
export default {
|
|
80
|
+
name: 'CnNcDashboardList',
|
|
81
|
+
|
|
82
|
+
components: { CnDataTable },
|
|
83
|
+
|
|
84
|
+
inject: {
|
|
85
|
+
/** The host app's translate (provided by `registerNcDashboardWidgets`). */
|
|
86
|
+
cnTranslate: { default: () => (key) => key },
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
props: {
|
|
90
|
+
/**
|
|
91
|
+
* The list's configuration (see the component docs).
|
|
92
|
+
*
|
|
93
|
+
* @type {object}
|
|
94
|
+
*/
|
|
95
|
+
config: {
|
|
96
|
+
type: Object,
|
|
97
|
+
default: () => ({}),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
data() {
|
|
102
|
+
return {
|
|
103
|
+
items: [],
|
|
104
|
+
loading: true,
|
|
105
|
+
error: false,
|
|
106
|
+
columns: COLUMNS,
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
computed: {
|
|
111
|
+
/** @return {number} The most rows shown. */
|
|
112
|
+
limit() {
|
|
113
|
+
return Number(this.config.limit) > 0 ? Number(this.config.limit) : 7
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The rows as the table draws them.
|
|
118
|
+
*
|
|
119
|
+
* @spec openspec/changes/nc-dashboard-widgets/specs/nc-dashboard-widgets/spec.md#requirement-dashboard-widgets-are-configuration
|
|
120
|
+
* @return {Array<{id: string, mainText: string, subText: string, href: string}>}
|
|
121
|
+
*/
|
|
122
|
+
rows() {
|
|
123
|
+
return this.items.slice(0, this.limit).map((item, index) => {
|
|
124
|
+
const id = item.id ?? (item['@self'] && item['@self'].id) ?? index
|
|
125
|
+
return {
|
|
126
|
+
id: String(id),
|
|
127
|
+
mainText: this.fill(this.config.title || '{title}', item) || String(id),
|
|
128
|
+
subText: this.fill(this.config.subtitle, item),
|
|
129
|
+
href: this.config.href ? appUrl(fillTemplate(this.config.href, { ...item, id })) : '',
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
/** @return {string} The "View all" href, or ''. */
|
|
135
|
+
viewAllHref() {
|
|
136
|
+
const viewAll = this.config.viewAll
|
|
137
|
+
return viewAll && viewAll.href ? appUrl(viewAll.href) : ''
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
mounted() {
|
|
142
|
+
this.load()
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
methods: {
|
|
146
|
+
t,
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A config text through the host translate.
|
|
150
|
+
*
|
|
151
|
+
* @param {unknown} value The text.
|
|
152
|
+
* @return {string}
|
|
153
|
+
*/
|
|
154
|
+
text(value) {
|
|
155
|
+
return typeof value === 'string' && value !== '' ? this.cnTranslate(value) : ''
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* A template filled from a row, translated first.
|
|
160
|
+
*
|
|
161
|
+
* @param {unknown} template The template.
|
|
162
|
+
* @param {object} item The row.
|
|
163
|
+
* @return {string}
|
|
164
|
+
*/
|
|
165
|
+
fill(template, item) {
|
|
166
|
+
return typeof template === 'string' && template !== '' ? fillTemplate(this.cnTranslate(template), item).trim() : ''
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Read the rows: OpenRegister objects or an endpoint.
|
|
171
|
+
*
|
|
172
|
+
* @spec openspec/changes/nc-dashboard-widgets/specs/nc-dashboard-widgets/spec.md#requirement-dashboard-widgets-are-configuration
|
|
173
|
+
* @return {Promise<void>}
|
|
174
|
+
*/
|
|
175
|
+
async load() {
|
|
176
|
+
const source = this.config.source || {}
|
|
177
|
+
this.loading = true
|
|
178
|
+
this.error = false
|
|
179
|
+
try {
|
|
180
|
+
// Tokens nested below an operator map (`'@self': { lastUserActivity: { lt: '@today-7d' } }`)
|
|
181
|
+
// resolve here; the endpoint path resolves one level only.
|
|
182
|
+
const params = resolveDeepTokens(source.params || {}, {})
|
|
183
|
+
const request = source.register && source.schema
|
|
184
|
+
? { url: `/apps/openregister/api/objects/${source.register}/${source.schema}`, params: { _limit: this.limit, ...params } }
|
|
185
|
+
: { ...source, params }
|
|
186
|
+
const data = await fetchEndpointSource(request, {}, { fresh: true })
|
|
187
|
+
const list = readPath(data, this.config.itemsPath || (source.register ? 'results' : '')) ?? data
|
|
188
|
+
this.items = Array.isArray(list) ? list.filter((item) => item && typeof item === 'object') : []
|
|
189
|
+
} catch {
|
|
190
|
+
this.error = true
|
|
191
|
+
this.items = []
|
|
192
|
+
} finally {
|
|
193
|
+
this.loading = false
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Open a row's page.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} row The row.
|
|
201
|
+
* @param {MouseEvent} event The click.
|
|
202
|
+
*/
|
|
203
|
+
onRowClick(row, event) {
|
|
204
|
+
if (row && row.href) {
|
|
205
|
+
openRowTarget(event, row.href)
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
</script>
|
|
211
|
+
|
|
212
|
+
<style scoped>
|
|
213
|
+
.cn-nc-dashboard-list__error {
|
|
214
|
+
margin: 0;
|
|
215
|
+
padding: 8px 0;
|
|
216
|
+
color: var(--color-text-maxcontrast);
|
|
217
|
+
}
|
|
218
|
+
</style>
|