@conduction/nextcloud-vue 2.56.0-beta.20 → 2.56.0-beta.21
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/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/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 +64 -0
- package/dist/esm/utils/selectOptions.js +57 -0
- package/dist/esm/utils/selectOptions.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +5945 -5645
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +64 -0
- package/l10n/en.json +15 -1
- package/l10n/nl.json +15 -1
- package/package.json +1 -1
- 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/index.js +1 -0
- package/src/dialogs/CnFlowNodeEditModal.vue +41 -20
- package/src/dialogs/CnRunNodeDialog.vue +8 -6
- package/src/index.js +1 -0
- package/src/utils/selectOptions.js +54 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/nextcloud-vue.cjs.js.map
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35684577 bytes
|
|
4
|
+
sha256: 40220f92c6defbd478644408d9d23a88047c47798fb52eefee28e6d70e0fbc9a
|
package/dist/nextcloud-vue.css
CHANGED
|
@@ -26582,6 +26582,18 @@ a.cn-flow-runs-widget__row-content:hover .cn-flow-runs-widget__name[data-v-75628
|
|
|
26582
26582
|
font-size: 0.85em;
|
|
26583
26583
|
color: var(--color-text-maxcontrast);
|
|
26584
26584
|
}
|
|
26585
|
+
|
|
26586
|
+
/* Full width, like the text fields around it: at its natural width the
|
|
26587
|
+
picker cut every option label in half. */
|
|
26588
|
+
.cn-flow-node-edit__select[data-v-e107484a] {
|
|
26589
|
+
width: 100%;
|
|
26590
|
+
}
|
|
26591
|
+
.cn-flow-node-edit__field-help[data-v-e107484a] {
|
|
26592
|
+
margin: 4px 0 0;
|
|
26593
|
+
padding-inline-start: 8px;
|
|
26594
|
+
font-size: 0.85em;
|
|
26595
|
+
color: var(--color-text-maxcontrast);
|
|
26596
|
+
}
|
|
26585
26597
|
.cn-flow-node-edit__advanced summary[data-v-e107484a] {
|
|
26586
26598
|
cursor: pointer;
|
|
26587
26599
|
color: var(--color-text-maxcontrast);
|
|
@@ -28036,6 +28048,58 @@ to {
|
|
|
28036
28048
|
}
|
|
28037
28049
|
|
|
28038
28050
|
|
|
28051
|
+
.cn-flow-step-outcomes[data-v-5ac5ad4f] {
|
|
28052
|
+
margin: 4px 0 0;
|
|
28053
|
+
font-size: 0.9em;
|
|
28054
|
+
}
|
|
28055
|
+
.cn-flow-step-outcomes__list[data-v-5ac5ad4f] {
|
|
28056
|
+
margin: 0;
|
|
28057
|
+
padding: 0;
|
|
28058
|
+
}
|
|
28059
|
+
|
|
28060
|
+
/* Stacked, name over value: the run sidebar is narrow, and a side-by-side
|
|
28061
|
+
row wrapped mid-list. The host's dt/dd margins and alignment are reset. */
|
|
28062
|
+
.cn-flow-step-outcomes__row[data-v-5ac5ad4f] {
|
|
28063
|
+
display: block;
|
|
28064
|
+
margin-block: 4px;
|
|
28065
|
+
padding-block: 2px;
|
|
28066
|
+
padding-inline-start: 8px;
|
|
28067
|
+
border-inline-start: 3px solid var(--color-border);
|
|
28068
|
+
text-align: start;
|
|
28069
|
+
}
|
|
28070
|
+
.cn-flow-step-outcomes__row--success[data-v-5ac5ad4f] {
|
|
28071
|
+
border-inline-start-color: var(--color-success);
|
|
28072
|
+
}
|
|
28073
|
+
.cn-flow-step-outcomes__row--warning[data-v-5ac5ad4f] {
|
|
28074
|
+
border-inline-start-color: var(--color-warning);
|
|
28075
|
+
}
|
|
28076
|
+
.cn-flow-step-outcomes__row--error[data-v-5ac5ad4f] {
|
|
28077
|
+
border-inline-start-color: var(--color-error);
|
|
28078
|
+
}
|
|
28079
|
+
.cn-flow-step-outcomes__name[data-v-5ac5ad4f] {
|
|
28080
|
+
display: block;
|
|
28081
|
+
white-space: normal;
|
|
28082
|
+
overflow-wrap: anywhere;
|
|
28083
|
+
margin: 0;
|
|
28084
|
+
padding: 0;
|
|
28085
|
+
font-weight: bold;
|
|
28086
|
+
text-align: start;
|
|
28087
|
+
}
|
|
28088
|
+
.cn-flow-step-outcomes__value[data-v-5ac5ad4f] {
|
|
28089
|
+
display: block;
|
|
28090
|
+
white-space: normal;
|
|
28091
|
+
margin: 0;
|
|
28092
|
+
padding: 0;
|
|
28093
|
+
text-align: start;
|
|
28094
|
+
color: var(--color-main-text);
|
|
28095
|
+
overflow-wrap: anywhere;
|
|
28096
|
+
}
|
|
28097
|
+
.cn-flow-step-outcomes__truncated[data-v-5ac5ad4f] {
|
|
28098
|
+
margin: 4px 0 0;
|
|
28099
|
+
color: var(--color-text-maxcontrast);
|
|
28100
|
+
}
|
|
28101
|
+
|
|
28102
|
+
|
|
28039
28103
|
.cn-run-sidebar[data-v-48e781f0] {
|
|
28040
28104
|
display: flex;
|
|
28041
28105
|
flex-direction: column;
|
package/l10n/en.json
CHANGED
|
@@ -1106,7 +1106,21 @@
|
|
|
1106
1106
|
"Loading…": "Loading…",
|
|
1107
1107
|
"A register and schema are required": "A register and schema are required",
|
|
1108
1108
|
"Change the search or try again.": "Change the search or try again.",
|
|
1109
|
-
"Try again later.": "Try again later."
|
|
1109
|
+
"Try again later.": "Try again later.",
|
|
1110
|
+
"Who this step reached": "Who this step reached",
|
|
1111
|
+
"Each list shows its first entries only.": "Each list shows its first entries only.",
|
|
1112
|
+
"Delivered": "Delivered",
|
|
1113
|
+
"Opted out": "Opted out",
|
|
1114
|
+
"Not sent, the opt-out check did not answer": "Not sent, the opt-out check did not answer",
|
|
1115
|
+
"Skipped by their notification settings": "Skipped by their notification settings",
|
|
1116
|
+
"Skipped, sending is switched off": "Skipped, sending is switched off",
|
|
1117
|
+
"Held back by the send limit": "Held back by the send limit",
|
|
1118
|
+
"Refused by the step’s address rule": "Refused by the step’s address rule",
|
|
1119
|
+
"Not a known user or group": "Not a known user or group",
|
|
1120
|
+
"Failed": "Failed",
|
|
1121
|
+
"external recipients are off": "external recipients are off",
|
|
1122
|
+
"not on the item": "not on the item",
|
|
1123
|
+
"not a valid address": "not a valid address"
|
|
1110
1124
|
},
|
|
1111
1125
|
"plurals": {
|
|
1112
1126
|
"%n year": ["%n year", "%n years"],
|
package/l10n/nl.json
CHANGED
|
@@ -1150,7 +1150,21 @@
|
|
|
1150
1150
|
"Loading…": "Laden…",
|
|
1151
1151
|
"A register and schema are required": "Een register en schema zijn verplicht",
|
|
1152
1152
|
"Change the search or try again.": "Pas de zoekopdracht aan of probeer het opnieuw.",
|
|
1153
|
-
"Try again later.": "Probeer het later opnieuw."
|
|
1153
|
+
"Try again later.": "Probeer het later opnieuw.",
|
|
1154
|
+
"Who this step reached": "Wie deze stap bereikte",
|
|
1155
|
+
"Each list shows its first entries only.": "Elke lijst toont alleen de eerste vermeldingen.",
|
|
1156
|
+
"Delivered": "Bezorgd",
|
|
1157
|
+
"Opted out": "Afgemeld",
|
|
1158
|
+
"Not sent, the opt-out check did not answer": "Niet verstuurd, de afmeldcontrole gaf geen antwoord",
|
|
1159
|
+
"Skipped by their notification settings": "Overgeslagen door hun meldingsinstellingen",
|
|
1160
|
+
"Skipped, sending is switched off": "Overgeslagen, verzenden staat uit",
|
|
1161
|
+
"Held back by the send limit": "Tegengehouden door de verzendlimiet",
|
|
1162
|
+
"Refused by the step’s address rule": "Geweigerd door de adresregel van de stap",
|
|
1163
|
+
"Not a known user or group": "Geen bekende gebruiker of groep",
|
|
1164
|
+
"Failed": "Mislukt",
|
|
1165
|
+
"external recipients are off": "externe ontvangers staan uit",
|
|
1166
|
+
"not on the item": "staat niet op het item",
|
|
1167
|
+
"not a valid address": "geen geldig adres"
|
|
1154
1168
|
},
|
|
1155
1169
|
"plurals": {
|
|
1156
1170
|
"%n year": ["%n jaar", "%n jaar"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.56.0-beta.
|
|
3
|
+
"version": "2.56.0-beta.21",
|
|
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": {
|
|
@@ -226,6 +226,10 @@
|
|
|
226
226
|
<strong>{{ step.transition }}</strong>
|
|
227
227
|
<span class="cn-run-sidebar__hint"> · {{ step.status }}</span>
|
|
228
228
|
<span v-if="step.error" class="cn-run-sidebar__error"> · {{ step.error }}</span>
|
|
229
|
+
<!-- A send step's outcome lists, by name: who got it, who
|
|
230
|
+
opted out, who the opt-out check could not answer for. -->
|
|
231
|
+
<CnFlowStepOutcomes v-if="step.report && step.report.messaging"
|
|
232
|
+
:report="step.report.messaging" />
|
|
229
233
|
</li>
|
|
230
234
|
</ol>
|
|
231
235
|
</component>
|
|
@@ -241,6 +245,7 @@ import CheckboxMarkedOutline from 'vue-material-design-icons/CheckboxMarkedOutli
|
|
|
241
245
|
import DatabaseOutline from 'vue-material-design-icons/DatabaseOutline.vue'
|
|
242
246
|
import FormatListBulleted from 'vue-material-design-icons/FormatListBulleted.vue'
|
|
243
247
|
import Replay from 'vue-material-design-icons/Replay.vue'
|
|
248
|
+
import CnFlowStepOutcomes from '../CnFlowStepOutcomes/CnFlowStepOutcomes.vue'
|
|
244
249
|
import { FLOW_RUN_ACTIVE_STATUSES, useFlowStore } from '../../composables/useFlowStore.js'
|
|
245
250
|
|
|
246
251
|
export default {
|
|
@@ -249,6 +254,7 @@ export default {
|
|
|
249
254
|
components: {
|
|
250
255
|
ArrowLeft,
|
|
251
256
|
CheckboxMarkedOutline,
|
|
257
|
+
CnFlowStepOutcomes,
|
|
252
258
|
DatabaseOutline,
|
|
253
259
|
FormatListBulleted,
|
|
254
260
|
NcAppSidebarTab,
|
|
@@ -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>
|
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'
|
|
@@ -40,16 +40,27 @@
|
|
|
40
40
|
:placeholder="t('nextcloud-vue', 'GET')"
|
|
41
41
|
@update:modelValue="setKey(key, $event)" />
|
|
42
42
|
|
|
43
|
-
<!-- A `select` field
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
<!-- A `select` field renders as a picker, never a bare text box.
|
|
44
|
+
Its choices come from the URL the OWNING APP declared
|
|
45
|
+
(`optionsFrom`) or from the form itself (`options`), for a
|
|
46
|
+
short fixed vocabulary with no endpoint of its own. -->
|
|
47
|
+
<template v-else-if="widgetFor(key) === 'select'">
|
|
48
|
+
<NcSelect :modelValue="selectedOption(key)"
|
|
49
|
+
class="cn-flow-node-edit__select"
|
|
50
|
+
:options="selectOptions[key] || []"
|
|
51
|
+
:inputLabel="labelFor(key)"
|
|
52
|
+
:loading="selectLoading[key] === true"
|
|
53
|
+
:placeholder="t('nextcloud-vue', 'Pick one…')"
|
|
54
|
+
@update:modelValue="setKey(key, $event ? $event.id : '')" />
|
|
55
|
+
<!-- NcSelect has no helper text of its own, so the help
|
|
56
|
+
follows the control, the next thing a screen reader
|
|
57
|
+
reads after it. -->
|
|
58
|
+
<p v-if="hintFor(key)"
|
|
59
|
+
class="cn-flow-node-edit__field-help"
|
|
60
|
+
:data-testid="`flow-node-help-${key}`">
|
|
61
|
+
{{ hintFor(key) }}
|
|
62
|
+
</p>
|
|
63
|
+
</template>
|
|
53
64
|
|
|
54
65
|
<NcTextArea v-else-if="widgetFor(key) === 'textarea'"
|
|
55
66
|
:modelValue="String(draft.config[key] ?? '')"
|
|
@@ -170,6 +181,7 @@ import {
|
|
|
170
181
|
} from '@nextcloud/vue'
|
|
171
182
|
import CnCronField from '../components/CnCronField/CnCronField.vue'
|
|
172
183
|
import { useFlowStore } from '../composables/useFlowStore.js'
|
|
184
|
+
import { hasStaticOptions, normaliseSelectOptions } from '../utils/selectOptions.js'
|
|
173
185
|
|
|
174
186
|
/** The verbs an HTTP-shaped `method` option can take. */
|
|
175
187
|
/**
|
|
@@ -287,7 +299,7 @@ export default {
|
|
|
287
299
|
|
|
288
300
|
/**
|
|
289
301
|
* The node's per-field declarations from the catalogue's `configForm`
|
|
290
|
-
* ({key, label, type, help, required, optionsFrom}), keyed by config
|
|
302
|
+
* ({key, label, type, help, required, optionsFrom, options}), keyed by config
|
|
291
303
|
* key. `configKeys` is the degraded form. Either way the node's OWNER
|
|
292
304
|
* declares the vocabulary — this dialog never invents fields.
|
|
293
305
|
*
|
|
@@ -398,7 +410,9 @@ export default {
|
|
|
398
410
|
created() {
|
|
399
411
|
// Select fields need their options; everything else is local.
|
|
400
412
|
for (const [key, spec] of Object.entries(this.fieldSpecs)) {
|
|
401
|
-
if (spec
|
|
413
|
+
if (hasStaticOptions(spec)) {
|
|
414
|
+
this.selectOptions = { ...this.selectOptions, [key]: normaliseSelectOptions(spec.options) }
|
|
415
|
+
} else if (spec?.type === 'select' && spec?.optionsFrom) {
|
|
402
416
|
this.loadSelectOptions(key, spec)
|
|
403
417
|
}
|
|
404
418
|
}
|
|
@@ -634,7 +648,7 @@ export default {
|
|
|
634
648
|
*/
|
|
635
649
|
widgetFor(key) {
|
|
636
650
|
const spec = this.fieldSpecs[key]
|
|
637
|
-
if (spec?.type === 'select' && spec?.optionsFrom) {
|
|
651
|
+
if (spec?.type === 'select' && (spec?.optionsFrom || hasStaticOptions(spec))) {
|
|
638
652
|
return 'select'
|
|
639
653
|
}
|
|
640
654
|
if (spec?.type === 'boolean') {
|
|
@@ -730,13 +744,7 @@ export default {
|
|
|
730
744
|
? generateUrl(url)
|
|
731
745
|
: url)
|
|
732
746
|
const rows = Array.isArray(response.data) ? response.data : (response.data?.results || [])
|
|
733
|
-
const options = rows
|
|
734
|
-
const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
|
|
735
|
-
return {
|
|
736
|
-
id,
|
|
737
|
-
label: row.label || row.name || row.title || String(id),
|
|
738
|
-
}
|
|
739
|
-
}).filter((o) => o.id !== undefined && o.id !== null && o.id !== '')
|
|
747
|
+
const options = normaliseSelectOptions(rows)
|
|
740
748
|
this.selectOptions = { ...this.selectOptions, [key]: options }
|
|
741
749
|
} catch (error) {
|
|
742
750
|
// A picker that could not load degrades to showing the stored
|
|
@@ -930,6 +938,19 @@ export default {
|
|
|
930
938
|
color: var(--color-text-maxcontrast);
|
|
931
939
|
}
|
|
932
940
|
|
|
941
|
+
/* Full width, like the text fields around it: at its natural width the
|
|
942
|
+
picker cut every option label in half. */
|
|
943
|
+
.cn-flow-node-edit__select {
|
|
944
|
+
width: 100%;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.cn-flow-node-edit__field-help {
|
|
948
|
+
margin: 4px 0 0;
|
|
949
|
+
padding-inline-start: 8px;
|
|
950
|
+
font-size: 0.85em;
|
|
951
|
+
color: var(--color-text-maxcontrast);
|
|
952
|
+
}
|
|
953
|
+
|
|
933
954
|
.cn-flow-node-edit__advanced summary {
|
|
934
955
|
cursor: pointer;
|
|
935
956
|
color: var(--color-text-maxcontrast);
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
import { translate as t } from '@nextcloud/l10n'
|
|
89
89
|
import { NcButton, NcCheckboxRadioSwitch, NcDialog, NcLoadingIcon, NcNoteCard, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
|
|
90
90
|
import { prefixUrl } from '../utils/headers.js'
|
|
91
|
+
import { hasStaticOptions, normaliseSelectOptions } from '../utils/selectOptions.js'
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* The dialog `run-node` opens to collect a directly-invoked flow node's own
|
|
@@ -128,7 +129,9 @@ export default {
|
|
|
128
129
|
title: { type: String, default: '' },
|
|
129
130
|
/**
|
|
130
131
|
* The node's declared config form — `IFlowNodeConfigForm::configForm()`'s
|
|
131
|
-
* shape, unchanged: `[{key, label, type, help?, required?, optionsFrom?}]`.
|
|
132
|
+
* shape, unchanged: `[{key, label, type, help?, required?, optionsFrom?, options?}]`.
|
|
133
|
+
* A `select` takes its choices from `optionsFrom` (a url) or `options`
|
|
134
|
+
* (strings, or `{value, label}` pairs declared in the form itself).
|
|
132
135
|
*
|
|
133
136
|
* @type {Array<object>}
|
|
134
137
|
*/
|
|
@@ -177,7 +180,9 @@ export default {
|
|
|
177
180
|
}
|
|
178
181
|
this.values = seed
|
|
179
182
|
for (const field of (fields || [])) {
|
|
180
|
-
if (field
|
|
183
|
+
if (hasStaticOptions(field)) {
|
|
184
|
+
this.options = { ...this.options, [field.key]: normaliseSelectOptions(field.options, (s) => this.tr(s)) }
|
|
185
|
+
} else if (field.type === 'select' && field.optionsFrom) {
|
|
181
186
|
this.loadOptions(field)
|
|
182
187
|
}
|
|
183
188
|
}
|
|
@@ -274,10 +279,7 @@ export default {
|
|
|
274
279
|
const url = prefixUrl(raw)
|
|
275
280
|
const response = await axios.get(url)
|
|
276
281
|
const rows = Array.isArray(response.data) ? response.data : (response.data?.results || [])
|
|
277
|
-
const opts = rows
|
|
278
|
-
const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
|
|
279
|
-
return { id, label: row.label || row.name || row.title || String(id) }
|
|
280
|
-
}).filter((o) => o.id !== undefined && o.id !== null && o.id !== '')
|
|
282
|
+
const opts = normaliseSelectOptions(rows)
|
|
281
283
|
this.options = { ...this.options, [field.key]: opts }
|
|
282
284
|
} catch (error) {
|
|
283
285
|
// A picker that fails to load degrades to an empty list rather
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The choices a node's `select` field offers, in the `{id, label}` shape
|
|
6
|
+
* NcSelect takes.
|
|
7
|
+
*
|
|
8
|
+
* A field gets its choices one of two ways. `optionsFrom` names a url the
|
|
9
|
+
* owning app answers. `options` lists them in the form itself, for a short
|
|
10
|
+
* fixed vocabulary that has no endpoint of its own. Both dialogs that render a
|
|
11
|
+
* node's configForm read the same shapes through this module, so the two
|
|
12
|
+
* cannot come to accept different ones.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalise rows into `{id, label}` options.
|
|
17
|
+
*
|
|
18
|
+
* Accepts plain strings and numbers, `{id, label}`, `{value, label}`, and
|
|
19
|
+
* OpenRegister objects (uuid from `@self`, label from name or title). A row
|
|
20
|
+
* without an id is dropped rather than shown as a blank option.
|
|
21
|
+
*
|
|
22
|
+
* @param {Array<unknown>} rows The raw rows.
|
|
23
|
+
* @param {(label: string) => string} [translate] Applied to each declared label.
|
|
24
|
+
* @return {Array<{id: string|number, label: string}>} The options.
|
|
25
|
+
*/
|
|
26
|
+
export function normaliseSelectOptions(rows, translate = (s) => s) {
|
|
27
|
+
if (!Array.isArray(rows)) {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return rows.map((row) => {
|
|
32
|
+
if (typeof row === 'string' || typeof row === 'number') {
|
|
33
|
+
return { id: row, label: String(row) }
|
|
34
|
+
}
|
|
35
|
+
if (!row || typeof row !== 'object') {
|
|
36
|
+
return { id: undefined, label: '' }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
|
|
40
|
+
const declared = row.label || row.name || row.title
|
|
41
|
+
|
|
42
|
+
return { id, label: declared ? String(translate(declared)) : String(id) }
|
|
43
|
+
}).filter((option) => option.id !== undefined && option.id !== null && option.id !== '')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether a field declares its choices in the form itself.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} field The field declaration.
|
|
50
|
+
* @return {boolean} True for a `select` with an `options` list.
|
|
51
|
+
*/
|
|
52
|
+
export function hasStaticOptions(field) {
|
|
53
|
+
return field?.type === 'select' && Array.isArray(field?.options)
|
|
54
|
+
}
|