@uipath/maestro-builder-sdk 6.24.0 → 6.25.1
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/api-index.md +115 -115
- package/dist/api-members.md +351 -351
- package/dist/case/compile-cli.js +58 -2
- package/dist/check.js +1 -392
- package/dist/core/actions.d.ts +0 -304
- package/dist/core/actions.js +0 -54
- package/dist/core/form-field-type.d.ts +16 -0
- package/dist/core/form-field-type.js +33 -0
- package/dist/core/hitl-routing.d.ts +17 -3
- package/dist/core/stable-id.d.ts +9 -0
- package/dist/core/stable-id.js +16 -0
- package/dist/core/step-ports.d.ts +25 -1
- package/dist/core/step-ports.js +6 -47
- package/dist/core/typed-raw-node.js +0 -8
- package/dist/decompile.js +7 -191
- package/dist/flow-node-registry.js +2 -1
- package/dist/flow-node-type.d.ts +45 -1
- package/dist/flow-sdk.d.ts +1 -0
- package/dist/flow-sdk.js +1 -0
- package/dist/flow-step-exits.js +1 -1
- package/dist/nodes/hitl/flow/check.d.ts +38 -0
- package/dist/nodes/hitl/flow/check.js +357 -0
- package/dist/nodes/hitl/flow/factories.d.ts +58 -0
- package/dist/nodes/hitl/flow/factories.js +59 -0
- package/dist/nodes/hitl/flow/family.d.ts +3 -0
- package/dist/nodes/hitl/flow/family.js +8 -0
- package/dist/nodes/hitl/flow/index.d.ts +3 -0
- package/dist/nodes/hitl/flow/index.js +3 -0
- package/dist/nodes/hitl/flow/node-type.d.ts +7 -0
- package/dist/nodes/hitl/flow/node-type.js +601 -0
- package/dist/nodes/hitl/flow/spec.d.ts +265 -0
- package/dist/nodes/hitl/flow/spec.js +1 -0
- package/dist/serialize.d.ts +2 -4
- package/dist/serialize.js +35 -344
- package/package.json +2 -2
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { Expr } from '../../../core/expr.js';
|
|
2
|
+
import { nearestName } from '../../../core/edit-distance.js';
|
|
3
|
+
import { FORM_FIELD_TYPES, formFieldTypeHint, formFieldTypeWhy } from '../../../core/form-field-type.js';
|
|
4
|
+
import { isAnswerField } from '../../../core/hitl-answers.js';
|
|
5
|
+
import { hitlDerivesOutcomeHandles, outcomeIdOf } from '../../../core/hitl-routing.js';
|
|
6
|
+
export const HITL_DIAGNOSTICS = {
|
|
7
|
+
HITL_VARIANT_UNKNOWN: { severity: 'error', summary: 'a variant that names no human-task node type' },
|
|
8
|
+
HITL_OUTCOME_PORTS_VARIANT: { severity: 'error', summary: 'exposeError or outcomePorts on a variant that cannot honour it' },
|
|
9
|
+
HITL_DOCUMENT_WITHOUT_VARIANT: { severity: 'error', summary: 'a document on a task that is not a validation station' },
|
|
10
|
+
HITL_DOCVAL_WITH_FIELDS: { severity: 'error', summary: 'form fields on a validation station' },
|
|
11
|
+
HITL_DOCVAL_NO_DOCUMENT: { severity: 'error', summary: 'a validation station with no document' },
|
|
12
|
+
DOCVAL_NO_EXTRACTION: { severity: 'error', summary: 'a validation station with no extractionResult' },
|
|
13
|
+
DOCVAL_TAXONOMY_REQUIRED: { severity: 'error', summary: 'a custom-rendered validation station with no taxonomy' },
|
|
14
|
+
DOCVAL_RENDER_UNKNOWN: { severity: 'error', summary: 'a render type the station does not know' },
|
|
15
|
+
HITL_APP_WITHOUT_VARIANT: { severity: 'error', summary: 'an app on a task whose form lives in the flow' },
|
|
16
|
+
HITL_APP_WITH_FIELDS: { severity: 'error', summary: 'form fields on an app-backed task' },
|
|
17
|
+
HITL_APP_MISSING: { severity: 'error', summary: 'an action-app task that names no app' },
|
|
18
|
+
HITL_APP_INCOMPLETE: { severity: 'error', summary: 'an app reference missing name, key or folderPath' },
|
|
19
|
+
HITL_NO_FIELDS: { severity: 'error', summary: 'no fields array' },
|
|
20
|
+
HITL_FIELD_NO_ID: { severity: 'error', summary: 'a form field with no id' },
|
|
21
|
+
HITL_DUP_FIELD: { severity: 'error', summary: 'a form field id declared twice' },
|
|
22
|
+
HITL_FIELD_RESERVED_ID: { severity: 'error', summary: 'a form field id the result already uses' },
|
|
23
|
+
HITL_FIELD_TYPE_UNKNOWN: { severity: 'error', summary: 'a form field type the platform does not recognize' },
|
|
24
|
+
HITL_FIELD_NO_VALUE: { severity: 'error', summary: 'a shown field with no value' },
|
|
25
|
+
HITL_FIELD_VALUE_NOT_REF: { severity: 'error', summary: 'a shown field filled from a literal' },
|
|
26
|
+
HITL_FIELD_VALUE_NOT_PATH: { severity: 'error', summary: 'a shown field filled from a computed expression' },
|
|
27
|
+
HITL_FIELD_DIRECTION: { severity: 'error', summary: 'a field direction that is not input, output or inOut' },
|
|
28
|
+
HITL_NO_OUTCOMES: { severity: 'error', summary: 'no outcomes, so the task never completes' },
|
|
29
|
+
HITL_OUTCOME_BAD_ID: { severity: 'error', summary: 'an outcome id that is not a non-empty string' },
|
|
30
|
+
HITL_DUP_OUTCOME_ID: { severity: 'error', summary: 'two outcomes on one id' },
|
|
31
|
+
HITL_OUTCOME_NO_NAME: { severity: 'error', summary: 'an outcome with no name' },
|
|
32
|
+
HITL_DUP_OUTCOME: { severity: 'error', summary: 'an outcome name declared twice' },
|
|
33
|
+
HITL_OUTCOME_ACTION: { severity: 'error', summary: 'an outcome action other than Continue or End' },
|
|
34
|
+
HITL_CHANNEL_UNKNOWN: { severity: 'error', summary: 'a delivery channel the platform does not recognize' },
|
|
35
|
+
HITL_RECIPIENT_TYPE: { severity: 'error', summary: 'an assignee type the platform does not recognize' },
|
|
36
|
+
HITL_RECIPIENT_VALUE: { severity: 'warning', summary: 'a custom assignment with no value' },
|
|
37
|
+
HITL_READ_STATUS: { severity: 'error', summary: 'a read of the status projection instead of output.Action' },
|
|
38
|
+
HITL_READ_NOT_OUTPUT: { severity: 'error', summary: 'a read of the task outside output' },
|
|
39
|
+
HITL_READ_UNKNOWN_FIELD: { severity: 'error', summary: 'a read of a field the form does not answer' },
|
|
40
|
+
};
|
|
41
|
+
/** Names the human task's own result already uses — an answer cannot claim one. */
|
|
42
|
+
const RESERVED_ANSWER_IDS = new Set(['output', 'status', 'Action']);
|
|
43
|
+
/**
|
|
44
|
+
* The human-task node types `variant` can name. `document-validation` resolves
|
|
45
|
+
* from the BUNDLED workbench manifest (the tenant registry does not serve the
|
|
46
|
+
* family — same availability note as do-while).
|
|
47
|
+
*/
|
|
48
|
+
const HITL_VARIANTS = ['quick-form', 'action-app', 'document-validation'];
|
|
49
|
+
/**
|
|
50
|
+
* A human task's form.
|
|
51
|
+
*
|
|
52
|
+
* Everything here is a mistake no later rung can see, which is the test for
|
|
53
|
+
* earning a place in `check` rather than in the docs: the flow compiles, deploys,
|
|
54
|
+
* and shows a person a form with something quietly missing from it. These catch it
|
|
55
|
+
* at rung 1, in the author's own vocabulary.
|
|
56
|
+
*/
|
|
57
|
+
export function checkHitl(inputs, ctx) {
|
|
58
|
+
const step = ctx.step;
|
|
59
|
+
const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
|
|
60
|
+
// Which of the three human-task node types this step emits. The two suffixed
|
|
61
|
+
// variants differ in exactly one authoring-visible way — who OWNS the form —
|
|
62
|
+
// and that is what the next four rules are about.
|
|
63
|
+
const variant = inputs?.variant;
|
|
64
|
+
if (variant !== undefined && variant !== 'quick-form' && variant !== 'action-app'
|
|
65
|
+
&& variant !== 'document-validation') {
|
|
66
|
+
const nearest = nearestName(String(variant), HITL_VARIANTS);
|
|
67
|
+
at('HITL_VARIANT_UNKNOWN', `Human task "${step}" names the variant ${JSON.stringify(variant)}, which is not a human-task `
|
|
68
|
+
+ `node type that resolves on this tenant. Valid: ${HITL_VARIANTS.join(', ')} — or omit it for `
|
|
69
|
+
+ `the default node type (uipath.human-in-the-loop).`, nearest !== undefined ? `variant: '${nearest}'` : `Omit \`variant\`.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const isApp = variant === 'action-app';
|
|
73
|
+
const isDocval = variant === 'document-validation';
|
|
74
|
+
// The error output is the BASE node's 1.2 definition, which no variant has.
|
|
75
|
+
// Per-outcome exits are not a version question: quick-form draws one handle
|
|
76
|
+
// per outcome like the base node, so it may ask for them; the two app-backed
|
|
77
|
+
// variants read their outcomes from the app and leave on `completed`.
|
|
78
|
+
if (variant !== undefined
|
|
79
|
+
&& (inputs?.exposeError === true || (inputs?.outcomePorts === true && !hitlDerivesOutcomeHandles(variant)))) {
|
|
80
|
+
at('HITL_OUTCOME_PORTS_VARIANT', inputs?.exposeError === true
|
|
81
|
+
? `Human task "${step}" asks for exposeError, but its variant is "${variant}" — the error output `
|
|
82
|
+
+ `exists only on the base node type's 1.2 definition.`
|
|
83
|
+
: `Human task "${step}" asks for outcomePorts, but its variant is "${variant}", which reads its `
|
|
84
|
+
+ `outcomes from its app and leaves on its single 'completed' exit.`, inputs?.exposeError === true
|
|
85
|
+
? 'Omit `variant`, or drop exposeError.'
|
|
86
|
+
: `Route on out('${step}', 'Action') downstream.`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (inputs?.document !== undefined && !isDocval) {
|
|
90
|
+
at('HITL_DOCUMENT_WITHOUT_VARIANT', `Human task "${step}" names a document to validate, but its variant is `
|
|
91
|
+
+ `${variant === undefined ? 'the default node type' : `"${variant}"`} — only the validation `
|
|
92
|
+
+ `station reads it, so it would be emitted and ignored.`, `variant: 'document-validation'`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (isDocval) {
|
|
96
|
+
if (Array.isArray(inputs?.fields) && inputs.fields.length > 0) {
|
|
97
|
+
at('HITL_DOCVAL_WITH_FIELDS', `Human task "${step}" is a Document Validation task but also declares ${inputs.fields.length} `
|
|
98
|
+
+ `form field(s). The validation station IS the form — these are dropped.`, `Delete \`fields\`.`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const doc = inputs?.document;
|
|
102
|
+
if (!doc || typeof doc !== 'object') {
|
|
103
|
+
at('HITL_DOCVAL_NO_DOCUMENT', `Human task "${step}" uses variant 'document-validation' but names no document — the station `
|
|
104
|
+
+ `has nothing to validate.`, `document: { extractionResult: out('<extractStep>', 'ExtractionResult') }`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const er = doc.extractionResult;
|
|
108
|
+
const erEmpty = er === undefined || er === null || (typeof er === 'string' && !er.trim());
|
|
109
|
+
if (erEmpty) {
|
|
110
|
+
at('DOCVAL_NO_EXTRACTION', `Human task "${step}" gives the validation station no extractionResult. Bind the `
|
|
111
|
+
+ `ExtractionResult output of the upstream extract step — the platform's own validator `
|
|
112
|
+
+ `rejects the node without it.`, `extractionResult: out('<extractStep>', 'ExtractionResult')`);
|
|
113
|
+
}
|
|
114
|
+
if (doc.render === 'custom') {
|
|
115
|
+
const tx = doc.taxonomy;
|
|
116
|
+
if (tx === undefined || tx === null || (typeof tx === 'string' && !tx.trim())) {
|
|
117
|
+
at('DOCVAL_TAXONOMY_REQUIRED', `Human task "${step}" renders validation in a custom app (render: 'custom') but names no `
|
|
118
|
+
+ `taxonomy — the platform requires one for the app's Validation Control.`, `taxonomy: out('<classifyStep>', 'taxonomy') // or the taxonomy id string`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (doc.render !== undefined && doc.render !== 'standard') {
|
|
122
|
+
at('DOCVAL_RENDER_UNKNOWN', `Human task "${step}" gives the validation render type ${JSON.stringify(doc.render)}. Valid: `
|
|
123
|
+
+ `'standard' (the native station, the default) or 'custom' (an app with a Validation Control).`, `render: 'standard'`);
|
|
124
|
+
}
|
|
125
|
+
// No inline form — skip the field rules; outcomes still route, fall through.
|
|
126
|
+
}
|
|
127
|
+
if (!isApp && !(isDocval && inputs?.document?.render === 'custom') && inputs?.app !== undefined) {
|
|
128
|
+
at('HITL_APP_WITHOUT_VARIANT', `Human task "${step}" names a deployed app, but its variant is `
|
|
129
|
+
+ `${variant === undefined ? 'the default node type' : `"${variant}"`}, whose form lives in the `
|
|
130
|
+
+ `flow. The app would be emitted and then ignored — the reviewer still gets the inline form.`, `variant: 'action-app'`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (isApp) {
|
|
134
|
+
// The app owns the form, so `fields` is not merely unnecessary here — it is a
|
|
135
|
+
// statement about a form this node does not have, and the platform drops it.
|
|
136
|
+
if (Array.isArray(inputs?.fields) && inputs.fields.length > 0) {
|
|
137
|
+
at('HITL_APP_WITH_FIELDS', `Human task "${step}" is backed by an Action App but also declares ${inputs.fields.length} form `
|
|
138
|
+
+ `field(s). The form belongs to the DEPLOYED APP, not to the flow, so these are dropped and `
|
|
139
|
+
+ `the reviewer sees the app's own form instead.`, `Delete \`fields\`; map values into the app with \`app.inputs\` instead.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const app = inputs?.app;
|
|
143
|
+
if (!app || typeof app !== 'object') {
|
|
144
|
+
at('HITL_APP_MISSING', `Human task "${step}" uses variant 'action-app', which takes its form from a deployed Action `
|
|
145
|
+
+ `App — but names no app, so nothing tells the platform which form to show.`, `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
for (const k of ['name', 'key', 'folderPath']) {
|
|
149
|
+
if (typeof app[k] !== 'string' || !app[k].trim()) {
|
|
150
|
+
at('HITL_APP_INCOMPLETE', `Human task "${step}" names an Action App with no ${k}. An app is resolved by its key plus `
|
|
151
|
+
+ `the folder it is deployed in, with the name shown to the reviewer — all three are needed `
|
|
152
|
+
+ `for the platform to find it.`, `app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared' }`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Outcomes still matter for an app task: the flow's own routing and its
|
|
157
|
+
// `Action`/`status` outputs are derived from them, so fall through to the
|
|
158
|
+
// outcome rules below rather than returning here.
|
|
159
|
+
}
|
|
160
|
+
// An app task legitimately has no form fields of its own — the rules below then
|
|
161
|
+
// walk an empty list and only the outcome rules bite, which is what we want.
|
|
162
|
+
const fields = isApp || isDocval ? (inputs?.fields ?? []) : inputs?.fields;
|
|
163
|
+
if (!Array.isArray(fields)) {
|
|
164
|
+
at('HITL_NO_FIELDS', `Human task "${step}" has no fields array. Give it the form: what the reviewer reads `
|
|
165
|
+
+ `(direction 'input') and what they fill in (direction 'output').`, `fields: [{ id: 'amount', type: 'number', direction: 'input', value: input('amount') }]`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
let answers = 0;
|
|
170
|
+
fields.forEach((f, i) => {
|
|
171
|
+
const where = `fields[${i}]`;
|
|
172
|
+
const id = typeof f?.id === 'string' ? f.id.trim() : '';
|
|
173
|
+
if (!id) {
|
|
174
|
+
at('HITL_FIELD_NO_ID', `Human task "${step}" has a form field with no id (${where}). The id is the key the `
|
|
175
|
+
+ `reviewer's answer comes back under, so a field without one is dropped.`, `id: 'amount'`);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (seen.has(id)) {
|
|
179
|
+
at('HITL_DUP_FIELD', `Human task "${step}" declares the form field "${id}" twice. The answer object is keyed `
|
|
180
|
+
+ `by field id, so the second silently replaces the first.`, `Rename one of them.`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
seen.add(id);
|
|
184
|
+
if (RESERVED_ANSWER_IDS.has(id)) {
|
|
185
|
+
at('HITL_FIELD_RESERVED_ID', `Human task "${step}" has a form field named "${id}", which the task's own result already `
|
|
186
|
+
+ `uses (its output object carries "Action", and the node publishes "output" and "status"). `
|
|
187
|
+
+ `The platform drops a field that collides with those.`, `Rename it, e.g. id: '${id}Value'.`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!FORM_FIELD_TYPES.includes(String(f?.type))) {
|
|
191
|
+
at('HITL_FIELD_TYPE_UNKNOWN', `Human task "${step}" gives the form field "${id}" the type ${JSON.stringify(f?.type)}, `
|
|
192
|
+
+ `${formFieldTypeWhy(f?.type)}. Valid: ${FORM_FIELD_TYPES.join(', ')}.`, formFieldTypeHint(f?.type));
|
|
193
|
+
}
|
|
194
|
+
if (isAnswerField(f))
|
|
195
|
+
answers++; // an `output`, or an `inOut` whose reviewed value comes back
|
|
196
|
+
if (f?.direction === 'input' || f?.direction === 'inOut') {
|
|
197
|
+
const value = f.value;
|
|
198
|
+
if (value === undefined || value === null) {
|
|
199
|
+
at('HITL_FIELD_NO_VALUE', `Human task "${step}" shows the form field "${id}" to the reviewer but gives it no value, `
|
|
200
|
+
+ `so the field renders empty — the flow says "show them this" and shows them nothing.`, `value: input('<flowInput>') // or out('<step>', '<field>')`);
|
|
201
|
+
}
|
|
202
|
+
else if (!(value instanceof Expr)) {
|
|
203
|
+
at('HITL_FIELD_VALUE_NOT_REF', `Human task "${step}" gives the form field "${id}" a raw `
|
|
204
|
+
+ `${Array.isArray(value) ? 'array' : typeof value} as its value. A shown field is filled `
|
|
205
|
+
+ `from a variable PATH, not a constant — the platform reads the string as a lookup, so a `
|
|
206
|
+
+ `literal resolves to nothing on the form.`, `value: input('<flowInput>')`);
|
|
207
|
+
}
|
|
208
|
+
else if (value.literal || !/^\$vars\.[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(value.js.trim())) {
|
|
209
|
+
// Same trap as a transform's `collection`: a computed expression resolves
|
|
210
|
+
// locally and to nothing once deployed, where it is used as a path.
|
|
211
|
+
at('HITL_FIELD_VALUE_NOT_PATH', `Human task "${step}" fills the form field "${id}" from the expression `
|
|
212
|
+
+ `"${value.literal ? String(value.value) : value.js.trim().slice(0, 60)}". A field binding is `
|
|
213
|
+
+ `a variable PATH, so a computed expression resolves to nothing on the deployed form. `
|
|
214
|
+
+ `Compute it in a script step first and point the field at that step's output.`, `value: out('<scriptStep>')`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (f?.direction !== 'output') {
|
|
218
|
+
at('HITL_FIELD_DIRECTION', `Human task "${step}" gives the form field "${id}" the direction `
|
|
219
|
+
+ `${JSON.stringify(f?.direction)}. A field is shown to the `
|
|
220
|
+
+ `reviewer ('input'), filled in by them ('output'), or pre-filled and editable ('inOut'); `
|
|
221
|
+
+ `anything else appears in the form schema and is neither displayed nor read back.`, `direction: 'output'`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
const outcomes = inputs?.outcomes;
|
|
225
|
+
if (!Array.isArray(outcomes) || outcomes.length === 0) {
|
|
226
|
+
at('HITL_NO_OUTCOMES', `Human task "${step}" declares no outcomes, so the reviewer has no button to complete the `
|
|
227
|
+
+ `task with and the flow waits forever.`, `outcomes: ['Approve', 'Reject']`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const names = new Set();
|
|
231
|
+
const ids = new Map();
|
|
232
|
+
outcomes.forEach((o, i) => {
|
|
233
|
+
const name = (typeof o === 'string' ? o : o?.name)?.trim();
|
|
234
|
+
// Two outcomes on one id would share one `outcome-<id>` port, so the
|
|
235
|
+
// second's edge could never be told apart. An explicit `id` can collide
|
|
236
|
+
// with another outcome's derived slug.
|
|
237
|
+
const id = outcomeIdOf(o, i);
|
|
238
|
+
const explicitId = typeof o === 'object' && o !== null ? o.id : undefined;
|
|
239
|
+
if (explicitId !== undefined && (typeof explicitId !== 'string' || explicitId === '')) {
|
|
240
|
+
at('HITL_OUTCOME_BAD_ID', `Human task "${step}" outcome ${JSON.stringify(name ?? i)} has an id that is not a non-empty string. `
|
|
241
|
+
+ `Leave \`id\` out to use the slug of the name.`, `{ name: ${JSON.stringify(name ?? 'Approve')} }`);
|
|
242
|
+
}
|
|
243
|
+
else if (ids.has(id) && ids.get(id) !== String(name ?? '')) {
|
|
244
|
+
// Same name, same id: HITL_DUP_OUTCOME below reports it once, as a name clash.
|
|
245
|
+
at('HITL_DUP_OUTCOME_ID', `Human task "${step}" gives outcomes "${ids.get(id)}" and "${name}" the same id "${id}", so both would `
|
|
246
|
+
+ `leave from one 'outcome-${id}' port. Give one of them a different \`id\`.`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
ids.set(id, String(name ?? ''));
|
|
250
|
+
}
|
|
251
|
+
if (!name) {
|
|
252
|
+
at('HITL_OUTCOME_NO_NAME', `Human task "${step}" has an outcome with no name (outcomes[${i}]). The result carries the `
|
|
253
|
+
+ `outcome's NAME — it is what "$vars.${step}.output.Action" holds and what the platform's `
|
|
254
|
+
+ `own routing conditions compare against — so a nameless one can never be matched.`, `outcomes: ['Approve', 'Reject']`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (names.has(name)) {
|
|
258
|
+
at('HITL_DUP_OUTCOME', `Human task "${step}" declares the outcome "${name}" twice. Only the name reaches the flow, `
|
|
259
|
+
+ `so two outcomes sharing one are indistinguishable downstream.`, `Rename one of them.`);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
names.add(name);
|
|
263
|
+
const action = typeof o === 'string' ? undefined : o?.action;
|
|
264
|
+
if (action !== undefined && action !== 'Continue' && action !== 'End') {
|
|
265
|
+
at('HITL_OUTCOME_ACTION', `Human task "${step}" gives the outcome "${name}" the action ${JSON.stringify(action)}. Only `
|
|
266
|
+
+ `the exact string 'End' means "end the process here"; anything else — including 'end' — is `
|
|
267
|
+
+ `read as 'Continue'.`, `action: 'End'`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// Delivery: the channel ids and assignee types are closed platform
|
|
271
|
+
// vocabularies; a typo silently falls back to tenant defaults at run time.
|
|
272
|
+
const HITL_CHANNELS = ['Slack', 'teams', 'Email', 'ActionCenter'];
|
|
273
|
+
const HITL_ASSIGNEE_TYPES = ['user', 'group', 'staticEmail', 'staticGroupName', 'workload', 'roundRobin', 'custom'];
|
|
274
|
+
const rec = inputs?.recipient;
|
|
275
|
+
if (rec !== undefined) {
|
|
276
|
+
for (const ch of rec.channels ?? []) {
|
|
277
|
+
if (!HITL_CHANNELS.includes(String(ch))) {
|
|
278
|
+
const nearest = nearestName(String(ch), HITL_CHANNELS);
|
|
279
|
+
at('HITL_CHANNEL_UNKNOWN', `Human task "${step}" delivers over the channel ${JSON.stringify(ch)}, which the platform `
|
|
280
|
+
+ `does not recognize — valid: ${HITL_CHANNELS.join(', ')} (Microsoft Teams really is the `
|
|
281
|
+
+ `lowercase 'teams').`, nearest !== undefined ? `channels: ['${nearest}']` : undefined);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const at2 = rec.assignee?.type;
|
|
285
|
+
if (!HITL_ASSIGNEE_TYPES.includes(String(at2))) {
|
|
286
|
+
const nearest = nearestName(String(at2), HITL_ASSIGNEE_TYPES);
|
|
287
|
+
at('HITL_RECIPIENT_TYPE', `Human task "${step}" assigns its recipient with the type ${JSON.stringify(at2)}. Valid: `
|
|
288
|
+
+ `${HITL_ASSIGNEE_TYPES.join(', ')}.`, nearest !== undefined ? `assignee: { type: '${nearest}' }` : undefined);
|
|
289
|
+
}
|
|
290
|
+
else if (at2 === 'custom' && !(typeof rec.assignee?.value === 'string' && rec.assignee.value.trim())) {
|
|
291
|
+
at('HITL_RECIPIENT_VALUE', `Human task "${step}" uses a custom assignment with no value — the engine gets an `
|
|
292
|
+
+ `empty custom-assignees expression and falls back to the tenant default.`, `assignee: { type: 'custom', value: '<expression>' }`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// A form with nothing to fill in and one outcome is a notification, not a task
|
|
296
|
+
// — but it is a legitimate one (an acknowledgement step), so this is not an
|
|
297
|
+
// error. Two or more outcomes with no answer fields is the common approval
|
|
298
|
+
// shape and equally fine. Nothing to report either way.
|
|
299
|
+
void answers;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* A read of a human task's output: `out('<step>', '<field>')`.
|
|
303
|
+
*
|
|
304
|
+
* The node publishes an object keyed by the form's ANSWER fields plus `Action`.
|
|
305
|
+
* Two mistakes are silent at run time and both are caught here:
|
|
306
|
+
*
|
|
307
|
+
* - reading a field the form does not answer (a shown field, or a typo) —
|
|
308
|
+
* `undefined`, offline and deployed;
|
|
309
|
+
* - reading `$vars.<step>.status`, the platform's own projection of the chosen
|
|
310
|
+
* outcome. It resolves *deployed*, so the flow works in production and reads
|
|
311
|
+
* `undefined` on this SDK's offline rungs — a divergence, which is why the read
|
|
312
|
+
* is redirected rather than merely flagged. (A runner that derives declared
|
|
313
|
+
* output variables resolves `status` offline as well; the surface still points
|
|
314
|
+
* at one spelling so a flow reads the same everywhere.)
|
|
315
|
+
*/
|
|
316
|
+
export function checkHitlRead({ node: name, field, rest, reader: step }, form, report) {
|
|
317
|
+
if (field === undefined) {
|
|
318
|
+
// `out('review')` — the whole answer object. Legitimate (a script may want
|
|
319
|
+
// all of it), same as a subflow's bare read being legitimate.
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (field === 'status') {
|
|
323
|
+
report('HITL_READ_STATUS', `"$vars.${name}.status" (read in "${step}") is the platform's own projection of the chosen `
|
|
324
|
+
+ `outcome, not part of the answer object. Read the outcome from the output instead, which is `
|
|
325
|
+
+ `the spelling flow-v1's routing conditions use and the one every rung here resolves.`, `out('${name}', 'Action')`);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (field !== 'output') {
|
|
329
|
+
report('HITL_READ_NOT_OUTPUT', `Human task "${name}" publishes its result under "output", but "$vars.${name}.${field}" `
|
|
330
|
+
+ `(read in "${step}") asks for "${field}", which is undefined at run time.`, `out('${name}', '<fieldId>')`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
// `out(name, 'x')` renders as `$vars.<name>.output.x`, so the answer field is
|
|
334
|
+
// the first segment of `rest` — which arrives with its leading dot
|
|
335
|
+
// (`.approved`), the same shape `checkSubflowRead` receives.
|
|
336
|
+
const answer = rest.replace(/^\./, '').split('.')[0];
|
|
337
|
+
if (!answer)
|
|
338
|
+
return; // `$vars.<name>.output` — the whole object, fine.
|
|
339
|
+
if (answer === 'Action')
|
|
340
|
+
return; // which outcome was pressed.
|
|
341
|
+
const answers = (form?.fields ?? [])
|
|
342
|
+
.filter((f) => isAnswerField(f) && typeof f?.id === 'string')
|
|
343
|
+
.map((f) => f.id);
|
|
344
|
+
if (answers.includes(answer))
|
|
345
|
+
return;
|
|
346
|
+
const shown = (form?.fields ?? [])
|
|
347
|
+
.filter((f) => f?.direction === 'input' && typeof f?.id === 'string')
|
|
348
|
+
.map((f) => f.id);
|
|
349
|
+
const nearest = nearestName(answer, [...answers, 'Action']);
|
|
350
|
+
report('HITL_READ_UNKNOWN_FIELD', `Human task "${name}" has no answer "${answer}" (read as "$vars.${name}.output.${answer}" in `
|
|
351
|
+
+ `"${step}"). Its result carries only the fields the reviewer FILLS IN or EDITS`
|
|
352
|
+
+ `${answers.length > 0 ? ` (${answers.join(', ')})` : ' (it declares none)'} plus "Action". `
|
|
353
|
+
+ (shown.includes(answer)
|
|
354
|
+
? `"${answer}" is a field the reviewer only reads, so it is not part of the answer — read the `
|
|
355
|
+
+ `value you bound to it instead.`
|
|
356
|
+
: 'Nothing else resolves at run time.'), nearest !== undefined ? `out('${name}', '${nearest}')` : undefined);
|
|
357
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HITL factory, `hitl()`. Moved from core/actions.ts in Phase 4 of
|
|
3
|
+
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
|
+
*/
|
|
5
|
+
import { type ActionSpec } from '../../../core/actions.js';
|
|
6
|
+
import type { HitlInputs } from './spec.js';
|
|
7
|
+
/**
|
|
8
|
+
* Declare a human task — a step that pauses for a person to read some values,
|
|
9
|
+
* fill others in, and press one of the outcome buttons.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* Read an answer with `out('review', '<fieldId>')`, and which button they pressed
|
|
13
|
+
* with `out('review', 'Action')` — branch on that for per-outcome routing.
|
|
14
|
+
*
|
|
15
|
+
* ### Picking a node type with `variant`
|
|
16
|
+
*
|
|
17
|
+
* The example above emits `uipath.human-in-the-loop`, the original node, and
|
|
18
|
+
* that stays the default. Two other spellings of the same human task exist
|
|
19
|
+
* ({@link HitlVariant}), and `variant` is how you say which:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* // Same inline form, the newer node type — no `inputs.type` is written.
|
|
23
|
+
* hitl({ variant: 'quick-form', fields: […], outcomes: ['Approve', 'Reject'] })
|
|
24
|
+
*
|
|
25
|
+
* // The form lives in a DEPLOYED APP: no `fields`, but still your outcomes.
|
|
26
|
+
* hitl({
|
|
27
|
+
* variant: 'action-app',
|
|
28
|
+
* app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared',
|
|
29
|
+
* inputs: { 'Expense Amount': out('fetchExpense', 'total') } },
|
|
30
|
+
* outcomes: ['Approve', 'Reject'],
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* What every variant shares: one `completed` port, and the same two outputs
|
|
35
|
+
* (`output`, carrying the answers, and `status`, carrying the outcome name) —
|
|
36
|
+
* all three definitions declare them identically, so downstream reads do not
|
|
37
|
+
* change when you switch.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* **Pause for a person to approve, then read their decision**
|
|
41
|
+
* ```ts
|
|
42
|
+
* .step('review', hitl({
|
|
43
|
+
* title: 'Review Expense',
|
|
44
|
+
* fields: [
|
|
45
|
+
* { id: 'amount', type: 'number', direction: 'input', value: input('amount') },
|
|
46
|
+
* { id: 'approved', type: 'boolean', direction: 'output' },
|
|
47
|
+
* { id: 'comments', type: 'text', direction: 'output' },
|
|
48
|
+
* ],
|
|
49
|
+
* outcomes: ['Approve', 'Reject'],
|
|
50
|
+
* }))
|
|
51
|
+
* .step('log', script({ code: 'return "Decision: " + $vars.review.output.approved;' }))
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @param inputs - The form's fields, outcomes and recipient.
|
|
55
|
+
* @returns An action spec for `.step(...)`.
|
|
56
|
+
* @example example-eval/GallerySubmission.flow.ts
|
|
57
|
+
*/
|
|
58
|
+
export declare function hitl(inputs: HitlInputs): ActionSpec;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The HITL factory, `hitl()`. Moved from core/actions.ts in Phase 4 of
|
|
3
|
+
* docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; the package entry re-exports it.
|
|
4
|
+
*/
|
|
5
|
+
import { builtinActionSpec } from '../../../core/actions.js';
|
|
6
|
+
/**
|
|
7
|
+
* Declare a human task — a step that pauses for a person to read some values,
|
|
8
|
+
* fill others in, and press one of the outcome buttons.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Read an answer with `out('review', '<fieldId>')`, and which button they pressed
|
|
12
|
+
* with `out('review', 'Action')` — branch on that for per-outcome routing.
|
|
13
|
+
*
|
|
14
|
+
* ### Picking a node type with `variant`
|
|
15
|
+
*
|
|
16
|
+
* The example above emits `uipath.human-in-the-loop`, the original node, and
|
|
17
|
+
* that stays the default. Two other spellings of the same human task exist
|
|
18
|
+
* ({@link HitlVariant}), and `variant` is how you say which:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* // Same inline form, the newer node type — no `inputs.type` is written.
|
|
22
|
+
* hitl({ variant: 'quick-form', fields: […], outcomes: ['Approve', 'Reject'] })
|
|
23
|
+
*
|
|
24
|
+
* // The form lives in a DEPLOYED APP: no `fields`, but still your outcomes.
|
|
25
|
+
* hitl({
|
|
26
|
+
* variant: 'action-app',
|
|
27
|
+
* app: { name: 'Expense Review', key: '<guid>', folderPath: 'Shared',
|
|
28
|
+
* inputs: { 'Expense Amount': out('fetchExpense', 'total') } },
|
|
29
|
+
* outcomes: ['Approve', 'Reject'],
|
|
30
|
+
* })
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* What every variant shares: one `completed` port, and the same two outputs
|
|
34
|
+
* (`output`, carrying the answers, and `status`, carrying the outcome name) —
|
|
35
|
+
* all three definitions declare them identically, so downstream reads do not
|
|
36
|
+
* change when you switch.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* **Pause for a person to approve, then read their decision**
|
|
40
|
+
* ```ts
|
|
41
|
+
* .step('review', hitl({
|
|
42
|
+
* title: 'Review Expense',
|
|
43
|
+
* fields: [
|
|
44
|
+
* { id: 'amount', type: 'number', direction: 'input', value: input('amount') },
|
|
45
|
+
* { id: 'approved', type: 'boolean', direction: 'output' },
|
|
46
|
+
* { id: 'comments', type: 'text', direction: 'output' },
|
|
47
|
+
* ],
|
|
48
|
+
* outcomes: ['Approve', 'Reject'],
|
|
49
|
+
* }))
|
|
50
|
+
* .step('log', script({ code: 'return "Decision: " + $vars.review.output.approved;' }))
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param inputs - The form's fields, outcomes and recipient.
|
|
54
|
+
* @returns An action spec for `.step(...)`.
|
|
55
|
+
* @example example-eval/GallerySubmission.flow.ts
|
|
56
|
+
*/
|
|
57
|
+
export function hitl(inputs) {
|
|
58
|
+
return builtinActionSpec({ kind: 'hitl', inputs });
|
|
59
|
+
}
|