Skip to content

Commit bb08ac4

Browse files
committed
feat: simplify and unify execution callbacks
- Unify callbacks into onTaskExecutionFinished with reason field - Remove onTaskExecutionInterrupted Result object now uses discriminated union: - success: true → contains variables - success: false → contains reason (incident/user.cancel/user.selectionChanged/error) BREAKING CHANGE: onTaskExecutionInterrupted removed. Use onTaskExecutionFinished with result.success and result.reason instead. Related to camunda/camunda-modeler#5597
1 parent 34f6769 commit bb08ac4

File tree

7 files changed

+532
-117
lines changed

7 files changed

+532
-117
lines changed

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,36 @@ import TaskTesting from '@camunda/task-testing';
1212
function App() {
1313
...
1414

15-
<TaskTesting api={ ... }>
15+
<TaskTesting
16+
api={ ... }
17+
onTaskExecutionStarted={ (element) => { ... } }
18+
onTaskExecutionFinished={ (element, result) => { ... } }
19+
>
1620
<TaskTesting.Tab label={ 'Foo' }>...</TaskTesting.Tab>;
1721
<TaskTesting.Link href="https://camunda.com">Foo</TaskTesting.Link>;
1822
</TaskTesting>
1923
}
2024
```
2125

26+
### Props
27+
28+
#### Lifecycle Callbacks
29+
30+
The TaskTesting component provides callbacks to track task execution lifecycle events:
31+
32+
- **`onTaskExecutionStarted(element)`** - Called when a task execution begins
33+
- `element` - The BPMN element being tested
34+
35+
- **`onTaskExecutionFinished(element, result)`** - Called when task execution ends (success, incident, or cancellation)
36+
- `element` - The BPMN element that was tested
37+
- `result` - Execution result object:
38+
- When `success: true`: Contains `variables` with execution output
39+
- When `success: false`: Contains `reason` explaining why:
40+
- `'incident'` - Task completed with an incident (includes `incident` and optional `variables`)
41+
- `'user.cancel'` - User clicked cancel button
42+
- `'user.selectionChanged'` - User selected a different element
43+
- `'error'` - Deployment or start instance failed (includes `error` object)
44+
2245
[See demo](https://github.com/camunda/task-testing/tree/main/demo)
2346

2447
## Development

demo/App.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ function App() {
162162
onConfigChanged={ onConfigChanged }
163163
operateBaseUrl={ operateURL }
164164
documentationUrl="https://docs.camunda.io/"
165+
onTaskExecutionStarted={ (element) => {
166+
console.log('Task execution started:', element.id);
167+
} }
168+
onTaskExecutionFinished={ (element, result) => {
169+
console.log('Task execution finished:', element.id, result.success ? 'success' : result.reason, result);
170+
} }
165171
/>
166172
</div>
167173
</>

lib/TaskExecution.js

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ export const SCOPES = {
3838
/**
3939
* Emits:
4040
* - `taskExecution.status.changed` with one of {@link TaskExecutionStatus}
41-
* - `taskExecution.finished` with {@link TaskExecutionResult}
42-
* - `taskExecution.error` with {@link TaskExecutionError}
43-
* - `taskExecution.interrupted` when execution is interrupted by switching focus
41+
* - `taskExecution.finished` with {@link TaskExecutionResult} (includes all completion outcomes: success, incident, cancellations, errors)
4442
*/
4543
export default class TaskExecution extends EventEmitter {
4644

@@ -62,10 +60,7 @@ export default class TaskExecution extends EventEmitter {
6260
const eventBus = injector.get('eventBus');
6361

6462
eventBus.on('selection.changed', () => {
65-
if (this._status !== 'idle') {
66-
this.emit('taskExecution.interrupted');
67-
}
68-
this.cancelTaskExecution();
63+
this.cancelTaskExecution('user.selectionChanged');
6964
});
7065
}
7166

@@ -81,7 +76,6 @@ export default class TaskExecution extends EventEmitter {
8176
const processId = getProcessId(element);
8277

8378
if (!processId) {
84-
this._emitError(`Process ID for element <${element.id}> not found`, null);
8579
return;
8680
}
8781

@@ -96,17 +90,17 @@ export default class TaskExecution extends EventEmitter {
9690
}
9791

9892
if (!deploymentResult.success) {
99-
this._emitError('Failed to deploy process definition', deploymentResult.error);
100-
this.cancelTaskExecution();
93+
const error = { message: 'Failed to deploy process definition', response: deploymentResult.error };
94+
this.cancelTaskExecution('error', error);
10195
return;
10296
}
10397

10498
const processDefinitionKey = getProcessDefinitionKey(deploymentResult.response, processId);
10599

106100
if (!processDefinitionKey) {
107-
this._emitError('Failed to retrieve process definition key from deployment response', deploymentResult.response);
101+
const error = { message: 'Failed to retrieve process definition key from deployment response', response: deploymentResult.response };
108102

109-
this.cancelTaskExecution();
103+
this.cancelTaskExecution('error', error);
110104

111105
return;
112106
}
@@ -122,16 +116,16 @@ export default class TaskExecution extends EventEmitter {
122116
}
123117

124118
if (!startInstanceResult.success) {
125-
this._emitError('Failed to start process instance', startInstanceResult.error);
126-
this.cancelTaskExecution();
119+
const error = { message: 'Failed to start process instance', response: startInstanceResult.error };
120+
this.cancelTaskExecution('error', error);
127121
return;
128122
}
129123

130124
const processInstanceKey = getProcessInstanceKey(startInstanceResult.response);
131125

132126
if (!processInstanceKey) {
133-
this._emitError('Failed to retrieve process instance key from start instance response');
134-
this.cancelTaskExecution();
127+
const error = { message: 'Failed to retrieve process instance key from start instance response' };
128+
this.cancelTaskExecution('error', error);
135129
return;
136130
}
137131

@@ -148,7 +142,10 @@ export default class TaskExecution extends EventEmitter {
148142
}
149143

150144
if (!getProcessInstanceResult.success) {
151-
this._emitError('Failed to get process instance', getProcessInstanceResult.error);
145+
this.cancelTaskExecution('error', {
146+
message: 'Failed to get process instance',
147+
response: getProcessInstanceResult.error
148+
});
152149
return;
153150
}
154151

@@ -175,7 +172,10 @@ export default class TaskExecution extends EventEmitter {
175172
}
176173

177174
if (!getProcessInstanceIncidentResult.success) {
178-
this._emitError('Failed to get process instance incident', getProcessInstanceIncidentResult.error);
175+
this.cancelTaskExecution('error', {
176+
message: 'Failed to get process instance incident',
177+
response: getProcessInstanceIncidentResult.error
178+
});
179179
return;
180180
}
181181

@@ -195,7 +195,10 @@ export default class TaskExecution extends EventEmitter {
195195
}
196196

197197
if (!getProcessInstanceVariablesResult.success) {
198-
this._emitError('Failed to get process instance variables', getProcessInstanceVariablesResult.error);
198+
this.cancelTaskExecution('error', {
199+
message: 'Failed to get process instance variables',
200+
response: getProcessInstanceVariablesResult.error
201+
});
199202
return;
200203
}
201204

@@ -209,7 +212,10 @@ export default class TaskExecution extends EventEmitter {
209212
}
210213

211214
if (!getProcessInstanceElementInstancesResult.success) {
212-
this._emitError('Failed to get process instance element instances', getProcessInstanceElementInstancesResult.error);
215+
this.cancelTaskExecution('error', {
216+
message: 'Failed to get process instance element instances',
217+
response: getProcessInstanceElementInstancesResult.error
218+
});
213219
return;
214220
}
215221

@@ -220,10 +226,14 @@ export default class TaskExecution extends EventEmitter {
220226
element.id
221227
);
222228

223-
this.emit('taskExecution.finished', {
224-
success: !incident,
229+
this.emit('taskExecution.finished', incident ? {
230+
success: false,
231+
reason: 'incident',
225232
incident,
226233
variables
234+
} : {
235+
success: true,
236+
variables
227237
});
228238

229239
this.cancelTaskExecution();
@@ -235,36 +245,35 @@ export default class TaskExecution extends EventEmitter {
235245

236246
/**
237247
* Cancel current task execution, clean up and change status to `idle`.
248+
*
249+
* @param {string} [reason] - Reason for cancellation: 'user.cancel', 'user.selectionChanged', or 'error'
250+
* @param {any} [error] - Error object when reason is 'error'
238251
*/
239-
async cancelTaskExecution() {
252+
async cancelTaskExecution(reason, error) {
240253

241254
// TODO: Proper clean up:
242255
// - delete process instance
243256
// - delete process definition
244257
// - cancel deploy and start instance if they are in progress
245258

259+
const wasCanceled = this._status !== 'idle';
260+
246261
if (this._interval) {
247262
clearInterval(this._interval);
248263
}
249264

250265
this._changeStatus('idle');
251-
}
252266

253-
/**
254-
* Emit `taskExecution.error` event.
255-
*
256-
* @param {string} message
257-
* @param {any} [response]
258-
*/
259-
_emitError(message, response) {
260-
261-
/** @type {import('./types').TaskExecutionError} */
262-
const error = {
263-
message,
264-
response
265-
};
266-
267-
this.emit('taskExecution.error', error);
267+
if (wasCanceled && reason) {
268+
this.emit('taskExecution.finished', reason === 'error' ? {
269+
success: false,
270+
reason,
271+
error
272+
} : {
273+
success: false,
274+
reason
275+
});
276+
}
268277
}
269278

270279
/** @param {TaskExecutionStatus} status */

lib/components/TaskTesting/TaskTesting.js

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ import { OutputTab } from '../Output/OutputVariables';
5252
* @param {string} [props.operateBaseUrl]
5353
* @param {string} [props.documentationUrl]
5454
* @param {Function} [props.onTaskExecutionStarted=() => {}]
55-
* @param {Function} [props.onTaskExecutionFinished=() => {}]
56-
* @param {Function} [props.onTaskExecutionInterrupted=() => {}]
55+
* @param {Function} [props.onTaskExecutionFinished=() => {}] - Called with (element, result) where result contains success and optional reason for failures
5756
* @param {React.ReactNode[]} [props.children=[]]
5857
*/
5958
export default function TaskTesting({
@@ -71,7 +70,6 @@ export default function TaskTesting({
7170
documentationUrl,
7271
onTaskExecutionStarted = () => {},
7372
onTaskExecutionFinished = () => {},
74-
onTaskExecutionInterrupted = () => {},
7573
children = []
7674
}) {
7775

@@ -204,14 +202,6 @@ export default function TaskTesting({
204202
// Subscribe to task execution events
205203
useEffect(() => {
206204

207-
/** @param {import('../../types').TaskExecutionError} error */
208-
const handleError = (error) => {
209-
elementConfigRef?.current?.setOutputConfigForElement(element, {
210-
success: false,
211-
error
212-
});
213-
};
214-
215205
/**
216206
* @param {import('../../types').TaskExecutionStatus} status
217207
* @param {string} [processInstanceKey]
@@ -229,30 +219,39 @@ export default function TaskTesting({
229219
}
230220
};
231221

232-
/** @param {import('../../types').ElementOutput} output */
233-
const handleFinished = (output) => {
234-
elementConfigRef?.current?.setOutputConfigForElement(element, {
235-
...output,
236-
operateUrl: currentOperateUrl
237-
});
238-
onTaskExecutionFinished(element, output);
239-
};
240-
241-
const handleInterrupted = () => {
242-
onTaskExecutionInterrupted();
222+
/** @param {import('../../types').TaskExecutionResult} result */
223+
const handleFinished = (result) => {
224+
225+
// Only save to config if it's a successful execution, incident, or error (not user cancellations)
226+
if (result.success) {
227+
elementConfigRef?.current?.setOutputConfigForElement(element, {
228+
success: true,
229+
variables: result.variables,
230+
operateUrl: currentOperateUrl
231+
});
232+
} else if (result.reason === 'incident') {
233+
elementConfigRef?.current?.setOutputConfigForElement(element, {
234+
success: false,
235+
variables: result.variables,
236+
incident: result.incident,
237+
operateUrl: currentOperateUrl
238+
});
239+
} else if (result.reason === 'error') {
240+
elementConfigRef?.current?.setOutputConfigForElement(element, {
241+
success: false,
242+
error: result.error
243+
});
244+
}
245+
onTaskExecutionFinished(element, result);
243246
};
244247

245248
taskExecutionRef?.current?.on('taskExecution.finished', handleFinished);
246249
taskExecutionRef?.current?.on('taskExecution.status.changed', handleStatusChange);
247-
taskExecutionRef?.current?.on('taskExecution.error', handleError);
248-
taskExecutionRef?.current?.on('taskExecution.interrupted', handleInterrupted);
249250

250251
return () => {
251252
if (taskExecutionRef.current) {
252253
taskExecutionRef.current.off('taskExecution.finished', handleFinished);
253254
taskExecutionRef.current.off('taskExecution.status.changed', handleStatusChange);
254-
taskExecutionRef.current.off('taskExecution.error', handleError);
255-
taskExecutionRef.current.off('taskExecution.interrupted', handleInterrupted);
256255
}
257256
};
258257
}, [ element, operateBaseUrl, currentOperateUrl ]);
@@ -308,7 +307,7 @@ export default function TaskTesting({
308307
};
309308

310309
const handleCancelTaskExecution = () => {
311-
taskExecutionRef?.current?.cancelTaskExecution();
310+
taskExecutionRef?.current?.cancelTaskExecution('user.cancel');
312311
};
313312

314313
const handleResetOutput = useCallback(() => {

lib/types.d.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,34 @@ export type TaskExecutionStatus =
7777

7878
export type TaskExecutionEvents =
7979
'taskExecution.status.changed' |
80-
'taskExecution.finished' |
81-
'taskExecution.error' |
82-
'taskExecution.interrupted';
80+
'taskExecution.finished';
8381

84-
export type TaskExecutionResult = {
85-
success: boolean;
86-
variables?: ElementOutputVariables;
87-
error?: TaskExecutionError;
88-
incident?: any;
89-
}
82+
export type TaskExecutionResult =
83+
// Success case - no reason needed
84+
| {
85+
success: true;
86+
variables: ElementOutputVariables;
87+
}
88+
// Failure cases - always have reason
89+
| {
90+
success: false;
91+
reason: 'incident';
92+
incident: any;
93+
variables?: ElementOutputVariables;
94+
}
95+
| {
96+
success: false;
97+
reason: 'user.cancel';
98+
}
99+
| {
100+
success: false;
101+
reason: 'user.selectionChanged';
102+
}
103+
| {
104+
success: false;
105+
reason: 'error';
106+
error: TaskExecutionError;
107+
};
90108

91109
export type TaskExecutionError = {
92110
message: string;

0 commit comments

Comments
 (0)