Skip to content

Commit e602c3d

Browse files
committed
feat: update taskmanager with new state machine
1 parent 08b3c61 commit e602c3d

File tree

12 files changed

+1053
-319
lines changed

12 files changed

+1053
-319
lines changed

packages/@webex/contact-center/src/services/task/Task.ts

Lines changed: 130 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ import {
3131
getDefaultUIControls,
3232
haveUIControlsChanged,
3333
} from './state-machine/uiControlsComputer';
34+
import type {TaskActionsMap} from './state-machine/actions';
3435

3536
type CallId = string;
3637

38+
export interface TaskActionCallbacks {
39+
onTaskHydrated?: (task: ITask, taskData: TaskData) => void;
40+
onTaskOffered?: (task: ITask, taskData: TaskData) => void;
41+
}
42+
43+
export interface TaskRuntimeOptions {
44+
actionCallbacks?: TaskActionCallbacks;
45+
}
46+
3747
export default abstract class Task extends EventEmitter implements ITask {
3848
protected contact: ReturnType<typeof routingContact>;
3949
protected metricsManager: MetricsManager;
@@ -44,16 +54,19 @@ export default abstract class Task extends EventEmitter implements ITask {
4454
private lastState?: TaskState;
4555
protected currentUiControls: TaskUIControls;
4656
protected uiControlConfig: UIControlConfig;
57+
protected actionCallbacks?: TaskActionCallbacks;
4758

4859
constructor(
4960
contact: ReturnType<typeof routingContact>,
5061
data: TaskData,
51-
uiControlConfig: UIControlConfig
62+
uiControlConfig: UIControlConfig,
63+
runtimeOptions: TaskRuntimeOptions = {}
5264
) {
5365
super();
5466
this.contact = contact;
5567
this.data = data;
5668
this.uiControlConfig = uiControlConfig;
69+
this.actionCallbacks = runtimeOptions.actionCallbacks;
5770
this.metricsManager = MetricsManager.getInstance();
5871
this.webCallMap = {};
5972
this.currentUiControls = getDefaultUIControls();
@@ -167,7 +180,9 @@ export default abstract class Task extends EventEmitter implements ITask {
167180
* Initialize the state machine
168181
*/
169182
private initializeStateMachine(): void {
170-
const machine: TaskStateMachine = createTaskStateMachine(this.uiControlConfig);
183+
const machine: TaskStateMachine = createTaskStateMachine(this.uiControlConfig, {
184+
actions: this.getStateMachineActionOverrides(),
185+
});
171186

172187
this.stateMachineService = createActor(machine);
173188

@@ -233,6 +248,119 @@ export default abstract class Task extends EventEmitter implements ITask {
233248
}
234249
}
235250

251+
private extractTaskDataFromEvent(event?: TaskEventPayload): TaskData | undefined {
252+
if (!event || typeof event !== 'object') {
253+
return undefined;
254+
}
255+
256+
if ('taskData' in event) {
257+
return (event as {taskData?: TaskData}).taskData;
258+
}
259+
260+
return undefined;
261+
}
262+
263+
private updateTaskFromEvent(event?: TaskEventPayload): void {
264+
const taskData = this.extractTaskDataFromEvent(event);
265+
if (taskData) {
266+
this.updateTaskData(taskData);
267+
}
268+
}
269+
270+
protected getStateMachineActionOverrides(): Partial<TaskActionsMap> {
271+
return {
272+
...this.getCommonActionOverrides(),
273+
...this.getChannelSpecificActionOverrides(),
274+
};
275+
}
276+
277+
protected getChannelSpecificActionOverrides(): Partial<TaskActionsMap> {
278+
return {};
279+
}
280+
281+
protected createEmitSelfAction(
282+
taskEvent: TASK_EVENTS,
283+
{updateTaskData = false}: {updateTaskData?: boolean} = {}
284+
) {
285+
return ({event}: {event: TaskEventPayload}) => {
286+
if (updateTaskData) {
287+
this.updateTaskFromEvent(event);
288+
}
289+
this.emit(taskEvent, this);
290+
};
291+
}
292+
293+
private getCommonActionOverrides(): Partial<TaskActionsMap> {
294+
return {
295+
emitTaskHydrate: ({event}: {event: TaskEventPayload}) => {
296+
const taskData = this.extractTaskDataFromEvent(event);
297+
if (!taskData) {
298+
return;
299+
}
300+
if (this.actionCallbacks?.onTaskHydrated) {
301+
this.actionCallbacks.onTaskHydrated(this, taskData);
302+
} else {
303+
this.updateTaskData(taskData);
304+
this.emit(TASK_EVENTS.TASK_HYDRATE, this);
305+
}
306+
},
307+
emitTaskOfferContact: ({event}: {event: TaskEventPayload}) => {
308+
const taskData = this.extractTaskDataFromEvent(event);
309+
if (!taskData) {
310+
return;
311+
}
312+
if (this.actionCallbacks?.onTaskOffered) {
313+
this.actionCallbacks.onTaskOffered(this, taskData);
314+
} else {
315+
this.updateTaskData(taskData);
316+
this.emit(TASK_EVENTS.TASK_OFFER_CONTACT, this);
317+
}
318+
},
319+
emitTaskAssigned: this.createEmitSelfAction(TASK_EVENTS.TASK_ASSIGNED, {
320+
updateTaskData: true,
321+
}),
322+
emitTaskEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_END, {updateTaskData: true}),
323+
emitTaskOfferConsult: this.createEmitSelfAction(TASK_EVENTS.TASK_OFFER_CONSULT, {
324+
updateTaskData: true,
325+
}),
326+
emitTaskConsultCreated: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_CREATED, {
327+
updateTaskData: true,
328+
}),
329+
emitTaskConsulting: ({event}: {event: TaskEventPayload}) => {
330+
this.updateTaskFromEvent(event);
331+
if (this.data.isConsulted) {
332+
this.emit(TASK_EVENTS.TASK_CONSULT_ACCEPTED, this);
333+
} else {
334+
this.emit(TASK_EVENTS.TASK_CONSULTING, this);
335+
}
336+
},
337+
emitTaskConsultAccepted: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_ACCEPTED),
338+
emitTaskConsultEnd: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_END, {
339+
updateTaskData: true,
340+
}),
341+
emitTaskConsultQueueCancelled: this.createEmitSelfAction(
342+
TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
343+
{
344+
updateTaskData: true,
345+
}
346+
),
347+
emitTaskConsultQueueFailed: this.createEmitSelfAction(TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED, {
348+
updateTaskData: true,
349+
}),
350+
emitTaskReject: ({event}: {event: TaskEventPayload}) => {
351+
this.updateTaskFromEvent(event);
352+
const reason =
353+
event && typeof event === 'object' && 'reason' in event
354+
? (event as {reason?: string}).reason
355+
: undefined;
356+
this.emit(TASK_EVENTS.TASK_REJECT, reason);
357+
},
358+
emitTaskWrappedup: this.createEmitSelfAction(TASK_EVENTS.TASK_WRAPPEDUP, {
359+
updateTaskData: true,
360+
}),
361+
};
362+
}
363+
236364
private reconcileData(oldData: TaskData, newData: TaskData): TaskData {
237365
Object.keys(newData).forEach((key) => {
238366
if (newData[key] && typeof newData[key] === 'object' && !Array.isArray(newData[key])) {

packages/@webex/contact-center/src/services/task/TaskFactory.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import routingContact from './contact';
22
import WebCallingService from '../WebCallingService';
3-
import Task from './Task';
3+
import Task, {TaskRuntimeOptions} from './Task';
44
import Voice from './voice/Voice';
55
import WebRTC from './voice/WebRTC';
66
import Digital from './digital/Digital';
@@ -15,7 +15,8 @@ export default class TaskFactory {
1515
contact: ReturnType<typeof routingContact>,
1616
webCallingService: WebCallingService,
1717
data: TaskData,
18-
configFlags: ConfigFlags
18+
configFlags: ConfigFlags,
19+
runtimeOptions: TaskRuntimeOptions = {}
1920
): Task {
2021
const mediaType = data.interaction.mediaType ?? MEDIA_CHANNEL.TELEPHONY;
2122
const {isEndTaskEnabled, isEndConsultEnabled} = configFlags;
@@ -29,15 +30,15 @@ export default class TaskFactory {
2930
switch (mediaType) {
3031
case MEDIA_CHANNEL.TELEPHONY:
3132
if (webCallingService.loginOption === 'BROWSER') {
32-
return new WebRTC(contact, webCallingService, data, voiceControlOptions);
33+
return new WebRTC(contact, webCallingService, data, voiceControlOptions, runtimeOptions);
3334
}
3435

35-
return new Voice(contact, data, voiceControlOptions);
36+
return new Voice(contact, data, voiceControlOptions, runtimeOptions);
3637

3738
case MEDIA_CHANNEL.CHAT:
3839
case MEDIA_CHANNEL.EMAIL:
3940
case MEDIA_CHANNEL.SOCIAL:
40-
return new Digital(contact, data);
41+
return new Digital(contact, data, runtimeOptions);
4142

4243
default:
4344
throw new Error(`Unknown media type: ${mediaType}`);

0 commit comments

Comments
 (0)