@webex/contact-center 3.12.0-task-refactor.6 → 3.12.0-task-refactor.8
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/services/task/Task.js +32 -0
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskUtils.js +3 -1
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +77 -1
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +113 -23
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +101 -20
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/types/services/task/Task.d.ts +10 -0
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
- package/dist/types/services/task/state-machine/actions.d.ts +2 -0
- package/dist/webex.js +1 -1
- package/package.json +1 -1
- package/src/services/task/Task.ts +34 -0
- package/src/services/task/TaskUtils.ts +5 -3
- package/src/services/task/state-machine/TaskStateMachine.ts +105 -1
- package/src/services/task/state-machine/actions.ts +151 -25
- package/src/services/task/state-machine/uiControlsComputer.ts +185 -29
- package/test/unit/spec/services/task/Task.ts +61 -0
- package/test/unit/spec/services/task/TaskUtils.ts +65 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +662 -1
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -66,6 +66,20 @@ export function getDefaultUIControls(): TaskUIControls {
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Consult media must exist on the interaction payload, not only as a stale resource id. */
|
|
70
|
+
function hasConsultMediaInInteraction(
|
|
71
|
+
interaction: TaskData['interaction'] | undefined,
|
|
72
|
+
consultMediaResourceId?: string
|
|
73
|
+
): boolean {
|
|
74
|
+
const media = interaction?.media ?? {};
|
|
75
|
+
const hasConsultLeg = Object.values(media).some((entry: any) => entry?.mType === 'consult');
|
|
76
|
+
|
|
77
|
+
if (hasConsultLeg) return true;
|
|
78
|
+
if (!consultMediaResourceId) return false;
|
|
79
|
+
|
|
80
|
+
return Boolean(media[consultMediaResourceId]);
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
function computeVoiceInteractionUIControls(
|
|
70
84
|
state: TaskState,
|
|
71
85
|
context: TaskContext,
|
|
@@ -78,8 +92,8 @@ function computeVoiceInteractionUIControls(
|
|
|
78
92
|
return getDefaultInteractionUIControls();
|
|
79
93
|
}
|
|
80
94
|
|
|
81
|
-
//
|
|
82
|
-
const taskData = context.taskData ??
|
|
95
|
+
// Prefer live task.data (fallback) over stale state-machine snapshot during multi-login sync.
|
|
96
|
+
const taskData = fallbackTaskData ?? context.taskData ?? null;
|
|
83
97
|
const interaction = taskData?.interaction;
|
|
84
98
|
const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
|
|
85
99
|
const isWebrtc = config.voiceVariant === VOICE_VARIANT.WEBRTC;
|
|
@@ -125,7 +139,6 @@ function computeVoiceInteractionUIControls(
|
|
|
125
139
|
// Backend sends destinationType as 'EP-DN'; SDK method uses 'entryPoint' — check both.
|
|
126
140
|
const isEpDnConsult =
|
|
127
141
|
consultDestinationType === 'entryPoint' || consultDestinationType === ('EP-DN' as any);
|
|
128
|
-
const isConsultDestinationReady = consultDestinationAgentJoined || isEpDnConsult;
|
|
129
142
|
|
|
130
143
|
const stateImpliesHeld = state === TaskState.HELD || state === TaskState.RESUME_INITIATING;
|
|
131
144
|
const stateImpliesConnected =
|
|
@@ -151,9 +164,32 @@ function computeVoiceInteractionUIControls(
|
|
|
151
164
|
const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
|
|
152
165
|
const selfInConsultCall =
|
|
153
166
|
Boolean(selfAgentId) && Boolean(consultMedia?.participants?.includes(selfAgentId as string));
|
|
154
|
-
const
|
|
167
|
+
const hasConsultMedia = hasConsultMediaInInteraction(
|
|
168
|
+
interaction,
|
|
169
|
+
taskData?.consultMediaResourceId
|
|
170
|
+
);
|
|
171
|
+
const isConsultEndedForSelf =
|
|
172
|
+
taskData?.type === 'AgentConsultEnded' ||
|
|
173
|
+
taskData?.type === 'AgentConsultFailed' ||
|
|
174
|
+
(selfParticipant?.consultState === 'consultCompleted' &&
|
|
175
|
+
!hasConsultMedia &&
|
|
176
|
+
taskData?.isConsulted === false);
|
|
177
|
+
const effectiveConsultInitiator = isConsultEndedForSelf ? false : consultInitiator;
|
|
178
|
+
const effectiveConsultCallHeld = isConsultEndedForSelf ? false : consultCallHeld;
|
|
179
|
+
const effectiveConsultFromConference = isConsultEndedForSelf ? false : consultFromConference;
|
|
180
|
+
const effectiveConsultDestinationAgentJoined = isConsultEndedForSelf
|
|
181
|
+
? false
|
|
182
|
+
: consultDestinationAgentJoined;
|
|
183
|
+
// After a consult ends for self, the backend stops sending consult media but the merged
|
|
184
|
+
// task.data can still carry the stale consult-media entry (reconcileData never deletes keys).
|
|
185
|
+
// Treat it as gone so post-consult main-leg controls (consult retry) are not blocked.
|
|
186
|
+
const effectiveHasConsultMedia = isConsultEndedForSelf ? false : hasConsultMedia;
|
|
187
|
+
const isConsultDestinationReady = effectiveConsultDestinationAgentJoined || isEpDnConsult;
|
|
188
|
+
const conferenceActive =
|
|
189
|
+
isConferencing || conferenceFromBackend || effectiveConsultFromConference;
|
|
155
190
|
// Treat consult initiator as "in conference" even if mainCall participant list lags while consulting.
|
|
156
|
-
const inConference =
|
|
191
|
+
const inConference =
|
|
192
|
+
conferenceActive && (isConferencing || selfInMainCall || effectiveConsultInitiator);
|
|
157
193
|
|
|
158
194
|
// Check if this is a consulted agent (must be after isConsulting is computed).
|
|
159
195
|
const isSoleAgentOnCall =
|
|
@@ -162,7 +198,7 @@ function computeVoiceInteractionUIControls(
|
|
|
162
198
|
inConference || isSoleAgentOnCall
|
|
163
199
|
? false
|
|
164
200
|
: getIsConsultedAgentForControls(taskData, context, isConsulting) ||
|
|
165
|
-
(!
|
|
201
|
+
(!effectiveConsultInitiator &&
|
|
166
202
|
(selfParticipant?.isConsulted === true ||
|
|
167
203
|
selfParticipant?.consultState === 'consulting'));
|
|
168
204
|
|
|
@@ -177,13 +213,10 @@ function computeVoiceInteractionUIControls(
|
|
|
177
213
|
|
|
178
214
|
// Consulted agents have limited controls until they're in conference or wrapup
|
|
179
215
|
// Use inConference (not isConferencing) so controls remain enabled after state downgrade
|
|
180
|
-
const hasFullControls = !isConsulted ||
|
|
216
|
+
const hasFullControls = !isConsulted || effectiveConsultInitiator || inConference || isWrappingUp;
|
|
181
217
|
const consultOwnedBySelf =
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
taskData?.consultMediaResourceId ||
|
|
185
|
-
Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
|
|
186
|
-
);
|
|
218
|
+
effectiveConsultInitiator ||
|
|
219
|
+
(Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
|
|
187
220
|
const selfConsultPendingOnConsultMedia =
|
|
188
221
|
selfParticipant?.consultState === 'consultInitiated' &&
|
|
189
222
|
!taskData?.isConsulted &&
|
|
@@ -207,17 +240,18 @@ function computeVoiceInteractionUIControls(
|
|
|
207
240
|
})
|
|
208
241
|
);
|
|
209
242
|
const isHydratedConferenceConsultPending =
|
|
210
|
-
inConference && selfConsultPendingOnConsultMedia && !
|
|
243
|
+
inConference && selfConsultPendingOnConsultMedia && !effectiveConsultDestinationAgentJoined;
|
|
211
244
|
const hasParallelConsultLeg =
|
|
245
|
+
!isConsultEndedForSelf &&
|
|
212
246
|
consultOwnedBySelf &&
|
|
213
247
|
!isConsulting &&
|
|
214
248
|
!isConsulted &&
|
|
215
|
-
(consultInProgress ||
|
|
216
|
-
const activeLegForConferenceConsult =
|
|
249
|
+
(consultInProgress || effectiveConsultCallHeld || hasConsultMedia);
|
|
250
|
+
const activeLegForConferenceConsult = effectiveConsultCallHeld ? 'main' : 'consult';
|
|
217
251
|
const isCurrentLegActive = currentLeg === activeLegForConferenceConsult;
|
|
218
252
|
const isConferenceConsultTransferContext =
|
|
219
|
-
inConference &&
|
|
220
|
-
const consultLegOnHold = isConsulting &&
|
|
253
|
+
inConference && effectiveConsultInitiator && hasConsultMedia && isConsultDestinationReady;
|
|
254
|
+
const consultLegOnHold = isConsulting && effectiveConsultCallHeld;
|
|
221
255
|
const callProcessingDetails = interaction?.callProcessingDetails as
|
|
222
256
|
| {conferenceHoldParticipant?: boolean | string}
|
|
223
257
|
| undefined;
|
|
@@ -259,6 +293,17 @@ function computeVoiceInteractionUIControls(
|
|
|
259
293
|
(isConsulting ||
|
|
260
294
|
taskData?.type === 'AgentConsulting' ||
|
|
261
295
|
selfParticipant?.consultState === 'consulting');
|
|
296
|
+
const hideExitConferenceOnMainLegForEpDnConsultFromConference =
|
|
297
|
+
currentLeg === 'main' &&
|
|
298
|
+
inConference &&
|
|
299
|
+
consultFromConference &&
|
|
300
|
+
consultInitiator &&
|
|
301
|
+
(isConsulting ||
|
|
302
|
+
consultInProgress ||
|
|
303
|
+
taskData?.type === 'AgentConsultCreated' ||
|
|
304
|
+
taskData?.type === 'AgentConsulting' ||
|
|
305
|
+
selfParticipant?.consultState === 'consultInitiated' ||
|
|
306
|
+
selfParticipant?.consultState === 'consulting');
|
|
262
307
|
const forceHeldPostConsultControls =
|
|
263
308
|
!hideExitConferenceWhileConsultPending &&
|
|
264
309
|
(postDeclineHeldMainLeg || postConsultCompletedHeldMainLeg);
|
|
@@ -269,6 +314,61 @@ function computeVoiceInteractionUIControls(
|
|
|
269
314
|
currentLeg === 'main' && inConference && consultInProgress && !selfOnConsultLeg;
|
|
270
315
|
const allowHeldMainLegControlsForNonInitiator =
|
|
271
316
|
showMainLegConferenceControlsDuringConsult && !isHydratedConferenceConsultPending;
|
|
317
|
+
const isConsultRequestedPhase =
|
|
318
|
+
isConsultPendingBeforeJoin &&
|
|
319
|
+
!isConsulted &&
|
|
320
|
+
(consultInitiator ||
|
|
321
|
+
(Boolean(selfAgentId) &&
|
|
322
|
+
selfParticipant?.consultState === 'consultInitiated' &&
|
|
323
|
+
taskData?.agentId === selfAgentId));
|
|
324
|
+
const isConsultUnansweredFailure =
|
|
325
|
+
currentLeg === 'main' &&
|
|
326
|
+
!inConference &&
|
|
327
|
+
!taskData?.isConsulted &&
|
|
328
|
+
!effectiveHasConsultMedia &&
|
|
329
|
+
isHeld &&
|
|
330
|
+
selfParticipant?.consultState === 'consultCompleted' &&
|
|
331
|
+
(taskData?.type === 'AgentConsultFailed' ||
|
|
332
|
+
taskData?.type === 'AgentConsultEnded' ||
|
|
333
|
+
isConsultEndedForSelf);
|
|
334
|
+
|
|
335
|
+
if (isConsultUnansweredFailure) {
|
|
336
|
+
const recordingControl =
|
|
337
|
+
recordingControlsAvailable && config.isRecordingEnabled && !inConference
|
|
338
|
+
? VISIBLE_ENABLED
|
|
339
|
+
: DISABLED;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...getDefaultInteractionUIControls(),
|
|
343
|
+
hold: VISIBLE_ENABLED,
|
|
344
|
+
transfer: VISIBLE_ENABLED,
|
|
345
|
+
consult: VISIBLE_ENABLED,
|
|
346
|
+
recording: recordingControl,
|
|
347
|
+
end: VISIBLE_DISABLED,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isConsultRequestedPhase) {
|
|
352
|
+
if (currentLeg === 'main') {
|
|
353
|
+
return {
|
|
354
|
+
...getDefaultInteractionUIControls(),
|
|
355
|
+
transfer: VISIBLE_DISABLED,
|
|
356
|
+
conference: VISIBLE_DISABLED,
|
|
357
|
+
end: VISIBLE_DISABLED,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (currentLeg === 'consult') {
|
|
362
|
+
return {
|
|
363
|
+
...getDefaultInteractionUIControls(),
|
|
364
|
+
endConsult: VISIBLE_ENABLED,
|
|
365
|
+
switch: VISIBLE_DISABLED,
|
|
366
|
+
transfer: VISIBLE_DISABLED,
|
|
367
|
+
transferConference: inConference ? VISIBLE_DISABLED : DISABLED,
|
|
368
|
+
mergeToConference: VISIBLE_DISABLED,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
272
372
|
|
|
273
373
|
return {
|
|
274
374
|
// Accept/Decline: Voice tasks in offered state
|
|
@@ -289,7 +389,10 @@ function computeVoiceInteractionUIControls(
|
|
|
289
389
|
hold: (() => {
|
|
290
390
|
if (!hasFullControls) return DISABLED;
|
|
291
391
|
if (forceHeldPostConsultControls) return VISIBLE_ENABLED;
|
|
292
|
-
if (
|
|
392
|
+
if (
|
|
393
|
+
consultOwnedBySelf &&
|
|
394
|
+
(isConsulting || hasParallelConsultLeg || effectiveConsultCallHeld)
|
|
395
|
+
) {
|
|
293
396
|
return DISABLED;
|
|
294
397
|
}
|
|
295
398
|
if (hasParallelConsultLeg) return DISABLED;
|
|
@@ -308,6 +411,17 @@ function computeVoiceInteractionUIControls(
|
|
|
308
411
|
if (!isWebrtc) return DISABLED;
|
|
309
412
|
if (isWrappingUp) return DISABLED;
|
|
310
413
|
if (currentLeg === 'consult' && !selfInConsultCall) return DISABLED;
|
|
414
|
+
// The consulted agent has no separate consult leg; their main leg is the active call, so
|
|
415
|
+
// mute stays available while they consult (the heuristic below targets the initiator's
|
|
416
|
+
// inactive leg, not the consultee's only leg).
|
|
417
|
+
const isConsultedActiveMainLeg = isConsulted && currentLeg === 'main';
|
|
418
|
+
if (
|
|
419
|
+
(isConsulting || hasParallelConsultLeg) &&
|
|
420
|
+
!isCurrentLegActive &&
|
|
421
|
+
!isConsultedActiveMainLeg
|
|
422
|
+
) {
|
|
423
|
+
return VISIBLE_DISABLED;
|
|
424
|
+
}
|
|
311
425
|
if (isConsulting) return VISIBLE_ENABLED;
|
|
312
426
|
|
|
313
427
|
if (isConnected || isHeld || isConferencing) {
|
|
@@ -383,6 +497,18 @@ function computeVoiceInteractionUIControls(
|
|
|
383
497
|
consult: (() => {
|
|
384
498
|
const isConnectedOrHeld = state === TaskState.CONNECTED || state === TaskState.HELD;
|
|
385
499
|
|
|
500
|
+
if (
|
|
501
|
+
isHeld &&
|
|
502
|
+
!inConference &&
|
|
503
|
+
!effectiveHasConsultMedia &&
|
|
504
|
+
selfParticipant?.consultState === 'consultCompleted' &&
|
|
505
|
+
!taskData?.isConsulted
|
|
506
|
+
) {
|
|
507
|
+
const canRetryConsult = !maxParticipants && customerPresent && !otherAgentConsultInProgress;
|
|
508
|
+
|
|
509
|
+
return {isVisible: true, isEnabled: canRetryConsult};
|
|
510
|
+
}
|
|
511
|
+
|
|
386
512
|
if (inConference && nonOwnerPostConsultCompletedHeldMainLeg) return VISIBLE_DISABLED;
|
|
387
513
|
if (hasParallelConsultLeg) return DISABLED;
|
|
388
514
|
if (!hasFullControls || !(isConnectedOrHeld || inConference)) {
|
|
@@ -469,6 +595,7 @@ function computeVoiceInteractionUIControls(
|
|
|
469
595
|
// ExitConference: in conference with multiple agents in main call
|
|
470
596
|
exitConference: (() => {
|
|
471
597
|
if (hideExitConferenceWhileConsultPending) return DISABLED;
|
|
598
|
+
if (hideExitConferenceOnMainLegForEpDnConsultFromConference) return DISABLED;
|
|
472
599
|
if (allowHeldMainLegControlsForNonInitiator) return VISIBLE_ENABLED;
|
|
473
600
|
if (showMainLegConferenceControlsDuringConsult) return VISIBLE_DISABLED;
|
|
474
601
|
if (hideExitConferenceDuringActiveConsultFromConference) return DISABLED;
|
|
@@ -578,10 +705,21 @@ function getVoiceLegState(
|
|
|
578
705
|
};
|
|
579
706
|
}
|
|
580
707
|
|
|
581
|
-
const taskData = context.taskData ??
|
|
708
|
+
const taskData = fallbackTaskData ?? context.taskData ?? null;
|
|
582
709
|
const interaction = taskData?.interaction;
|
|
583
710
|
const mainCallId = interaction?.mainInteractionId || taskData?.interactionId;
|
|
584
711
|
const selfAgentId = config.agentId ?? taskData?.agentId;
|
|
712
|
+
const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
|
|
713
|
+
const hasConsultMedia = hasConsultMediaInInteraction(
|
|
714
|
+
interaction,
|
|
715
|
+
taskData?.consultMediaResourceId
|
|
716
|
+
);
|
|
717
|
+
const isConsultEndedForSelf =
|
|
718
|
+
taskData?.type === 'AgentConsultEnded' ||
|
|
719
|
+
taskData?.type === 'AgentConsultFailed' ||
|
|
720
|
+
(selfParticipant?.consultState === 'consultCompleted' &&
|
|
721
|
+
!hasConsultMedia &&
|
|
722
|
+
taskData?.isConsulted === false);
|
|
585
723
|
const consultInProgress = getIsConsultInProgressForConferenceControls(
|
|
586
724
|
interaction,
|
|
587
725
|
mainCallId,
|
|
@@ -594,30 +732,48 @@ function getVoiceLegState(
|
|
|
594
732
|
const consultOwnedBySelf =
|
|
595
733
|
context.consultInitiator ||
|
|
596
734
|
(Boolean(selfAgentId) && taskData?.consultingAgentId === selfAgentId);
|
|
597
|
-
const hasConsultMedia = Boolean(
|
|
598
|
-
taskData?.consultMediaResourceId ||
|
|
599
|
-
Object.values(interaction?.media ?? {}).some((media: any) => media?.mType === 'consult')
|
|
600
|
-
);
|
|
601
|
-
const selfParticipant = selfAgentId ? interaction?.participants?.[selfAgentId] : null;
|
|
602
735
|
const selfConsultPendingOnConsultMedia =
|
|
603
736
|
selfParticipant?.consultState === 'consultInitiated' &&
|
|
604
737
|
!taskData?.isConsulted &&
|
|
605
738
|
hasConsultMedia;
|
|
606
739
|
const selfConsultingOnConsultMedia =
|
|
607
740
|
selfParticipant?.consultState === 'consulting' && hasConsultMedia;
|
|
741
|
+
const isConsultUnansweredFailure =
|
|
742
|
+
!taskData?.isConsulted &&
|
|
743
|
+
!hasConsultMedia &&
|
|
744
|
+
selfParticipant?.consultState === 'consultCompleted' &&
|
|
745
|
+
Boolean(mainCallId && taskData?.interaction?.media?.[mainCallId]?.isHold === true) &&
|
|
746
|
+
taskData?.interaction?.state !== 'conference' &&
|
|
747
|
+
!getIsConferenceInProgress(taskData);
|
|
608
748
|
const hasConsultLeg = Boolean(
|
|
609
|
-
!
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
749
|
+
!isConsultEndedForSelf &&
|
|
750
|
+
!interaction?.isTerminated &&
|
|
751
|
+
!isConsultUnansweredFailure &&
|
|
752
|
+
(consultOwnedBySelf || selfConsultingOnConsultMedia || selfConsultPendingOnConsultMedia) &&
|
|
613
753
|
(consultInProgress || isConsultingState || context.consultCallHeld || hasConsultMedia)
|
|
614
754
|
);
|
|
615
755
|
|
|
616
756
|
if (!hasConsultLeg) {
|
|
757
|
+
let mainState = currentState;
|
|
758
|
+
if (isConsultEndedForSelf && taskData?.interaction) {
|
|
759
|
+
const resolvedMainId = taskData.interaction.mainInteractionId || taskData.interactionId;
|
|
760
|
+
const isMainHeld = Boolean(
|
|
761
|
+
resolvedMainId && taskData.interaction.media?.[resolvedMainId]?.isHold === true
|
|
762
|
+
);
|
|
763
|
+
|
|
764
|
+
if (taskData.interaction.state === 'conference' || getIsConferenceInProgress(taskData)) {
|
|
765
|
+
mainState = TaskState.CONFERENCING;
|
|
766
|
+
} else if (isMainHeld || taskData.interaction.state === 'hold') {
|
|
767
|
+
mainState = TaskState.HELD;
|
|
768
|
+
} else {
|
|
769
|
+
mainState = TaskState.CONNECTED;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
617
773
|
return {
|
|
618
774
|
hasConsultLeg: false,
|
|
619
775
|
activeLeg: 'main',
|
|
620
|
-
mainState
|
|
776
|
+
mainState,
|
|
621
777
|
consultState: TaskState.CONSULTING,
|
|
622
778
|
};
|
|
623
779
|
}
|
|
@@ -89,6 +89,67 @@ describe('Task (base class)', () => {
|
|
|
89
89
|
expect((task.data as any).foo).toBeUndefined();
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
it('drops stale interaction.media/participants entries the backend no longer sends (merge keeps other fields)', () => {
|
|
93
|
+
const withConsult = createTaskData({
|
|
94
|
+
interaction: {
|
|
95
|
+
callAssociatedData: {a: {value: 'keep-me'}},
|
|
96
|
+
media: {
|
|
97
|
+
main: {mediaResourceId: 'main', mType: 'mainCall', isHold: true},
|
|
98
|
+
consult: {mediaResourceId: 'consult', mType: 'consult', isHold: false},
|
|
99
|
+
},
|
|
100
|
+
participants: {
|
|
101
|
+
'customer-1': {id: 'customer-1', pType: 'Customer'},
|
|
102
|
+
'agent-1': {id: 'agent-1', pType: 'Agent'},
|
|
103
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', consultState: 'consulting'},
|
|
104
|
+
},
|
|
105
|
+
} as any,
|
|
106
|
+
}) as unknown as TaskData;
|
|
107
|
+
task.updateTaskData(withConsult, true);
|
|
108
|
+
|
|
109
|
+
// Backend resume snapshot: consult leg gone (only main media + main participants).
|
|
110
|
+
const resumeData = createTaskData({
|
|
111
|
+
interaction: {
|
|
112
|
+
media: {main: {mediaResourceId: 'main', mType: 'mainCall', isHold: false}},
|
|
113
|
+
participants: {
|
|
114
|
+
'customer-1': {id: 'customer-1', pType: 'Customer'},
|
|
115
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', consultState: null},
|
|
116
|
+
},
|
|
117
|
+
} as any,
|
|
118
|
+
}) as unknown as TaskData;
|
|
119
|
+
task.updateTaskData(resumeData);
|
|
120
|
+
|
|
121
|
+
const interaction = (task.data as any).interaction;
|
|
122
|
+
// Stale consult media + consultee participant (absent from the resume snapshot) are removed.
|
|
123
|
+
expect(interaction.media.consult).toBeUndefined();
|
|
124
|
+
expect(interaction.participants['agent-2']).toBeUndefined();
|
|
125
|
+
// Entries still present in the incoming snapshot survive and reflect the new values.
|
|
126
|
+
expect(interaction.media.main).toBeDefined();
|
|
127
|
+
expect(interaction.media.main.isHold).toBe(false);
|
|
128
|
+
expect(interaction.participants['agent-1']).toBeDefined();
|
|
129
|
+
expect(interaction.participants['customer-1']).toBeDefined();
|
|
130
|
+
// Unrelated merge-able fields (CAD) are preserved.
|
|
131
|
+
expect(interaction.callAssociatedData.a.value).toBe('keep-me');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not touch interaction maps when the incoming payload omits them', () => {
|
|
135
|
+
const withConsult = createTaskData({
|
|
136
|
+
interaction: {
|
|
137
|
+
media: {main: {mediaResourceId: 'main'}, consult: {mediaResourceId: 'consult'}},
|
|
138
|
+
participants: {'agent-1': {id: 'agent-1'}, 'agent-2': {id: 'agent-2'}},
|
|
139
|
+
} as any,
|
|
140
|
+
}) as unknown as TaskData;
|
|
141
|
+
task.updateTaskData(withConsult, true);
|
|
142
|
+
|
|
143
|
+
// A partial update with no interaction maps must not prune existing media/participants.
|
|
144
|
+
task.updateTaskData({foo: 'changed'} as unknown as TaskData);
|
|
145
|
+
|
|
146
|
+
const interaction = (task.data as any).interaction;
|
|
147
|
+
expect(interaction.media.main).toBeDefined();
|
|
148
|
+
expect(interaction.media.consult).toBeDefined();
|
|
149
|
+
expect(interaction.participants['agent-1']).toBeDefined();
|
|
150
|
+
expect(interaction.participants['agent-2']).toBeDefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
92
153
|
it('getUIControls returns default controls shape for idle voice task', () => {
|
|
93
154
|
const controls = task.uiControls;
|
|
94
155
|
const mainControls = controls.main;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
isSecondaryAgent,
|
|
14
14
|
isSecondaryEpDnAgent,
|
|
15
15
|
getConsultMediaResourceId,
|
|
16
|
+
getIsConsultInProgressForConferenceControls,
|
|
16
17
|
} from '../../../../../src/services/task/TaskUtils';
|
|
17
18
|
import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
|
|
18
19
|
import {LoginOption} from '../../../../../src/types';
|
|
@@ -571,4 +572,68 @@ describe('TaskUtils', () => {
|
|
|
571
572
|
expect(result).toBe('direct-id');
|
|
572
573
|
});
|
|
573
574
|
});
|
|
575
|
+
|
|
576
|
+
describe('getIsConsultInProgressForConferenceControls', () => {
|
|
577
|
+
it('returns false when consultee is reserved but has not joined (RONA)', () => {
|
|
578
|
+
const interaction = {
|
|
579
|
+
participants: {
|
|
580
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
|
|
581
|
+
'agent-2': {
|
|
582
|
+
id: 'agent-2',
|
|
583
|
+
pType: 'Agent',
|
|
584
|
+
hasLeft: false,
|
|
585
|
+
hasJoined: false,
|
|
586
|
+
isConsulted: true,
|
|
587
|
+
consultState: 'consultReserved',
|
|
588
|
+
},
|
|
589
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
590
|
+
},
|
|
591
|
+
media: {
|
|
592
|
+
'interaction-1': {
|
|
593
|
+
mType: 'mainCall',
|
|
594
|
+
participants: ['customer-1', 'agent-1'],
|
|
595
|
+
},
|
|
596
|
+
'consult-media': {
|
|
597
|
+
mType: 'consult',
|
|
598
|
+
participants: ['agent-2', 'agent-1'],
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
} as any;
|
|
602
|
+
|
|
603
|
+
expect(
|
|
604
|
+
getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
|
|
605
|
+
).toBe(false);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('returns true when consultee is actively consulting', () => {
|
|
609
|
+
const interaction = {
|
|
610
|
+
participants: {
|
|
611
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false, isConsulted: false},
|
|
612
|
+
'agent-2': {
|
|
613
|
+
id: 'agent-2',
|
|
614
|
+
pType: 'Agent',
|
|
615
|
+
hasLeft: false,
|
|
616
|
+
hasJoined: true,
|
|
617
|
+
isConsulted: true,
|
|
618
|
+
consultState: 'consulting',
|
|
619
|
+
},
|
|
620
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
621
|
+
},
|
|
622
|
+
media: {
|
|
623
|
+
'interaction-1': {
|
|
624
|
+
mType: 'mainCall',
|
|
625
|
+
participants: ['customer-1', 'agent-1'],
|
|
626
|
+
},
|
|
627
|
+
'consult-media': {
|
|
628
|
+
mType: 'consult',
|
|
629
|
+
participants: ['agent-2', 'agent-1'],
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
} as any;
|
|
633
|
+
|
|
634
|
+
expect(
|
|
635
|
+
getIsConsultInProgressForConferenceControls(interaction, 'interaction-1', 'agent-1')
|
|
636
|
+
).toBe(true);
|
|
637
|
+
});
|
|
638
|
+
});
|
|
574
639
|
});
|