@conduction/nextcloud-vue 2.70.0 → 2.72.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/CnAppRoot/CnAppRoot.vue2.js +151 -13
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +38 -42
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js +2 -0
- package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js +7 -0
- package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js +10 -0
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js.map +1 -0
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js +150 -0
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js.map +1 -0
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js +54 -0
- package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +29 -2
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +71 -34
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +22 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +46 -32
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +30 -9
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/composables/useWalkthrough.js +11 -3
- package/dist/esm/composables/useWalkthrough.js.map +1 -1
- package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js +7 -10
- package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js +23 -13
- package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js.map +1 -1
- package/dist/esm/dialogs/CnRunNodeDialog.vue2.js +8 -6
- package/dist/esm/dialogs/CnRunNodeDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnRunNodeDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +15 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +15 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +117 -0
- package/dist/esm/utils/columnFilters.js +54 -4
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/selectOptions.js +57 -0
- package/dist/esm/utils/selectOptions.js.map +1 -0
- package/dist/esm/utils/userAutocomplete.js +69 -3
- package/dist/esm/utils/userAutocomplete.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +6560 -5880
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +117 -0
- package/l10n/en.json +15 -1
- package/l10n/nl.json +15 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.vue +153 -13
- package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -18
- package/src/components/CnFlowDetail/CnRunDetailSidebar.vue +6 -0
- package/src/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue +250 -0
- package/src/components/CnFlowStepOutcomes/index.js +8 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +62 -2
- package/src/components/CnIndexPage/CnIndexPage.vue +23 -1
- package/src/components/CnObjectSidebar/CnNotesTab.vue +1 -1
- package/src/components/CnSetupWizard/CnSetupWizard.vue +7 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +30 -9
- package/src/components/index.js +1 -0
- package/src/composables/useWalkthrough.js +11 -3
- package/src/css/dashboard.css +44 -0
- package/src/dialogs/CnFlowNodeEditModal.vue +41 -20
- package/src/dialogs/CnRunNodeDialog.vue +8 -6
- package/src/index.js +1 -0
- package/src/schemas/app-manifest-v2.schema.json +2 -1
- package/src/utils/columnFilters.js +54 -4
- package/src/utils/selectOptions.js +54 -0
- package/src/utils/userAutocomplete.js +68 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
CnFlowStepOutcomes: who a send step reached, and who it did not, by name.
|
|
3
|
+
|
|
4
|
+
A messaging step (send email, send notification, post to Talk) writes its
|
|
5
|
+
per-recipient outcome onto its run-log entry as `report.messaging`: one
|
|
6
|
+
bucket per outcome, each `{count, sample}`. Nothing rendered it, so the run
|
|
7
|
+
log could not show that a person was skipped because they opted out, or
|
|
8
|
+
because the opt-out check did not answer.
|
|
9
|
+
|
|
10
|
+
Only buckets with someone in them are listed. A bucket this component does
|
|
11
|
+
not know is still shown, under its own key made readable, so a new outcome
|
|
12
|
+
on the server is never hidden by an older library.
|
|
13
|
+
|
|
14
|
+
SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
15
|
+
SPDX-License-Identifier: EUPL-1.2
|
|
16
|
+
-->
|
|
17
|
+
<template>
|
|
18
|
+
<div v-if="rows.length" class="cn-flow-step-outcomes" data-testid="flow-step-outcomes">
|
|
19
|
+
<dl class="cn-flow-step-outcomes__list" :aria-label="t('nextcloud-vue', 'Who this step reached')">
|
|
20
|
+
<div v-for="row in rows"
|
|
21
|
+
:key="row.key"
|
|
22
|
+
class="cn-flow-step-outcomes__row"
|
|
23
|
+
:class="`cn-flow-step-outcomes__row--${row.tone}`"
|
|
24
|
+
:data-testid="`flow-step-outcome-${row.key}`">
|
|
25
|
+
<dt class="cn-flow-step-outcomes__name">
|
|
26
|
+
{{ row.label }}
|
|
27
|
+
</dt>
|
|
28
|
+
<dd class="cn-flow-step-outcomes__value">
|
|
29
|
+
<span class="cn-flow-step-outcomes__count">{{ row.count }}</span>
|
|
30
|
+
<span v-if="row.sample.length" class="cn-flow-step-outcomes__sample"> · {{ row.sample.join(', ') }}</span>
|
|
31
|
+
</dd>
|
|
32
|
+
</div>
|
|
33
|
+
</dl>
|
|
34
|
+
<p v-if="report && report.truncated"
|
|
35
|
+
class="cn-flow-step-outcomes__truncated"
|
|
36
|
+
data-testid="flow-step-outcomes-truncated">
|
|
37
|
+
{{ t('nextcloud-vue', 'Each list shows its first entries only.') }}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The buckets a messaging report carries, in reading order: who got it first,
|
|
47
|
+
* then each reason someone did not. `tone` marks the row for styling only.
|
|
48
|
+
*/
|
|
49
|
+
const KNOWN_BUCKETS = [
|
|
50
|
+
{ key: 'delivered', tone: 'success' },
|
|
51
|
+
{ key: 'optedOut', tone: 'neutral' },
|
|
52
|
+
{ key: 'authorityUnavailable', tone: 'warning' },
|
|
53
|
+
{ key: 'skippedByPreference', tone: 'neutral' },
|
|
54
|
+
{ key: 'skippedByKillSwitch', tone: 'neutral' },
|
|
55
|
+
{ key: 'rateLimited', tone: 'warning' },
|
|
56
|
+
{ key: 'refusedRecipients', tone: 'warning' },
|
|
57
|
+
{ key: 'unknownRecipients', tone: 'warning' },
|
|
58
|
+
{ key: 'failed', tone: 'error' },
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
/** Report keys that are facts about the send, not outcome buckets. */
|
|
62
|
+
const NOT_BUCKETS = ['channel', 'actor', 'recipients', 'truncated']
|
|
63
|
+
|
|
64
|
+
export default {
|
|
65
|
+
name: 'CnFlowStepOutcomes',
|
|
66
|
+
|
|
67
|
+
props: {
|
|
68
|
+
/**
|
|
69
|
+
* A step's messaging report: the run-log entry's `report.messaging`.
|
|
70
|
+
* Each outcome bucket is `{count, sample}`; a `refusedRecipients`
|
|
71
|
+
* sample entry is `{recipient, reason}`. `null` renders nothing.
|
|
72
|
+
*/
|
|
73
|
+
report: {
|
|
74
|
+
type: Object,
|
|
75
|
+
default: null,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Names to use instead of the built-in ones, keyed by bucket
|
|
80
|
+
* (`{ optedOut: 'Unsubscribed' }`). Pass already-translated text.
|
|
81
|
+
*/
|
|
82
|
+
labels: {
|
|
83
|
+
type: Object,
|
|
84
|
+
default: () => ({}),
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
computed: {
|
|
89
|
+
/**
|
|
90
|
+
* The non-empty buckets, known ones first in reading order, then any
|
|
91
|
+
* bucket the server added that this component does not know.
|
|
92
|
+
*
|
|
93
|
+
* @return {Array<object>} One row per bucket: key, label, count, sample, tone.
|
|
94
|
+
*/
|
|
95
|
+
rows() {
|
|
96
|
+
const report = this.report
|
|
97
|
+
if (!report || typeof report !== 'object') {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const known = KNOWN_BUCKETS.map((bucket) => bucket.key)
|
|
102
|
+
const extra = Object.keys(report).filter((key) => !known.includes(key) && !NOT_BUCKETS.includes(key))
|
|
103
|
+
const ordered = [
|
|
104
|
+
...KNOWN_BUCKETS,
|
|
105
|
+
...extra.map((key) => ({ key, tone: 'neutral' })),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
return ordered
|
|
109
|
+
.map(({ key, tone }) => ({ key, tone, bucket: report[key] }))
|
|
110
|
+
.filter(({ bucket }) => bucket && typeof bucket === 'object' && Number(bucket.count) > 0)
|
|
111
|
+
.map(({ key, tone, bucket }) => ({
|
|
112
|
+
key,
|
|
113
|
+
tone,
|
|
114
|
+
label: this.labelFor(key),
|
|
115
|
+
count: Number(bucket.count),
|
|
116
|
+
sample: (Array.isArray(bucket.sample) ? bucket.sample : []).map((entry) => this.describeEntry(entry)),
|
|
117
|
+
}))
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
methods: {
|
|
122
|
+
t,
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The readable name of a bucket: the host's, then the built-in one,
|
|
126
|
+
* then the key itself (`heldForReview` reads "Held for review").
|
|
127
|
+
*
|
|
128
|
+
* @param {string} key The bucket key.
|
|
129
|
+
* @return {string} The name.
|
|
130
|
+
*/
|
|
131
|
+
labelFor(key) {
|
|
132
|
+
if (this.labels && this.labels[key]) {
|
|
133
|
+
return String(this.labels[key])
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const builtIn = {
|
|
137
|
+
delivered: t('nextcloud-vue', 'Delivered'),
|
|
138
|
+
optedOut: t('nextcloud-vue', 'Opted out'),
|
|
139
|
+
authorityUnavailable: t('nextcloud-vue', 'Not sent, the opt-out check did not answer'),
|
|
140
|
+
skippedByPreference: t('nextcloud-vue', 'Skipped by their notification settings'),
|
|
141
|
+
skippedByKillSwitch: t('nextcloud-vue', 'Skipped, sending is switched off'),
|
|
142
|
+
rateLimited: t('nextcloud-vue', 'Held back by the send limit'),
|
|
143
|
+
refusedRecipients: t('nextcloud-vue', 'Refused by the step’s address rule'),
|
|
144
|
+
unknownRecipients: t('nextcloud-vue', 'Not a known user or group'),
|
|
145
|
+
failed: t('nextcloud-vue', 'Failed'),
|
|
146
|
+
}
|
|
147
|
+
if (builtIn[key]) {
|
|
148
|
+
return builtIn[key]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const words = String(key).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[-_]+/g, ' ').trim().toLowerCase()
|
|
152
|
+
|
|
153
|
+
return words.charAt(0).toUpperCase() + words.slice(1)
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* One sample entry as text. A refused address carries its reason.
|
|
158
|
+
*
|
|
159
|
+
* @param {string|object} entry The sample entry.
|
|
160
|
+
* @return {string} The text.
|
|
161
|
+
*/
|
|
162
|
+
describeEntry(entry) {
|
|
163
|
+
if (entry && typeof entry === 'object') {
|
|
164
|
+
const who = String(entry.recipient ?? entry.id ?? '')
|
|
165
|
+
const reason = this.reasonFor(entry.reason)
|
|
166
|
+
|
|
167
|
+
return reason ? `${who} (${reason})` : who
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return String(entry)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Why the step's address rule refused an address, in words.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} [code] The refusal code.
|
|
177
|
+
* @return {string} The reason, or the code when it is not known.
|
|
178
|
+
*/
|
|
179
|
+
reasonFor(code) {
|
|
180
|
+
const reasons = {
|
|
181
|
+
'external-recipients-off': t('nextcloud-vue', 'external recipients are off'),
|
|
182
|
+
'not-on-item': t('nextcloud-vue', 'not on the item'),
|
|
183
|
+
'invalid-address': t('nextcloud-vue', 'not a valid address'),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return code ? (reasons[code] || String(code)) : ''
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
</script>
|
|
191
|
+
|
|
192
|
+
<style scoped>
|
|
193
|
+
.cn-flow-step-outcomes {
|
|
194
|
+
margin: 4px 0 0;
|
|
195
|
+
font-size: 0.9em;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.cn-flow-step-outcomes__list {
|
|
199
|
+
margin: 0;
|
|
200
|
+
padding: 0;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Stacked, name over value: the run sidebar is narrow, and a side-by-side
|
|
204
|
+
row wrapped mid-list. The host's dt/dd margins and alignment are reset. */
|
|
205
|
+
.cn-flow-step-outcomes__row {
|
|
206
|
+
display: block;
|
|
207
|
+
margin-block: 4px;
|
|
208
|
+
padding-block: 2px;
|
|
209
|
+
padding-inline-start: 8px;
|
|
210
|
+
border-inline-start: 3px solid var(--color-border);
|
|
211
|
+
text-align: start;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.cn-flow-step-outcomes__row--success {
|
|
215
|
+
border-inline-start-color: var(--color-success);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.cn-flow-step-outcomes__row--warning {
|
|
219
|
+
border-inline-start-color: var(--color-warning);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.cn-flow-step-outcomes__row--error {
|
|
223
|
+
border-inline-start-color: var(--color-error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cn-flow-step-outcomes__name {
|
|
227
|
+
display: block;
|
|
228
|
+
white-space: normal;
|
|
229
|
+
overflow-wrap: anywhere;
|
|
230
|
+
margin: 0;
|
|
231
|
+
padding: 0;
|
|
232
|
+
font-weight: bold;
|
|
233
|
+
text-align: start;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.cn-flow-step-outcomes__value {
|
|
237
|
+
display: block;
|
|
238
|
+
white-space: normal;
|
|
239
|
+
margin: 0;
|
|
240
|
+
padding: 0;
|
|
241
|
+
text-align: start;
|
|
242
|
+
color: var(--color-main-text);
|
|
243
|
+
overflow-wrap: anywhere;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.cn-flow-step-outcomes__truncated {
|
|
247
|
+
margin: 4px 0 0;
|
|
248
|
+
color: var(--color-text-maxcontrast);
|
|
249
|
+
}
|
|
250
|
+
</style>
|
|
@@ -250,6 +250,17 @@
|
|
|
250
250
|
<!-- @binding {object} option The option being rendered (`{ id, label }`). -->
|
|
251
251
|
<slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
|
|
252
252
|
</template>
|
|
253
|
+
<!-- NcSelect's own option splits a label in two spans
|
|
254
|
+
(NcEllipsisedOption, keeping its last characters
|
|
255
|
+
visible), and in a narrow dropdown the halves broke
|
|
256
|
+
onto two lines mid-word: "Agricu lture". A plain
|
|
257
|
+
label wraps at spaces only. NcSelectUsers keeps its
|
|
258
|
+
avatar rendering. -->
|
|
259
|
+
<template
|
|
260
|
+
v-else-if="!isUserField(field)"
|
|
261
|
+
#option="optionProps">
|
|
262
|
+
<span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
|
|
263
|
+
</template>
|
|
253
264
|
<template
|
|
254
265
|
v-if="$slots['field-' + field.key + '-selected-option']"
|
|
255
266
|
#selected-option="optionProps">
|
|
@@ -284,11 +295,17 @@
|
|
|
284
295
|
#option="optionProps">
|
|
285
296
|
<slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
|
|
286
297
|
</template>
|
|
298
|
+
<template v-else #option="optionProps">
|
|
299
|
+
<span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
|
|
300
|
+
</template>
|
|
287
301
|
<template
|
|
288
302
|
v-if="$slots['field-' + field.key + '-selected-option']"
|
|
289
303
|
#selected-option="optionProps">
|
|
290
304
|
<slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
|
|
291
305
|
</template>
|
|
306
|
+
<template v-else #selected-option="optionProps">
|
|
307
|
+
<span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
|
|
308
|
+
</template>
|
|
292
309
|
</NcSelect>
|
|
293
310
|
<CnFieldHelper
|
|
294
311
|
:text="field.description"
|
|
@@ -318,11 +335,17 @@
|
|
|
318
335
|
#option="optionProps">
|
|
319
336
|
<slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
|
|
320
337
|
</template>
|
|
338
|
+
<template v-else #option="optionProps">
|
|
339
|
+
<span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
|
|
340
|
+
</template>
|
|
321
341
|
<template
|
|
322
342
|
v-if="$slots['field-' + field.key + '-selected-option']"
|
|
323
343
|
#selected-option="optionProps">
|
|
324
344
|
<slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
|
|
325
345
|
</template>
|
|
346
|
+
<template v-else #selected-option="optionProps">
|
|
347
|
+
<span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
|
|
348
|
+
</template>
|
|
326
349
|
</NcSelect>
|
|
327
350
|
<CnFieldHelper
|
|
328
351
|
:text="field.description"
|
|
@@ -2452,6 +2475,26 @@ export default {
|
|
|
2452
2475
|
* @param {object} field The field definition
|
|
2453
2476
|
* @return {boolean}
|
|
2454
2477
|
*/
|
|
2478
|
+
/**
|
|
2479
|
+
* The text a select option shows: its label, else a display name, a
|
|
2480
|
+
* name or the id. A plain string option (a tag) is its own text.
|
|
2481
|
+
*
|
|
2482
|
+
* @spec openspec/changes/audit-round-lib-fixes/specs/schema-utilities/spec.md
|
|
2483
|
+
* @param {object|string} option The option (slot props).
|
|
2484
|
+
* @return {string} The text.
|
|
2485
|
+
*/
|
|
2486
|
+
optionText(option) {
|
|
2487
|
+
if (option === null || option === undefined) {
|
|
2488
|
+
return ''
|
|
2489
|
+
}
|
|
2490
|
+
if (typeof option !== 'object') {
|
|
2491
|
+
return String(option)
|
|
2492
|
+
}
|
|
2493
|
+
const text = [option.label, option.displayName, option.name, option.id]
|
|
2494
|
+
.find((v) => v !== undefined && v !== null && v !== '')
|
|
2495
|
+
return text === undefined ? '' : String(text)
|
|
2496
|
+
},
|
|
2497
|
+
|
|
2455
2498
|
isUserField(field) {
|
|
2456
2499
|
return !!(field && field.userPicker && !field.userPicker.multiple)
|
|
2457
2500
|
},
|
|
@@ -3519,8 +3562,15 @@ export default {
|
|
|
3519
3562
|
if (this.isIdPickerField(field)) {
|
|
3520
3563
|
// Reference / user fields store the chosen id (UUID / UID),
|
|
3521
3564
|
// not the full option. Cache its label so the selection displays.
|
|
3565
|
+
// A user option can carry only `displayName` (NcSelectUsers' own
|
|
3566
|
+
// shape), and without that fallback the chip showed the uid.
|
|
3522
3567
|
if (option && option.id) {
|
|
3523
|
-
|
|
3568
|
+
const name = option.label || option.displayName || this.referenceLabels[option.id]
|
|
3569
|
+
if (name) {
|
|
3570
|
+
this.referenceLabels = { ...this.referenceLabels, [option.id]: name }
|
|
3571
|
+
} else if (this.isUserField(field)) {
|
|
3572
|
+
this.resolveUserLabel(option.id)
|
|
3573
|
+
}
|
|
3524
3574
|
}
|
|
3525
3575
|
this.updateField(field.key, option ? option.id : null)
|
|
3526
3576
|
// Reference options are label-only ({id,label}) — pass the id so
|
|
@@ -3587,7 +3637,7 @@ export default {
|
|
|
3587
3637
|
const labels = {}
|
|
3588
3638
|
for (const o of list) {
|
|
3589
3639
|
if (o && o.id) {
|
|
3590
|
-
labels[o.id] = o.label || String(o.id)
|
|
3640
|
+
labels[o.id] = o.label || o.displayName || this.referenceLabels[o.id] || String(o.id)
|
|
3591
3641
|
}
|
|
3592
3642
|
}
|
|
3593
3643
|
if (Object.keys(labels).length > 0) {
|
|
@@ -3861,6 +3911,16 @@ export default {
|
|
|
3861
3911
|
</script>
|
|
3862
3912
|
|
|
3863
3913
|
<style scoped>
|
|
3914
|
+
/* A select option breaks between words only, never inside one. */
|
|
3915
|
+
.cn-form-dialog__option {
|
|
3916
|
+
display: block;
|
|
3917
|
+
min-width: 0;
|
|
3918
|
+
white-space: normal;
|
|
3919
|
+
overflow-wrap: normal;
|
|
3920
|
+
word-break: normal;
|
|
3921
|
+
hyphens: manual;
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3864
3924
|
.cn-form-dialog__draft-actions {
|
|
3865
3925
|
display: flex;
|
|
3866
3926
|
gap: 8px;
|
|
@@ -488,7 +488,7 @@
|
|
|
488
488
|
:includeColumns="includeColumns"
|
|
489
489
|
:columnOverrides="columnOverrides"
|
|
490
490
|
:rowClass="rowClass"
|
|
491
|
-
:filterable="
|
|
491
|
+
:filterable="tableHeaderFilters"
|
|
492
492
|
:activeFilters="effectiveActiveFilters"
|
|
493
493
|
:filterRegister="typeof register === 'string' ? register : ''"
|
|
494
494
|
@columnFilter="onColumnFilterEvent"
|
|
@@ -3873,6 +3873,28 @@ export default {
|
|
|
3873
3873
|
},
|
|
3874
3874
|
|
|
3875
3875
|
/** Resolved schema OBJECT (for column generation / icons / labels). */
|
|
3876
|
+
/**
|
|
3877
|
+
* Whether the table shows its header filters. A self-fetching page
|
|
3878
|
+
* applies them itself, with or without a resolved schema (the columns
|
|
3879
|
+
* then say how each one filters, else a text filter on contains). A
|
|
3880
|
+
* host-fed table shows them when a schema came along, as before, or
|
|
3881
|
+
* when the host listens for `filter-change`; a filter nobody applies
|
|
3882
|
+
* would be a control that does nothing.
|
|
3883
|
+
*
|
|
3884
|
+
* @spec openspec/changes/audit-round-lib-fixes/specs/cn-data-table/spec.md
|
|
3885
|
+
* @return {boolean} True when header filters show.
|
|
3886
|
+
*/
|
|
3887
|
+
tableHeaderFilters() {
|
|
3888
|
+
if (!this.headerFilters) {
|
|
3889
|
+
return false
|
|
3890
|
+
}
|
|
3891
|
+
if (this.effectiveSchema || this.isSelfFetchMode) {
|
|
3892
|
+
return true
|
|
3893
|
+
}
|
|
3894
|
+
const vnodeProps = (this.$ && this.$.vnode && this.$.vnode.props) || {}
|
|
3895
|
+
return typeof vnodeProps.onFilterChange === 'function' || Array.isArray(vnodeProps.onFilterChange)
|
|
3896
|
+
},
|
|
3897
|
+
|
|
3876
3898
|
effectiveSchema() {
|
|
3877
3899
|
if (this.isSelfFetchMode) {
|
|
3878
3900
|
return this.list.schema.value
|
|
@@ -279,7 +279,7 @@ export default {
|
|
|
279
279
|
* @param {(suggestions: Array<object>) => void} callback Receives the suggestion array.
|
|
280
280
|
*/
|
|
281
281
|
async fetchMentionSuggestions(search, callback) {
|
|
282
|
-
const users = await searchNextcloudUsers(search)
|
|
282
|
+
const users = await searchNextcloudUsers(search, { includeCurrentUser: false })
|
|
283
283
|
callback(users.map((user) => ({
|
|
284
284
|
id: user.id,
|
|
285
285
|
label: user.label,
|
|
@@ -123,6 +123,13 @@
|
|
|
123
123
|
|
|
124
124
|
<!-- config-fields -->
|
|
125
125
|
<template v-else-if="step.type === 'config-fields'">
|
|
126
|
+
<!-- The step's intro, drawn like choice and run-action steps.
|
|
127
|
+
It was left out here, so pipelinq's organisation step
|
|
128
|
+
("Your organisation details…") showed fields with no
|
|
129
|
+
word about what they were for. -->
|
|
130
|
+
<NcNoteCard v-if="step.body" type="info" data-testid="cn-setup-step-intro">
|
|
131
|
+
{{ stepBody(step) }}
|
|
132
|
+
</NcNoteCard>
|
|
126
133
|
<div
|
|
127
134
|
v-for="field in fieldsFor(step)"
|
|
128
135
|
:key="field.key"
|
|
@@ -172,6 +172,12 @@ export default {
|
|
|
172
172
|
tourId: { type: String, default: '' },
|
|
173
173
|
/** Optional resume token `{ tourId, stepId }` (refresh / cross-app hand-off). */
|
|
174
174
|
resume: { type: Object, default: null },
|
|
175
|
+
/**
|
|
176
|
+
* Whether a first-visit tour may start on its own. The host turns it
|
|
177
|
+
* off while the user's tour is paused, so a paused tour stays hidden
|
|
178
|
+
* across page loads until the user picks "Continue".
|
|
179
|
+
*/
|
|
180
|
+
autoStart: { type: Boolean, default: true },
|
|
175
181
|
/** Stacking order of the overlay. */
|
|
176
182
|
zIndex: { type: Number, default: 10000 },
|
|
177
183
|
/** Next button label. */
|
|
@@ -571,6 +577,9 @@ export default {
|
|
|
571
577
|
this.wt.start(this.tourId)
|
|
572
578
|
return
|
|
573
579
|
}
|
|
580
|
+
if (!this.autoStart) {
|
|
581
|
+
return
|
|
582
|
+
}
|
|
574
583
|
const auto = this.wt.autoStartTour.value
|
|
575
584
|
if (!auto) {
|
|
576
585
|
return
|
|
@@ -1171,31 +1180,43 @@ export default {
|
|
|
1171
1180
|
},
|
|
1172
1181
|
|
|
1173
1182
|
/**
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1183
|
+
* The corner close button PAUSES the tour, like ESC and the dim. It
|
|
1184
|
+
* used to complete it, and the host then wiped the saved step, so a
|
|
1185
|
+
* restart began at step 1 again. Only Finish on the last step (and a
|
|
1186
|
+
* host-supplied Skip) completes the tour; "Start over" in the user
|
|
1187
|
+
* settings stays the way back to step 1.
|
|
1176
1188
|
*
|
|
1189
|
+
* @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
|
|
1177
1190
|
* @return {void}
|
|
1178
1191
|
*/
|
|
1179
1192
|
close() {
|
|
1180
|
-
this.
|
|
1181
|
-
this.$emit('complete')
|
|
1193
|
+
this.onBackdrop()
|
|
1182
1194
|
},
|
|
1183
1195
|
|
|
1184
1196
|
/**
|
|
1185
|
-
* Pause the tour from a backdrop click or ESC. The tour
|
|
1186
|
-
* keeps its step; only
|
|
1197
|
+
* Pause the tour from the corner X, a backdrop click or ESC. The tour
|
|
1198
|
+
* hides and keeps its step; only Finish (or a host Skip) ends it.
|
|
1187
1199
|
* Pressing ESC used to dismiss it, and the host recorded that as
|
|
1188
|
-
* "seen", so the tour never came back.
|
|
1200
|
+
* "seen", so the tour never came back. The `pause` event carries
|
|
1201
|
+
* where the user was, so the host can keep a paused tour hidden on
|
|
1202
|
+
* the next page load.
|
|
1189
1203
|
*
|
|
1190
1204
|
* @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
|
|
1205
|
+
* @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
|
|
1191
1206
|
* @return {void}
|
|
1192
1207
|
*/
|
|
1193
1208
|
onBackdrop() {
|
|
1209
|
+
const where = {
|
|
1210
|
+
tourId: (this.wt.activeTour.value && this.wt.activeTour.value.id) || '',
|
|
1211
|
+
stepId: (this.step && this.step.id) || '',
|
|
1212
|
+
index: this.index,
|
|
1213
|
+
}
|
|
1194
1214
|
this.wt.pause()
|
|
1195
1215
|
/**
|
|
1196
|
-
* @event pause Emitted when the user hides the tour (backdrop
|
|
1216
|
+
* @event pause Emitted when the user hides the tour (X, backdrop, ESC). The tour can continue later.
|
|
1217
|
+
* @type {{ tourId: string, stepId: string, index: number }}
|
|
1197
1218
|
*/
|
|
1198
|
-
this.$emit('pause')
|
|
1219
|
+
this.$emit('pause', where)
|
|
1199
1220
|
},
|
|
1200
1221
|
},
|
|
1201
1222
|
}
|
package/src/components/index.js
CHANGED
|
@@ -191,6 +191,7 @@ export { CnDeltaWidgetForm } from './CnDeltaWidgetForm/index.js'
|
|
|
191
191
|
export { CnGaugeWidget } from './CnGaugeWidget/index.js'
|
|
192
192
|
export { CnGaugeWidgetForm } from './CnGaugeWidgetForm/index.js'
|
|
193
193
|
export { CnFlowRunsWidget } from './CnFlowRunsWidget/index.js'
|
|
194
|
+
export { CnFlowStepOutcomes } from './CnFlowStepOutcomes/index.js'
|
|
194
195
|
export { CnFlowRunsWidgetForm } from './CnFlowRunsWidgetForm/index.js'
|
|
195
196
|
export { CnTasksWidget } from './CnTasksWidget/index.js'
|
|
196
197
|
export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
|
|
@@ -216,11 +216,13 @@ export function walkthroughProgressKey(configKey) {
|
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
218
|
* Normalise stored progress (an object, or the JSON string a preference
|
|
219
|
-
* holds) into `{ tourId, stepId, index, version }`,
|
|
219
|
+
* holds) into `{ tourId, stepId, index, version }`, plus `paused: true`
|
|
220
|
+
* when the user paused the tour, or null.
|
|
220
221
|
*
|
|
221
222
|
* @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
|
|
223
|
+
* @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
|
|
222
224
|
* @param {unknown} value The stored value.
|
|
223
|
-
* @return {{tourId: string, stepId: string, index: number, version: string}|null} The progress.
|
|
225
|
+
* @return {{tourId: string, stepId: string, index: number, version: string, paused: (boolean|undefined)}|null} The progress.
|
|
224
226
|
*/
|
|
225
227
|
export function normaliseWalkthroughProgress(value) {
|
|
226
228
|
let data = value
|
|
@@ -237,12 +239,18 @@ export function normaliseWalkthroughProgress(value) {
|
|
|
237
239
|
if (!isPlainObject(data) || typeof data.tourId !== 'string' || data.tourId === '') {
|
|
238
240
|
return null
|
|
239
241
|
}
|
|
240
|
-
|
|
242
|
+
const progress = {
|
|
241
243
|
tourId: data.tourId,
|
|
242
244
|
stepId: typeof data.stepId === 'string' ? data.stepId : '',
|
|
243
245
|
index: Number.isInteger(data.index) && data.index >= 0 ? data.index : 0,
|
|
244
246
|
version: typeof data.version === 'string' ? data.version : '',
|
|
245
247
|
}
|
|
248
|
+
// A paused tour stays hidden on the next visit until the user picks
|
|
249
|
+
// "Continue". Only written when true, so older values read unchanged.
|
|
250
|
+
if (data.paused === true) {
|
|
251
|
+
progress.paused = true
|
|
252
|
+
}
|
|
253
|
+
return progress
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
/**
|
package/src/css/dashboard.css
CHANGED
|
@@ -129,3 +129,47 @@
|
|
|
129
129
|
.cn-requires-app.cn-requires-app .empty-content__action {
|
|
130
130
|
margin-top: var(--default-grid-baseline, 4px);
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
/* NcEmptyContent forces its icon's svg to 64px with !important while the
|
|
134
|
+
* rule above lets the icon box shrink, so the svg spilled out of its box and
|
|
135
|
+
* over the name. Hold the svg to the size the caller asked for. */
|
|
136
|
+
.cn-requires-app.cn-requires-app .empty-content__icon svg {
|
|
137
|
+
width: 32px !important;
|
|
138
|
+
height: 32px !important;
|
|
139
|
+
max-width: 32px !important;
|
|
140
|
+
max-height: 32px !important;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* The dashboard's compact "Install <app>" placeholder: two lines of text and
|
|
144
|
+
* the button, nothing else, so it fits a two-row tile even under a title row.
|
|
145
|
+
* `safe center` keeps the top in view when a tile is smaller still, and the
|
|
146
|
+
* body scrolls rather than clipping the button. */
|
|
147
|
+
.cn-requires-app--compact {
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: safe center;
|
|
152
|
+
gap: calc(2 * var(--default-grid-baseline, 4px));
|
|
153
|
+
box-sizing: border-box;
|
|
154
|
+
height: 100%;
|
|
155
|
+
min-height: 0;
|
|
156
|
+
overflow: auto;
|
|
157
|
+
padding: calc(2 * var(--default-grid-baseline, 4px));
|
|
158
|
+
text-align: center;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.cn-requires-app--compact .cn-requires-app__description {
|
|
162
|
+
display: -webkit-box;
|
|
163
|
+
margin: 0;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
color: var(--color-text-maxcontrast);
|
|
166
|
+
font-size: 0.9em;
|
|
167
|
+
line-height: 1.4;
|
|
168
|
+
-webkit-box-orient: vertical;
|
|
169
|
+
-webkit-line-clamp: 2;
|
|
170
|
+
line-clamp: 2;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.cn-requires-app--compact .button-vue {
|
|
174
|
+
flex-shrink: 0;
|
|
175
|
+
}
|