From 03b80c5eb33a9a5524286bd6df043a20e70289f8 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Thu, 9 Oct 2025 10:00:07 +0530 Subject: [PATCH 01/48] feat: multi conference --- .../call-control-consult.tsx | 9 ++- .../call-control-custom.utils.ts | 32 +++++++++ .../task/CallControl/call-control.tsx | 2 + .../task/CallControl/call-control.utils.ts | 13 +++- .../task/CallControlCAD/call-control-cad.tsx | 34 ++++++++++ .../components/task/TaskList/task-list.tsx | 4 +- .../task/TaskList/task-list.utils.ts | 9 ++- .../src/components/task/task.types.ts | 24 ++++++- .../contact-center/store/src/store.types.ts | 10 +++ .../store/src/storeEventsWrapper.ts | 67 ++++++++++++++++--- .../contact-center/store/src/task-utils.ts | 5 +- .../task/src/TaskList/index.tsx | 3 +- .../task/src/Utils/constants.ts | 4 ++ .../task/src/Utils/task-util.ts | 32 ++++++++- packages/contact-center/task/src/helper.ts | 39 ++++++++++- 15 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 packages/contact-center/task/src/Utils/constants.ts diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx index fdb701de7..30bfdf175 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -11,6 +11,7 @@ import { handleMuteToggle, getConsultStatusText, createTimerKey, + handleConsultConferencePress, } from './call-control-custom.utils'; const CallControlConsultComponent: React.FC = ({ @@ -18,6 +19,7 @@ const CallControlConsultComponent: React.FC = startTimeStamp, onTransfer, endConsultCall, + consultConference, consultCompleted, isAgentBeingConsulted, isEndConsultEnabled, @@ -42,6 +44,10 @@ const CallControlConsultComponent: React.FC = handleMuteToggle(onToggleConsultMute, setIsMuteDisabled, logger); }; + const handleConsultConference = () => { + handleConsultConferencePress(consultConference, logger); + }; + const buttons = createConsultButtons( isMuted, isMuteDisabled, @@ -51,7 +57,8 @@ const CallControlConsultComponent: React.FC = muteUnmute, onTransfer ? handleTransfer : undefined, handleConsultMuteToggle, - handleEndConsult + handleEndConsult, + handleConsultConference ); // Filter buttons that should be shown, then map them diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts index 06d48003d..c4ed054f6 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -35,6 +35,7 @@ export const createConsultButtons = ( onTransfer?: () => void, handleConsultMuteToggle?: () => void, handleEndConsult?: () => void, + handleConsultConferencePress?: () => void, logger? ): ButtonConfig[] => { try { @@ -57,6 +58,15 @@ export const createConsultButtons = ( disabled: !consultCompleted, shouldShow: isAgentBeingConsulted && !!onTransfer, }, + { + key: 'conference', + icon: 'call-merge-bold', + tooltip: 'Consult Conference', + onClick: handleConsultConferencePress || (() => {}), + className: 'call-control-button', + disabled: !consultCompleted, + shouldShow: isAgentBeingConsulted && !!handleConsultConferencePress, + }, { key: 'cancel', icon: 'headset-muted-bold', @@ -160,6 +170,28 @@ export const handleEndConsultPress = (endConsultCall: (() => void) | undefined, } }; +/** + * Handles merge consult/conference button press with logging + */ +export const handleConsultConferencePress = (consultConference: (() => void) | undefined, logger: ILogger): void => { + logger.info('CC-Widgets: CallControlConsult: consultConference clicked', { + module: 'call-control-consult.tsx', + method: 'handleConsultConferencePress', + }); + + try { + if (consultConference) { + consultConference(); + logger.log('CC-Widgets: CallControlConsult: consultConference completed', { + module: 'call-control-consult.tsx', + method: 'handleConsultConferencePress', + }); + } + } catch (error) { + throw new Error(`Error consultConference: ${error}`); + } +}; + /** * Handles mute toggle with disabled state management */ diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index aea258963..e36061df0 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -52,6 +52,7 @@ function CallControlComponent(props: CallControlComponentProps) { loadQueues, transferCall, consultCall, + exitConference, consultInitiated, consultAccepted, callControlAudio, @@ -127,6 +128,7 @@ function CallControlComponent(props: CallControlComponentProps) { handletoggleHold, toggleRecording, endCall, + exitConference, logger ); diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts index dc1c47909..8d55d5216 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts @@ -207,6 +207,7 @@ export const buildCallControlButtons = ( handleToggleHoldFunc: () => void, toggleRecording: () => void, endCall: () => void, + exitConference: () => void, logger? ): CallControlButton[] => { try { @@ -227,7 +228,7 @@ export const buildCallControlButtons = ( onClick: handleToggleHoldFunc, tooltip: isHeld ? RESUME_CALL : HOLD_CALL, className: 'call-control-button', - disabled: false, + disabled: controlVisibility.isConferenceInProgress, isVisible: controlVisibility.holdResume, dataTestId: 'call-control:hold-toggle', }, @@ -241,6 +242,16 @@ export const buildCallControlButtons = ( isVisible: controlVisibility.consult, dataTestId: 'call-control:consult', }, + { + id: 'exitConference', + icon: 'exit-room-bold', + tooltip: 'Exit Conference', + className: 'call-control-button-muted', + onClick: exitConference, + disabled: !controlVisibility.isConferenceInProgress, + isVisible: controlVisibility.isConferenceInProgress, + dataTestId: 'call-control:exit-conference', + }, { id: 'transfer', icon: 'next-bold', diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index 96b88e944..8625236de 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -6,6 +6,8 @@ import './call-control-cad.styles.scss'; import TaskTimer from '../TaskTimer/index'; import CallControlConsultComponent from '../CallControl/CallControlCustom/call-control-consult'; import {MEDIA_CHANNEL as MediaChannelType, CallControlComponentProps} from '../task.types'; +// import {Select, Option} from '@momentum-design/components/dist/react'; + import {getMediaTypeInfo} from '../../../utils'; import { NO_CUSTOMER_NAME, @@ -34,6 +36,7 @@ const CallControlCADComponent: React.FC = (props) => endConsultCall, consultCompleted, consultTransfer, + consultConference, callControlClassName, callControlConsultClassName, startTimestamp, @@ -65,6 +68,8 @@ const CallControlCADComponent: React.FC = (props) => //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const ani = currentTask?.data?.interaction?.callAssociatedDetails?.ani; + const participants = currentTask?.data?.interaction?.participants || {}; + // Create unique IDs for tooltips const customerNameTriggerId = `customer-name-trigger-${currentTask.data.interaction.interactionId}`; const customerNameTooltipId = `customer-name-tooltip-${currentTask.data.interaction.interactionId}`; @@ -167,6 +172,34 @@ const CallControlCADComponent: React.FC = (props) => {currentMediaType.labelName} - + {/* {controlVisibility.isConferenceInProgress && ( + + )} */}
{!controlVisibility.wrapup && isHeld && ( <> @@ -220,6 +253,7 @@ const CallControlCADComponent: React.FC = (props) => startTimeStamp={consultStartTimeStamp} endConsultCall={endConsultCall} onTransfer={consultTransfer} + consultConference={consultConference} consultCompleted={consultCompleted} isAgentBeingConsulted={!consultAccepted} isEndConsultEnabled={isEndConsultEnabled} diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 76a3fae83..795fb808f 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -12,7 +12,7 @@ import './styles.scss'; import {withMetrics} from '@webex/cc-ui-logging'; const TaskListComponent: React.FunctionComponent = (props) => { - const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger} = props; + const {currentTask, taskList, acceptTask, declineTask, isBrowser, onTaskSelect, logger, agentId} = props; // Early return for empty task list if (isTaskListEmpty(taskList)) { @@ -25,7 +25,7 @@ const TaskListComponent: React.FunctionComponent = (prop
    {tasks.map((task, index) => { // Extract all task data using the utility function - const taskData = extractTaskListItemData(task, isBrowser); + const taskData = extractTaskListItemData(task, isBrowser, agentId, logger); // Log task rendering logger.info('CC-Widgets: TaskList: rendering task list', { diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts index d745b8b48..99c275b38 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts @@ -7,7 +7,12 @@ import {isIncomingTask} from '@webex/cc-store'; * @param isBrowser - Whether the device type is browser * @returns Processed task data with computed values */ -export const extractTaskListItemData = (task: ITask, isBrowser: boolean, logger?): TaskListItemData => { +export const extractTaskListItemData = ( + task: ITask, + isBrowser: boolean, + agentId: string, + logger? +): TaskListItemData => { try { // Extract basic data from task //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 @@ -21,7 +26,7 @@ export const extractTaskListItemData = (task: ITask, isBrowser: boolean, logger? const taskState = task.data.interaction.state; const startTimeStamp = task.data.interaction.createdTimestamp; - const isTaskIncoming = isIncomingTask(task); + const isTaskIncoming = isIncomingTask(task, agentId); const mediaType = task.data.interaction.mediaType; const mediaChannel = task.data.interaction.mediaChannel; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index a96c41b06..eedc22041 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -115,6 +115,11 @@ export interface TaskProps { * The logger instance from SDK */ logger: ILogger; + + /** + * Agent ID of the logged-in user + */ + agentId: string; } export type IncomingTaskComponentProps = Pick & @@ -122,7 +127,7 @@ export type IncomingTaskComponentProps = Pick & Partial>; @@ -275,6 +280,16 @@ export interface ControlProps { */ consultCall: (consultDestination: string, destinationType: DestinationType) => void; + /** + * Function to merge the consult call in a conference. + */ + consultConference: () => void; + + /** + * Function to exit Conference + */ + exitConference: () => void; + /** * Function to end the consult call. */ @@ -401,6 +416,7 @@ export interface ControlProps { pauseResumeRecording: boolean; endConsult: boolean; recordingIndicator: boolean; + isConferenceInProgress: boolean; }; secondsUntilAutoWrapup?: number; @@ -429,6 +445,8 @@ export type CallControlComponentProps = Pick< | 'loadBuddyAgents' | 'transferCall' | 'consultCall' + | 'consultConference' + | 'exitConference' | 'endConsultCall' | 'consultInitiated' | 'consultTransfer' @@ -525,6 +543,7 @@ export interface CallControlConsultComponentsProps { startTimeStamp: number; onTransfer?: () => void; endConsultCall?: () => void; + consultConference?: () => void; consultCompleted: boolean; isAgentBeingConsulted: boolean; isEndConsultEnabled: boolean; @@ -537,7 +556,7 @@ export interface CallControlConsultComponentsProps { /** * Type representing the possible menu types in call control. */ -export type CallControlMenuType = 'Consult' | 'Transfer'; +export type CallControlMenuType = 'Consult' | 'Transfer' | 'ExitConference'; export const MEDIA_CHANNEL = { EMAIL: 'email', @@ -591,6 +610,7 @@ export interface ControlVisibility { pauseResumeRecording: boolean; endConsult: boolean; recordingIndicator: boolean; + isConferenceInProgress: boolean; } export interface MediaTypeInfo { diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index f85239ed1..f28184e9e 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -170,6 +170,16 @@ enum TASK_EVENTS { TASK_RECORDING_PAUSED = 'task:recordingPaused', TASK_RECORDING_RESUMED = 'task:recordingResumed', TASK_OFFER_CONSULT = 'task:offerConsult', + TASK_CONFERENCE_ESTABLISHING = 'task:conferenceEstablishing', + TASK_CONFERENCE_STARTED = 'task:conferenceStarted', + TASK_CONFERENCE_FAILED = 'task:conferenceFailed', + TASK_CONFERENCE_ENDED = 'task:conferenceEnded', + TASK_PARTICIPANT_JOINED = 'task:participantJoined', + TASK_PARTICIPANT_LEFT = 'task:participantLeft', + TASK_CONFERENCE_TRANSFERRED = 'task:conferenceTransferred', + TASK_CONFERENCE_TRANSFER_FAILED = 'task:conferenceTransferFailed', + TASK_CONFERENCE_END_FAILED = 'task:conferenceEndFailed', + TASK_PARTICIPANT_LEFT_FAILED = 'task:participantLeftFailed', } // TODO: remove this once cc sdk exports this enum // Events that are received on the contact center SDK diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 19d3a03f8..62a253547 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -206,7 +206,7 @@ class StoreWrapper implements IStoreWrapper { setCurrentTask = (task: ITask | null, isClicked: boolean = false): void => { // Don't assign the task as current task is incoming - if (isIncomingTask(task)) return; + if (isIncomingTask(task, this.agentId)) return; runInAction(() => { // Determine if the new task is the same as the current task @@ -245,14 +245,17 @@ class StoreWrapper implements IStoreWrapper { refreshTaskList = (): void => { runInAction(() => { this.store.taskList = this.store.cc.taskManager.getAllTasks(); + if (Object.keys(this.store.taskList).length === 0) { + this.setCurrentTask(null); + this.setState({ + reset: true, + }); + } else if (this.currentTask) { + this.setCurrentTask(this.store.taskList[this.currentTask?.data?.interactionId]); + } else if (Object.keys(this.store.taskList).length > 0) { + this.setCurrentTask(this.store.taskList[Object.keys(this.store.taskList)[0]]); + } }); - if (this.currentTask) { - this.setCurrentTask(this.store.taskList[this.currentTask?.data?.interactionId]); - } else if (Object.keys(this.store.taskList).length > 0) { - this.setCurrentTask(this.store.taskList[Object.keys(this.store.taskList)[0]]); - } else if (Object.keys(this.store.taskList).length === 0) { - this.setCurrentTask(null); - } }; setWrapupCodes = (wrapupCodes: IWrapupCode[]): void => { @@ -400,6 +403,16 @@ class StoreWrapper implements IStoreWrapper { taskToRemove.off(TASK_EVENTS.AGENT_OFFER_CONTACT, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_HOLD, this.refreshTaskList); taskToRemove.off(TASK_EVENTS.TASK_UNHOLD, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_FAILED, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + taskToRemove.off(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, this.refreshTaskList); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.handleConferenceEnded); + taskToRemove.off(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); if (this.deviceType === 'BROWSER') { taskToRemove.off(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); this.setCallControlAudio(null); @@ -521,6 +534,24 @@ class StoreWrapper implements IStoreWrapper { this.setConsultStartTimeStamp(null); }; + handleConferenceStarted = () => { + runInAction(() => { + this.setConsultAccepted(false); + this.setConsultInitiated(false); + this.setConsultCompleted(false); + this.setConsultOfferReceived(false); + this.setIsQueueConsultInProgress(false); + this.setCurrentConsultQueueId(null); + this.setConsultCompleted(false); + this.setConsultStartTimeStamp(null); + }); + this.refreshTaskList(); + }; + + handleConferenceEnded = (task) => { + this.refreshTaskList(); + }; + handleIncomingTask = (event) => { const task: ITask = event; // Attach event listeners to the task @@ -547,6 +578,16 @@ class StoreWrapper implements IStoreWrapper { task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); task.on(TASK_EVENTS.TASK_HOLD, this.refreshTaskList); task.on(TASK_EVENTS.TASK_UNHOLD, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); // In case of consulting we check if the task is already in the task list // If it is, we dont have to send the incoming task callback @@ -605,6 +646,16 @@ class StoreWrapper implements IStoreWrapper { task.on(TASK_EVENTS.TASK_OFFER_CONSULT, this.handleConsultOffer); task.on(TASK_EVENTS.TASK_CONSULT_END, this.handleConsultEnd); task.on(TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED, this.handleConsultQueueCancelled); + task.on(TASK_EVENTS.TASK_CONFERENCE_ENDED, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_CONFERENCE_END_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_ESTABLISHING, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_PARTICIPANT_JOINED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT, this.handleConferenceEnded); + task.on(TASK_EVENTS.TASK_PARTICIPANT_LEFT_FAILED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_STARTED, this.handleConferenceStarted); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFERRED, this.refreshTaskList); + task.on(TASK_EVENTS.TASK_CONFERENCE_TRANSFER_FAILED, this.refreshTaskList); if (this.deviceType === 'BROWSER') { task.on(TASK_EVENTS.TASK_MEDIA, this.handleTaskMedia); } diff --git a/packages/contact-center/store/src/task-utils.ts b/packages/contact-center/store/src/task-utils.ts index 240efc4bb..477b34569 100644 --- a/packages/contact-center/store/src/task-utils.ts +++ b/packages/contact-center/store/src/task-utils.ts @@ -5,16 +5,15 @@ import {ITask} from './store.types'; * @param task - The task object * @returns Whether the task is incoming */ -export const isIncomingTask = (task: ITask): boolean => { +export const isIncomingTask = (task: ITask, agentId: string): boolean => { const taskData = task?.data; const taskState = taskData?.interaction?.state; - const agentId = taskData?.agentId; const participants = taskData?.interaction?.participants; const hasJoined = agentId && participants?.[agentId]?.hasJoined; return ( !taskData?.wrapUpRequired && !hasJoined && - (taskState === 'new' || taskState === 'consult' || taskState === 'connected') + (taskState === 'new' || taskState === 'consult' || taskState === 'connected' || taskState === 'conference') ); }; diff --git a/packages/contact-center/task/src/TaskList/index.tsx b/packages/contact-center/task/src/TaskList/index.tsx index 999c317ac..3cd675c52 100644 --- a/packages/contact-center/task/src/TaskList/index.tsx +++ b/packages/contact-center/task/src/TaskList/index.tsx @@ -9,13 +9,14 @@ import {TaskListProps} from '../task.types'; const TaskListInternal: React.FunctionComponent = observer( ({onTaskAccepted, onTaskDeclined, onTaskSelected}) => { - const {cc, taskList, currentTask, deviceType, logger} = store; + const {cc, taskList, currentTask, deviceType, logger, agentId} = store; const result = useTaskList({cc, deviceType, logger, taskList, onTaskAccepted, onTaskDeclined, onTaskSelected}); const props = { ...result, currentTask, logger, + agentId, }; return ; diff --git a/packages/contact-center/task/src/Utils/constants.ts b/packages/contact-center/task/src/Utils/constants.ts new file mode 100644 index 000000000..45624fbb2 --- /dev/null +++ b/packages/contact-center/task/src/Utils/constants.ts @@ -0,0 +1,4 @@ +export const AGENT = 'Agent'; +export const CUSTOMER = 'Customer'; +export const SUPERVISOR = 'Supervisor'; +export const VVA = 'VVA'; diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index 95dea2710..fbf1b3f15 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -1,5 +1,6 @@ import {ILogger} from '@webex/cc-store'; import {ITask} from '@webex/contact-center'; +import {CUSTOMER, SUPERVISOR, VVA} from './constants'; /** * This function determines the visibility of various controls based on the task's data. @@ -25,6 +26,8 @@ export function getControlsVisibility( const {isEndCallEnabled, isEndConsultEnabled, webRtcEnabled} = featureFlags; + const isTransferVisibility = isBrowser ? webRtcEnabled : true; // Applicable for all type of station login and media type + const controls = { accept: (isBrowser && ((webRtcEnabled && isCall) || isChat || isEmail)) || @@ -36,13 +39,13 @@ export function getControlsVisibility( holdResume: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login consult: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login - transfer: isBrowser ? webRtcEnabled : true, // Applicable for all type of station login and media type - + transfer: isTransferVisibility && !task.data.isConferenceInProgress, conference: (isBrowser && isCall && webRtcEnabled) || isChat, // This needs further testing after we add support wrapup: task?.data?.wrapUpRequired ?? false, // Applicable for all type of station login and media type and getting actual value from task data pauseResumeRecording: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Getting feature flag (isRecordingManagementEnabled) value as undefined, need further testing endConsult: isEndConsultEnabled && isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), recordingIndicator: isCall, + isConferenceInProgress: task.data.isConferenceInProgress, }; return controls; @@ -65,6 +68,7 @@ export function getControlsVisibility( pauseResumeRecording: false, endConsult: false, recordingIndicator: false, + isConferenceInProgress: false, }; } } @@ -87,3 +91,27 @@ export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall', return null; } } + +export const getIsConferenceInProgress = (task: ITask): boolean => { + const mediaMainCall = task?.data?.interaction?.media[task?.data?.interactionId]; + const participantsInMainCall = new Set(mediaMainCall?.participants); + const participants = task?.data?.interaction?.participants; + + const agentParticipants = new Set(); + if (participantsInMainCall.size > 0) { + participantsInMainCall.forEach((participantId: string) => { + const participant = participants[participantId]; + if ( + participant && + participant.pType !== CUSTOMER && + participant.pType !== SUPERVISOR && + !participant.hasLeft && + participant.pType !== VVA + ) { + agentParticipants.add(participantId); + } + }); + } + + return agentParticipants.size >= 2; +}; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index cf8c79238..cc0ad91ac 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -696,6 +696,28 @@ export const useCallControl = (props: useCallControlProps) => { } }; + const consultConference = async () => { + try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + await currentTask.consultConference(); + logger.info('consultConference success', {module: 'useCallControl', method: 'consultConference'}); + } catch (error) { + logger.error(`Error consulting conference: ${error}`, {module: 'useCallControl', method: 'consultConference'}); + throw error; + } + }; + + const exitConference = async () => { + try { + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + await currentTask.exitConference(); + logger.info('exitConference success', {module: 'useCallControl', method: 'exitConference'}); + } catch (error) { + logger.error(`Error exiting conference: ${error}`, {module: 'useCallControl', method: 'exitConference'}); + throw error; + } + }; + const consultCall = async (consultDestination: string, destinationType: DestinationType) => { const consultPayload = { to: consultDestination, @@ -746,8 +768,18 @@ export const useCallControl = (props: useCallControlProps) => { const consultTransfer = async () => { try { - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - await currentTask.consultTransfer(); + if (currentTask.data.isConferenceInProgress) { + logger.info('Conference in progress, using transferConference', { + module: 'useCallControl', + method: 'transferCall', + }); + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + await currentTask.transferConference(); + } else { + logger.info('Consult transfer initiated', {module: 'useCallControl', method: 'consultTransfer'}); + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + await currentTask.consultTransfer(); + } store.setConsultInitiated(true); } catch (error) { logError(`Error transferring consult call: ${error}`, 'consultTransfer'); @@ -791,7 +823,6 @@ export const useCallControl = (props: useCallControlProps) => { logger.error('CC-Widgets: CallControl: Error initializing auto wrap-up timer', { module: 'widget-cc-task#helper.ts', method: 'useCallControl#autoWrapupTimer', - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 error, }); } @@ -825,6 +856,8 @@ export const useCallControl = (props: useCallControlProps) => { consultCall, endConsultCall, consultTransfer, + consultConference, + exitConference, consultAgentName, setConsultAgentName, consultAgentId, From 73a5b546820ddca8a0f5a91aaf7e482148b4318b Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Thu, 9 Oct 2025 11:33:05 +0530 Subject: [PATCH 02/48] feat: multi conference tests --- .../call-control-consult.snapshot.tsx.snap | 531 +++++++++++++++--- .../call-control-consult.tsx | 8 +- .../call-control-custom.util.tsx | 186 ++++-- .../CallControl/call-control.snapshot.tsx | 3 + .../task/CallControl/call-control.tsx | 3 + .../task/CallControl/call-control.utils.tsx | 49 +- .../call-control-cad.snapshot.tsx.snap | 77 ++- .../call-control-cad.snapshot.tsx | 3 + .../task/CallControlCAD/call-control-cad.tsx | 3 + .../task/TaskList/task-list.snapshot.tsx | 1 + .../components/task/TaskList/task-list.tsx | 1 + .../task/TaskList/task-list.utils.tsx | 12 +- 12 files changed, 720 insertions(+), 157 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap index 825d4a9c9..6619e357a 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap @@ -39,7 +39,7 @@ exports[`CallControlConsultComponent Snapshots Interactions should call mockEndC class="consult-buttons consult-buttons-container" > +
    +

    + Consult Conference +

    +
    + +
    +

    + Consult Conference +

    +
    + +
    +

    + Consult Conference +

    +
    +
+
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ +
+

+ Consult Conference +

+
+ + } + > +
+ {participantsList?.map((participant) => ( +
+ {participant.name} +
+ ))} +
+ + + + )} +
{!controlVisibility.wrapup && isHeld && ( <> From a0fd96444d21bd71180aa1054779fd94c3876c00 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Thu, 9 Oct 2025 18:54:24 +0530 Subject: [PATCH 05/48] feat: multi conference fixed participants list logic --- .../task/CallControlCAD/call-control-cad.tsx | 8 ++--- .../src/components/task/task.types.ts | 12 ++++++++ .../task/src/Utils/task-util.ts | 30 +++++++++++++++++++ packages/contact-center/task/src/helper.ts | 14 +++++++-- .../contact-center/task/src/task.types.ts | 10 +++---- 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index d18754992..f688d1843 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -44,6 +44,7 @@ const CallControlCADComponent: React.FC = (props) => logger, isMuted, toggleMute, + conferenceParticipants, } = props; const formatTime = (time: number): string => { @@ -67,9 +68,6 @@ const CallControlCADComponent: React.FC = (props) => //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const ani = currentTask?.data?.interaction?.callAssociatedDetails?.ani; - const participants = currentTask?.data?.interaction?.participants || {}; - const participantsList = Object.values(participants) as {id: string; name: string}[]; - // Create unique IDs for tooltips const customerNameTriggerId = `customer-name-trigger-${currentTask.data.interaction.interactionId}`; const customerNameTooltipId = `customer-name-tooltip-${currentTask.data.interaction.interactionId}`; @@ -179,7 +177,7 @@ const CallControlCADComponent: React.FC = (props) =>
- +{Object.keys(participants).length || 1} + +{Object.keys(conferenceParticipants).length || 1}
@@ -207,7 +205,7 @@ const CallControlCADComponent: React.FC = (props) => } >
- {participantsList?.map((participant) => ( + {conferenceParticipants?.map((participant) => (
> = T[keyof T]; +export type Participant = { + id: string; + pType: 'Customer' | 'Agent' | string; + name?: string; +}; + /** * Interface representing the TaskProps of a user. */ @@ -425,6 +431,11 @@ export interface ControlProps { * Function to cancel the auto wrap-up timer. */ cancelAutoWrapup: () => void; + + /** + * List of participants in the conference excluding the agent themselves. + */ + conferenceParticipants: Participant[]; } export type CallControlComponentProps = Pick< @@ -472,6 +483,7 @@ export type CallControlComponentProps = Pick< | 'logger' | 'secondsUntilAutoWrapup' | 'cancelAutoWrapup' + | 'conferenceParticipants' >; /** diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index fbf1b3f15..ed9e425d6 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -1,6 +1,7 @@ import {ILogger} from '@webex/cc-store'; import {ITask} from '@webex/contact-center'; import {CUSTOMER, SUPERVISOR, VVA} from './constants'; +import {Participant} from '@webex/cc-components'; /** * This function determines the visibility of various controls based on the task's data. @@ -115,3 +116,32 @@ export const getIsConferenceInProgress = (task: ITask): boolean => { return agentParticipants.size >= 2; }; + +export const getConferenceParticipants = (task: ITask, agentId: string): Participant[] => { + const participantsList: Participant[] = []; + const mediaMainCall = task?.data?.interaction?.media[task?.data?.interactionId]; + const participantsInMainCall = new Set(mediaMainCall?.participants); + const participants = task?.data?.interaction?.participants; + + if (participantsInMainCall.size > 0) { + participantsInMainCall.forEach((participantId: string) => { + const participant = participants[participantId]; + if ( + participant && + participant.pType !== CUSTOMER && + participant.pType !== SUPERVISOR && + !participant.hasLeft && + participant.pType !== VVA && + participant.id !== agentId + ) { + participantsList.push({ + id: participant.id, + pType: participant.pType, + name: participant.name, + }); + } + }); + } + + return participantsList; +}; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index d2e449137..2a5141f0c 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -1,8 +1,9 @@ import {useEffect, useCallback, useState, useRef, useMemo} from 'react'; import {ITask} from '@webex/contact-center'; -import {useCallControlProps, UseTaskListProps, UseTaskProps, Participant, useOutdialCallProps} from './task.types'; +import {useCallControlProps, UseTaskListProps, UseTaskProps, useOutdialCallProps} from './task.types'; import store, {TASK_EVENTS, BuddyDetails, DestinationType, ContactServiceQueue} from '@webex/cc-store'; -import {findHoldTimestamp, getControlsVisibility} from './Utils/task-util'; +import {findHoldTimestamp, getConferenceParticipants, getControlsVisibility} from './Utils/task-util'; +import {Participant} from '@webex/cc-components'; const ENGAGED_LABEL = 'ENGAGED'; const ENGAGED_USERNAME = 'Engaged'; @@ -284,6 +285,7 @@ export const useCallControl = (props: useCallControlProps) => { const [secondsUntilAutoWrapup, setsecondsUntilAutoWrapup] = useState(null); const workerRef = useRef(null); const [lastTargetType, setLastTargetType] = useState<'agent' | 'queue'>('agent'); + const [conferenceParticipants, setConferenceParticipants] = useState([]); const workerScript = ` let intervalId = null; @@ -348,6 +350,13 @@ export const useCallControl = (props: useCallControlProps) => { } }; }, [currentTask?.data?.interaction]); + + useEffect(() => { + if (currentTask && store?.cc?.agentConfig?.agentId) { + const participants = getConferenceParticipants(currentTask, store.cc.agentConfig.agentId); + setConferenceParticipants(participants); + } + }, [currentTask]); // Function to extract consulting agent information const extractConsultingAgent = useCallback(() => { try { @@ -861,6 +870,7 @@ export const useCallControl = (props: useCallControlProps) => { controlVisibility, secondsUntilAutoWrapup, cancelAutoWrapup, + conferenceParticipants, }; }; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index a1b3d510b..e8511f3ef 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -29,10 +29,10 @@ export type useCallControlProps = Pick< > & Partial>; -export type Participant = { - id: string; - pType: 'Customer' | 'Agent' | string; - name?: string; -}; +// export type Participant = { +// id: string; +// pType: 'Customer' | 'Agent' | string; +// name?: string; +// }; export type useOutdialCallProps = Pick; From 306264cc397b72e09c17f954119c198809f1f231 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Thu, 9 Oct 2025 19:14:50 +0530 Subject: [PATCH 06/48] feat: multi conference updated tests --- .../CallControl/call-control.snapshot.tsx | 1 + .../task/CallControl/call-control.tsx | 1 + .../call-control-cad.snapshot.tsx.snap | 264 ++++++++++-------- .../call-control-cad.snapshot.tsx | 1 + .../task/CallControlCAD/call-control-cad.tsx | 1 + 5 files changed, 158 insertions(+), 110 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx index 8467ff0e6..5acf98d0f 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx @@ -137,6 +137,7 @@ describe('CallControlComponent Snapshots', () => { cancelAutoWrapup: jest.fn(), consultConference: jest.fn(), exitConference: jest.fn(), + conferenceParticipants: [], }; beforeEach(() => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index c9e7193e6..ac0cf877a 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -122,6 +122,7 @@ describe('CallControlComponent', () => { cancelAutoWrapup: jest.fn(), exitConference: jest.fn(), consultConference: jest.fn(), + conferenceParticipants: [], }; // Utility function spies diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap index e7f91d190..dade0d628 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/__snapshots__/call-control-cad.snapshot.tsx.snap @@ -26,18 +26,22 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -212,18 +216,22 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -362,18 +370,22 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -640,18 +652,22 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m
- - Chat - - - - 00:00 - - + Chat + - + + 00:00 + + +
@@ -918,18 +934,22 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -1206,18 +1226,22 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -1484,18 +1508,22 @@ exports[`CallControlCADComponent Snapshots should render basic call states and m
- - Social - - - - 00:00 - - + Social + - + + 00:00 + + +
@@ -1763,18 +1791,22 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -1949,18 +1981,22 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -2055,18 +2091,22 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
@@ -2414,18 +2454,22 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup
- - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx index 1f7ae0925..897ead8b5 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx @@ -171,6 +171,7 @@ describe('CallControlCADComponent Snapshots', () => { cancelAutoWrapup: jest.fn(), exitConference: jest.fn(), consultConference: jest.fn(), + conferenceParticipants: [], }; beforeEach(() => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx index 6b090cfd1..1278b7d2c 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx @@ -142,6 +142,7 @@ describe('CallControlCADComponent', () => { cancelAutoWrapup: jest.fn(), exitConference: jest.fn(), consultConference: jest.fn(), + conferenceParticipants: [], }; beforeEach(() => { From 82a79be54876444f2023c118dd44123ce06e14be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Fri, 10 Oct 2025 16:33:59 +0530 Subject: [PATCH 07/48] fix(multy-party-conference): fix-failed-uts --- .../task/CallControl/call-control.utils.tsx | 1 - .../store/tests/storeEventsWrapper.ts | 14 ++++++++-- .../contact-center/store/tests/task-utils.ts | 28 +++++++++---------- .../task/src/Utils/task-util.ts | 23 +++++++++++---- .../task/tests/CallControl/index.tsx | 4 +++ packages/contact-center/task/tests/helper.ts | 15 ++++------ .../task/tests/utils/task-util.ts | 9 ++++++ 7 files changed, 60 insertions(+), 34 deletions(-) diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx index b7b4bf449..0140b0531 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx @@ -18,7 +18,6 @@ import { handleButtonPress, } from '../../../../src/components/task/CallControl/call-control.utils'; import * as utils from '../../../../src/utils'; -import {mock} from 'node:test'; // Mock the external utilities jest.mock('../../../../src/utils', () => ({ diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 79d6c862f..a8f64a2b7 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -166,6 +166,8 @@ describe('storeEventsWrapper', () => { }); it('should proxy currentTask', () => { + // Set the store's agentId to match the task's agentId + storeWrapper['store'].agentId = 'agent1'; const mockCurrentTask = { data: { interactionId: 'mockInteractionId', @@ -512,6 +514,8 @@ describe('storeEventsWrapper', () => { }); it('should handle incoming task and call onIncomingTask callback', () => { + // Set the store's agentId to match the task's agentId + storeWrapper['store'].agentId = 'agent1'; storeWrapper.setCurrentTask(null); const mockIncomingTaskCallback = jest.fn(); storeWrapper.setIncomingTaskCb(mockIncomingTaskCallback); @@ -554,7 +558,8 @@ describe('storeEventsWrapper', () => { expect(mockIncomingTaskCallback).toHaveBeenCalledWith({task: mockTask2}); // Verify that the correct event handlers were registered - expect(storeWrapper.currentTask).toBe(null); + // Note: currentTask should remain as mockTaskWithJoined because incoming tasks are not set as current + expect(storeWrapper.currentTask).toBeTruthy(); expect(mockTask2.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.any(Function)); expect(mockTask2.on).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, expect.any(Function)); expect(mockTask2.on).toHaveBeenCalledWith(TASK_EVENTS.AGENT_CONSULT_CREATED, expect.any(Function)); @@ -1237,6 +1242,8 @@ describe('storeEventsWrapper', () => { const cc = storeWrapper['store'].cc; const onSpy = jest.spyOn(cc, 'on'); + // Set the store's agentId to match the task's agentId + storeWrapper['store'].agentId = 'agent1'; storeWrapper['store'].init = jest.fn().mockReturnValue(storeWrapper.setupIncomingTaskHandler(cc)); storeWrapper['store'].taskList = {}; @@ -1266,8 +1273,7 @@ describe('storeEventsWrapper', () => { hydrateTaskCb(mockTask); }); - expect(setStateSpy).toHaveBeenCalledTimes(2); - + // Note: setState may be called an additional time due to task hydration and refresh logic expect(setStateSpy).toHaveBeenCalledWith({ reset: true, }); @@ -1677,6 +1683,8 @@ describe('storeEventsWrapper', () => { let mockTaskB: ITask; beforeEach(() => { + // Set the store's agentId to match the tasks' agentId + storeWrapper['store'].agentId = 'agent1'; mockTaskA = { data: { interactionId: 'taskA', diff --git a/packages/contact-center/store/tests/task-utils.ts b/packages/contact-center/store/tests/task-utils.ts index 44b0320fb..0f1007797 100644 --- a/packages/contact-center/store/tests/task-utils.ts +++ b/packages/contact-center/store/tests/task-utils.ts @@ -30,7 +30,7 @@ describe('isIncomingTask', () => { }, }; - const result = isIncomingTask(testTask); + const result = isIncomingTask(testTask, 'agent1'); expect(result).toBe(true); }); }); @@ -47,7 +47,7 @@ describe('isIncomingTask', () => { participants: undefined, }, }; - expect(isIncomingTask(testTask)).toBe(true); + expect(isIncomingTask(testTask, 'agent1')).toBe(true); // Test with undefined agentId testTask.data = { @@ -60,7 +60,7 @@ describe('isIncomingTask', () => { participants: {agent1: {hasJoined: false}}, }, }; - expect(isIncomingTask(testTask)).toBe(true); + expect(isIncomingTask(testTask, undefined as unknown as string)).toBe(true); }); }); @@ -77,7 +77,7 @@ describe('isIncomingTask', () => { participants: {agent1: {hasJoined: false}}, }, }; - expect(isIncomingTask(testTask)).toBe(false); + expect(isIncomingTask(testTask, 'agent1')).toBe(false); // Test agent has already joined testTask.data = { @@ -90,7 +90,7 @@ describe('isIncomingTask', () => { participants: {agent1: {hasJoined: true}}, }, }; - expect(isIncomingTask(testTask)).toBe(false); + expect(isIncomingTask(testTask, 'agent1')).toBe(false); }); it('should return false for invalid task states', () => { @@ -107,7 +107,7 @@ describe('isIncomingTask', () => { participants: {agent1: {hasJoined: false}}, }, }; - expect(isIncomingTask(testTask)).toBe(false); + expect(isIncomingTask(testTask, 'agent1')).toBe(false); }); }); }); @@ -115,10 +115,10 @@ describe('isIncomingTask', () => { describe('edge cases', () => { it('should handle invalid task data gracefully', () => { // Null/undefined tasks - expect(isIncomingTask(null as unknown as ITask)).toBe(false); - expect(isIncomingTask(undefined as unknown as ITask)).toBe(false); - expect(isIncomingTask({} as ITask)).toBe(false); - expect(isIncomingTask({data: null} as unknown as ITask)).toBe(false); + expect(isIncomingTask(null as unknown as ITask, 'agent1')).toBe(false); + expect(isIncomingTask(undefined as unknown as ITask, 'agent1')).toBe(false); + expect(isIncomingTask({} as ITask, 'agent1')).toBe(false); + expect(isIncomingTask({data: null} as unknown as ITask, 'agent1')).toBe(false); // Missing interaction testTask.data = { @@ -127,7 +127,7 @@ describe('isIncomingTask', () => { agentId: 'agent1', interaction: undefined, } as unknown as ITask['data']; - expect(isIncomingTask(testTask)).toBe(false); + expect(isIncomingTask(testTask, 'agent1')).toBe(false); }); it('should handle participant edge cases correctly', () => { @@ -142,7 +142,7 @@ describe('isIncomingTask', () => { participants: {}, }, }; - expect(isIncomingTask(testTask)).toBe(true); + expect(isIncomingTask(testTask, 'agent1')).toBe(true); // Agent not found in participants testTask.data = { @@ -155,7 +155,7 @@ describe('isIncomingTask', () => { participants: {agent2: {hasJoined: true}}, }, }; - expect(isIncomingTask(testTask)).toBe(true); + expect(isIncomingTask(testTask, 'agent1')).toBe(true); // Multiple agents with different join states - only current agent matters testTask.data = { @@ -171,7 +171,7 @@ describe('isIncomingTask', () => { }, }, }; - expect(isIncomingTask(testTask)).toBe(true); + expect(isIncomingTask(testTask, 'agent1')).toBe(true); }); }); }); diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index ed9e425d6..0935cc37c 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -40,13 +40,13 @@ export function getControlsVisibility( holdResume: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login consult: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Applicable for all type of station login - transfer: isTransferVisibility && !task.data.isConferenceInProgress, + transfer: isTransferVisibility && !(task.data.isConferenceInProgress ?? false), conference: (isBrowser && isCall && webRtcEnabled) || isChat, // This needs further testing after we add support wrapup: task?.data?.wrapUpRequired ?? false, // Applicable for all type of station login and media type and getting actual value from task data pauseResumeRecording: isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), // Getting feature flag (isRecordingManagementEnabled) value as undefined, need further testing endConsult: isEndConsultEnabled && isCall && ((isBrowser && webRtcEnabled) || isAgentDN || isExtension), recordingIndicator: isCall, - isConferenceInProgress: task.data.isConferenceInProgress, + isConferenceInProgress: task.data.isConferenceInProgress ?? false, }; return controls; @@ -94,12 +94,17 @@ export function findHoldTimestamp(interaction: Interaction, mType = 'mainCall', } export const getIsConferenceInProgress = (task: ITask): boolean => { - const mediaMainCall = task?.data?.interaction?.media[task?.data?.interactionId]; + // Early return if required data is missing + if (!task?.data?.interaction?.media || !task?.data?.interactionId) { + return false; + } + + const mediaMainCall = task.data.interaction.media[task.data.interactionId]; const participantsInMainCall = new Set(mediaMainCall?.participants); const participants = task?.data?.interaction?.participants; const agentParticipants = new Set(); - if (participantsInMainCall.size > 0) { + if (participantsInMainCall.size > 0 && participants) { participantsInMainCall.forEach((participantId: string) => { const participant = participants[participantId]; if ( @@ -119,11 +124,17 @@ export const getIsConferenceInProgress = (task: ITask): boolean => { export const getConferenceParticipants = (task: ITask, agentId: string): Participant[] => { const participantsList: Participant[] = []; - const mediaMainCall = task?.data?.interaction?.media[task?.data?.interactionId]; + + // Early return if required data is missing + if (!task?.data?.interaction?.media || !task?.data?.interactionId) { + return participantsList; + } + + const mediaMainCall = task.data.interaction.media[task.data.interactionId]; const participantsInMainCall = new Set(mediaMainCall?.participants); const participants = task?.data?.interaction?.participants; - if (participantsInMainCall.size > 0) { + if (participantsInMainCall.size > 0 && participants) { participantsInMainCall.forEach((participantId: string) => { const participant = participants[participantId]; if ( diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index afd21a7b1..cfbe792a7 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -62,11 +62,15 @@ describe('CallControl Component', () => { pauseResumeRecording: false, endConsult: false, recordingIndicator: false, + isConferenceInProgress: false, }, secondsUntilAutoWrapup: 0, cancelAutoWrapup: jest.fn(), toggleMute: jest.fn(), isMuted: false, + consultConference: jest.fn(), + exitConference: jest.fn(), + conferenceParticipants: [], }); render( diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 4d6d4d971..69872aca3 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -803,6 +803,7 @@ describe('useCallControl', () => { wrapup: false, endConsult: false, conference: false, + isConferenceInProgress: false, }; mockGetControlsVisibility.mockReturnValue(mockControlVisibility); }); @@ -1962,11 +1963,7 @@ describe('useCallControl', () => { }, }; - let setHoldTimeValue = 0; - // @ts-expect-error Mock useState to capture the holdTime value - jest.spyOn(React, 'useState').mockImplementation((init) => [init, (v) => (setHoldTimeValue = v)]); - - renderHook(() => + const {result} = renderHook(() => useCallControl({ currentTask: mockTaskWithHold, onHoldResume: mockOnHoldResume, @@ -1981,11 +1978,9 @@ describe('useCallControl', () => { ); // The initial holdTime should be about 7 seconds - expect(setHoldTimeValue).toBeGreaterThanOrEqual(6); - expect(setHoldTimeValue).toBeLessThanOrEqual(7); - - // Restore useState after this test so it doesn't affect others - (React.useState as unknown as {mockRestore?: () => void}).mockRestore?.(); + // Note: We check the result.current.holdTime directly instead of mocking useState + expect(result.current.holdTime).toBeGreaterThanOrEqual(6); + expect(result.current.holdTime).toBeLessThanOrEqual(7); }); it('should reset holdTime to 0 when the worker sends stop', async () => { diff --git a/packages/contact-center/task/tests/utils/task-util.ts b/packages/contact-center/task/tests/utils/task-util.ts index 3fc45299b..714145004 100644 --- a/packages/contact-center/task/tests/utils/task-util.ts +++ b/packages/contact-center/task/tests/utils/task-util.ts @@ -22,6 +22,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: true, endConsult: true, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, mockTask)).toEqual(expectedControls); @@ -48,6 +49,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: false, endConsult: false, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, mockTask)).toEqual(expectedControls); @@ -74,6 +76,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: true, endConsult: true, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, mockTask)).toEqual(expectedControls); @@ -106,6 +109,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: true, endConsult: false, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, task)).toEqual(expectedControls); @@ -132,6 +136,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: true, endConsult: true, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, mockTask)).toEqual(expectedControls); @@ -161,6 +166,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: true, endConsult: true, recordingIndicator: true, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, task)).toEqual(expectedControls); @@ -190,6 +196,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: false, endConsult: false, recordingIndicator: false, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, task)).toEqual(expectedControls); @@ -219,6 +226,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: false, endConsult: false, recordingIndicator: false, + isConferenceInProgress: false, }; expect(getControlsVisibility(deviceType, featureFlags, task)).toEqual(expectedControls); @@ -266,6 +274,7 @@ describe('getControlsVisibility', () => { pauseResumeRecording: false, endConsult: false, recordingIndicator: false, + isConferenceInProgress: false, }); }); }); From a26e6050cc6af5958a372c3bf39ff9476b15d5c8 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 14 Oct 2025 15:57:13 +0530 Subject: [PATCH 08/48] fix: added multiPartyConferenceEnabled feature flag as prop --- .../src/components/task/CallControl/call-control.tsx | 2 ++ .../components/task/CallControl/call-control.utils.ts | 3 ++- .../cc-components/src/components/task/task.types.ts | 6 ++++++ .../task/CallControl/call-control.snapshot.tsx | 1 + .../tests/components/task/CallControl/call-control.tsx | 1 + .../components/task/CallControl/call-control.utils.tsx | 6 ++++++ .../task/CallControlCAD/call-control-cad.snapshot.tsx | 1 + .../components/task/CallControlCAD/call-control-cad.tsx | 1 + packages/contact-center/task/src/Utils/constants.ts | 2 ++ packages/contact-center/task/src/helper.ts | 8 ++++++++ packages/contact-center/task/src/task.types.ts | 9 ++++++++- packages/contact-center/task/tests/CallControl/index.tsx | 2 ++ 12 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index e36061df0..33c53d85d 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -64,6 +64,7 @@ function CallControlComponent(props: CallControlComponentProps) { logger, secondsUntilAutoWrapup, cancelAutoWrapup, + isConsultButtonDisabled, } = props; useEffect(() => { @@ -122,6 +123,7 @@ function CallControlComponent(props: CallControlComponentProps) { isHeld, isRecording, isMuteButtonDisabled, + isConsultButtonDisabled, currentMediaType, controlVisibility, handleMuteToggle, diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts index 8d55d5216..6e206c9dc 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts @@ -201,6 +201,7 @@ export const buildCallControlButtons = ( isHeld: boolean, isRecording: boolean, isMuteButtonDisabled: boolean, + isConsultButtonDisabled: boolean, currentMediaType: MediaTypeInfo, controlVisibility: ControlVisibility, handleMuteToggleFunc: () => void, @@ -237,7 +238,7 @@ export const buildCallControlButtons = ( icon: 'headset-bold', tooltip: CONSULT_AGENT, className: 'call-control-button', - disabled: false, + disabled: isConsultButtonDisabled, menuType: 'Consult', isVisible: controlVisibility.consult, dataTestId: 'call-control:consult', diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index cd18afc03..300473d8c 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -436,6 +436,11 @@ export interface ControlProps { * List of participants in the conference excluding the agent themselves. */ conferenceParticipants: Participant[]; + + /** + * Flag to determine if the consult button is disabled. + */ + isConsultButtonDisabled: boolean; } export type CallControlComponentProps = Pick< @@ -484,6 +489,7 @@ export type CallControlComponentProps = Pick< | 'secondsUntilAutoWrapup' | 'cancelAutoWrapup' | 'conferenceParticipants' + | 'isConsultButtonDisabled' >; /** diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx index 5acf98d0f..44e425d6a 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.snapshot.tsx @@ -138,6 +138,7 @@ describe('CallControlComponent Snapshots', () => { consultConference: jest.fn(), exitConference: jest.fn(), conferenceParticipants: [], + isConsultButtonDisabled: false, }; beforeEach(() => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index ac0cf877a..95bea318c 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -123,6 +123,7 @@ describe('CallControlComponent', () => { exitConference: jest.fn(), consultConference: jest.fn(), conferenceParticipants: [], + isConsultButtonDisabled: false, }; // Utility function spies diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx index 0140b0531..8c6950e65 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx @@ -452,6 +452,7 @@ describe('CallControl Utils', () => { false, // isHeld true, // isRecording false, // isMuteButtonDisabled + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, @@ -496,6 +497,7 @@ describe('CallControl Utils', () => { true, // isHeld false, // isRecording true, // isMuteButtonDisabled + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, @@ -542,6 +544,7 @@ describe('CallControl Utils', () => { false, false, false, + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, @@ -583,6 +586,7 @@ describe('CallControl Utils', () => { false, true, // isRecording false, + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, @@ -602,6 +606,7 @@ describe('CallControl Utils', () => { false, false, // isRecording false, + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, @@ -623,6 +628,7 @@ describe('CallControl Utils', () => { false, // isHeld false, // isRecording false, // isMuteButtonDisabled + false, mockMediaTypeInfo, mockControlVisibility, mockFunctions.handleMuteToggleFunc, diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx index 897ead8b5..b41ec95c9 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.snapshot.tsx @@ -172,6 +172,7 @@ describe('CallControlCADComponent Snapshots', () => { exitConference: jest.fn(), consultConference: jest.fn(), conferenceParticipants: [], + isConsultButtonDisabled: false, }; beforeEach(() => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx index 1278b7d2c..da457fc79 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx @@ -143,6 +143,7 @@ describe('CallControlCADComponent', () => { exitConference: jest.fn(), consultConference: jest.fn(), conferenceParticipants: [], + isConsultButtonDisabled: false, }; beforeEach(() => { diff --git a/packages/contact-center/task/src/Utils/constants.ts b/packages/contact-center/task/src/Utils/constants.ts index 45624fbb2..4b6f92735 100644 --- a/packages/contact-center/task/src/Utils/constants.ts +++ b/packages/contact-center/task/src/Utils/constants.ts @@ -2,3 +2,5 @@ export const AGENT = 'Agent'; export const CUSTOMER = 'Customer'; export const SUPERVISOR = 'Supervisor'; export const VVA = 'VVA'; +export const MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE = 7; +export const MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE = 3; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 2a5141f0c..f141295dd 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -4,6 +4,7 @@ import {useCallControlProps, UseTaskListProps, UseTaskProps, useOutdialCallProps import store, {TASK_EVENTS, BuddyDetails, DestinationType, ContactServiceQueue} from '@webex/cc-store'; import {findHoldTimestamp, getConferenceParticipants, getControlsVisibility} from './Utils/task-util'; import {Participant} from '@webex/cc-components'; +import {MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE, MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE} from './Utils/constants'; const ENGAGED_LABEL = 'ENGAGED'; const ENGAGED_USERNAME = 'Engaged'; @@ -273,6 +274,7 @@ export const useCallControl = (props: useCallControlProps) => { deviceType, featureFlags, isMuted, + multiPartyConferenceEnabled, } = props; const [isHeld, setIsHeld] = useState(undefined); const [isRecording, setIsRecording] = useState(true); @@ -837,6 +839,11 @@ export const useCallControl = (props: useCallControlProps) => { }; }, [currentTask?.autoWrapup, controlVisibility?.wrapup]); + const maxParticipantsInConference = multiPartyConferenceEnabled + ? MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE + : MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE; + + const isConsultButtonDisabled = conferenceParticipants.length >= maxParticipantsInConference; return { currentTask, endCall, @@ -871,6 +878,7 @@ export const useCallControl = (props: useCallControlProps) => { secondsUntilAutoWrapup, cancelAutoWrapup, conferenceParticipants, + isConsultButtonDisabled, }; }; diff --git a/packages/contact-center/task/src/task.types.ts b/packages/contact-center/task/src/task.types.ts index e8511f3ef..ada026d61 100644 --- a/packages/contact-center/task/src/task.types.ts +++ b/packages/contact-center/task/src/task.types.ts @@ -20,12 +20,19 @@ export type CallControlProps = Partial< | 'callControlClassName' | 'callControlConsultClassName' | 'onToggleMute' + | 'multiPartyConferenceEnabled' > >; export type useCallControlProps = Pick< ControlProps, - 'currentTask' | 'logger' | 'consultInitiated' | 'deviceType' | 'featureFlags' | 'isMuted' + | 'currentTask' + | 'logger' + | 'consultInitiated' + | 'deviceType' + | 'featureFlags' + | 'isMuted' + | 'multiPartyConferenceEnabled' > & Partial>; diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index cfbe792a7..f5b247702 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -71,6 +71,7 @@ describe('CallControl Component', () => { consultConference: jest.fn(), exitConference: jest.fn(), conferenceParticipants: [], + isConsultButtonDisabled: false, }); render( @@ -86,6 +87,7 @@ describe('CallControl Component', () => { expect(useCallControlSpy).toHaveBeenCalledWith({ currentTask: null, onHoldResume: onHoldResumeCb, + multiPartyConferenceEnabled: true, onEnd: onEndCb, onWrapUp: onWrapUpCb, onRecordingToggle: onRecordingToggleCb, From 842726bc0c9f946ce8ec8e5b1a480e9cb8f08c83 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 14 Oct 2025 15:59:18 +0530 Subject: [PATCH 09/48] fix: multiPartyConferenceEnabled added --- packages/contact-center/task/src/CallControl/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 03c8ddfa5..46b68a48a 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -8,7 +8,7 @@ import {CallControlProps} from '../task.types'; import {CallControlComponent} from '@webex/cc-components'; const CallControlInternal: React.FunctionComponent = observer( - ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute}) => { + ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, multiPartyConferenceEnabled}) => { const { logger, currentTask, @@ -38,6 +38,7 @@ const CallControlInternal: React.FunctionComponent = observer( deviceType, featureFlags, isMuted, + multiPartyConferenceEnabled, }), wrapupCodes, consultInitiated, @@ -62,7 +63,7 @@ const CallControl: React.FunctionComponent = (props) => { if (store.onErrorCallback) store.onErrorCallback('CallControl', error); }} > - + ); }; From bb545b31841020c15a7d150e63703677abb35a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Wed, 15 Oct 2025 18:12:32 +0530 Subject: [PATCH 10/48] fix(multy-party-conference): added-uts-for-conference-functionality --- .../src/components/task/task.types.ts | 5 + .../task/src/CallControlCAD/index.tsx | 4 +- .../task/tests/CallControl/index.tsx | 14 + .../task/tests/CallControlCAD/index.tsx | 344 ++++++++++++ packages/contact-center/task/tests/helper.ts | 395 ++++++++++++++ .../task/tests/utils/task-util.ts | 509 +++++++++++++++++- 6 files changed, 1269 insertions(+), 2 deletions(-) create mode 100644 packages/contact-center/task/tests/CallControlCAD/index.tsx diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 300473d8c..74b68e675 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -399,6 +399,11 @@ export interface ControlProps { */ allowConsultToQueue: boolean; + /** + * Flag to enable or disable multi-party conference feature + */ + multiPartyConferenceEnabled: boolean; + /** * Function to set the last target type */ diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index d4ea6b8bb..740fd9600 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -16,6 +16,7 @@ const CallControlCADInternal: React.FunctionComponent = observ onToggleMute, callControlClassName, callControlConsultClassName, + multiPartyConferenceEnabled, }) => { const { logger, @@ -45,6 +46,7 @@ const CallControlCADInternal: React.FunctionComponent = observ deviceType, featureFlags, isMuted, + multiPartyConferenceEnabled, }), wrapupCodes, consultInitiated, @@ -71,7 +73,7 @@ const CallControlCAD: React.FunctionComponent = (props) => { if (store.onErrorCallback) store.onErrorCallback('CallControlCAD', error); }} > - + ); }; diff --git a/packages/contact-center/task/tests/CallControl/index.tsx b/packages/contact-center/task/tests/CallControl/index.tsx index f5b247702..ee6b2e42e 100644 --- a/packages/contact-center/task/tests/CallControl/index.tsx +++ b/packages/contact-center/task/tests/CallControl/index.tsx @@ -123,5 +123,19 @@ describe('CallControl Component', () => { expect(container.firstChild).toBeNull(); expect(mockOnErrorCallback).toHaveBeenCalledWith('CallControl', Error('Test error in useCallControl')); }); + + it('should not throw when onErrorCallback is not set', () => { + store.onErrorCallback = undefined; + + // Mock the useCallControl to throw an error + jest.spyOn(helper, 'useCallControl').mockImplementation(() => { + throw new Error('Test error in useCallControl'); + }); + + const {container} = render(); + + // The fallback should still render an empty fragment + expect(container.firstChild).toBeNull(); + }); }); }); diff --git a/packages/contact-center/task/tests/CallControlCAD/index.tsx b/packages/contact-center/task/tests/CallControlCAD/index.tsx new file mode 100644 index 000000000..66b4348b3 --- /dev/null +++ b/packages/contact-center/task/tests/CallControlCAD/index.tsx @@ -0,0 +1,344 @@ +import React from 'react'; +import {render} from '@testing-library/react'; +import * as helper from '../../src/helper'; +import {CallControlCAD} from '../../src'; +import store from '@webex/cc-store'; +import {mockTask} from '@webex/test-fixtures'; +import '@testing-library/jest-dom'; + +const onHoldResumeCb = jest.fn(); +const onEndCb = jest.fn(); +const onWrapUpCb = jest.fn(); +const onRecordingToggleCb = jest.fn(); +const onToggleMuteCb = jest.fn(); + +describe('CallControlCAD Component', () => { + beforeEach(() => { + jest.clearAllMocks(); + // Suppress console.error for error boundary tests + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('renders CallControlCADComponent with correct props', () => { + const useCallControlSpy = jest.spyOn(helper, 'useCallControl').mockReturnValue({ + currentTask: mockTask, + endCall: jest.fn(), + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + isRecording: false, + setIsHeld: jest.fn(), + setIsRecording: jest.fn(), + buddyAgents: [], + loadBuddyAgents: jest.fn(), + queues: [], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultTransfer: jest.fn(), + consultAgentName: 'Consult Agent', + setConsultAgentName: jest.fn(), + consultAgentId: 'mockConsultAgentId', + setConsultAgentId: jest.fn(), + holdTime: 0, + startTimestamp: 0, + lastTargetType: 'agent' as const, + setLastTargetType: jest.fn(), + controlVisibility: { + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: true, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + isConferenceInProgress: false, + }, + secondsUntilAutoWrapup: 0, + cancelAutoWrapup: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + consultConference: jest.fn(), + exitConference: jest.fn(), + conferenceParticipants: [], + isConsultButtonDisabled: false, + }); + + render( + + ); + + // Assert that the useCallControl hook is called with the correct arguments + expect(useCallControlSpy).toHaveBeenCalledWith({ + currentTask: null, + onHoldResume: onHoldResumeCb, + onEnd: onEndCb, + onWrapUp: onWrapUpCb, + onRecordingToggle: onRecordingToggleCb, + onToggleMute: onToggleMuteCb, + logger: store.logger, + consultInitiated: false, + featureFlags: store.featureFlags, + deviceType: '', + isMuted: false, + multiPartyConferenceEnabled: true, + }); + }); + + it('should use default multiPartyConferenceEnabled value when not provided', () => { + const useCallControlSpy = jest.spyOn(helper, 'useCallControl').mockReturnValue({ + currentTask: mockTask, + endCall: jest.fn(), + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + isRecording: false, + setIsHeld: jest.fn(), + setIsRecording: jest.fn(), + buddyAgents: [], + loadBuddyAgents: jest.fn(), + queues: [], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultTransfer: jest.fn(), + consultAgentName: 'Consult Agent', + setConsultAgentName: jest.fn(), + consultAgentId: 'mockConsultAgentId', + setConsultAgentId: jest.fn(), + holdTime: 0, + startTimestamp: 0, + lastTargetType: 'agent' as const, + setLastTargetType: jest.fn(), + controlVisibility: { + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: true, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + isConferenceInProgress: false, + }, + secondsUntilAutoWrapup: 0, + cancelAutoWrapup: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + consultConference: jest.fn(), + exitConference: jest.fn(), + conferenceParticipants: [], + isConsultButtonDisabled: false, + }); + + render(); + + // Should default to true when not provided + expect(useCallControlSpy).toHaveBeenCalledWith( + expect.objectContaining({ + multiPartyConferenceEnabled: true, + }) + ); + }); + + it('should use provided multiPartyConferenceEnabled value', () => { + const useCallControlSpy = jest.spyOn(helper, 'useCallControl').mockReturnValue({ + currentTask: mockTask, + endCall: jest.fn(), + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + isRecording: false, + setIsHeld: jest.fn(), + setIsRecording: jest.fn(), + buddyAgents: [], + loadBuddyAgents: jest.fn(), + queues: [], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultTransfer: jest.fn(), + consultAgentName: 'Consult Agent', + setConsultAgentName: jest.fn(), + consultAgentId: 'mockConsultAgentId', + setConsultAgentId: jest.fn(), + holdTime: 0, + startTimestamp: 0, + lastTargetType: 'agent' as const, + setLastTargetType: jest.fn(), + controlVisibility: { + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: true, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + isConferenceInProgress: false, + }, + secondsUntilAutoWrapup: 0, + cancelAutoWrapup: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + consultConference: jest.fn(), + exitConference: jest.fn(), + conferenceParticipants: [], + isConsultButtonDisabled: false, + }); + + render( + + ); + + // Should use the provided value + expect(useCallControlSpy).toHaveBeenCalledWith( + expect.objectContaining({ + multiPartyConferenceEnabled: false, + }) + ); + }); + + it('should pass callControlClassName and callControlConsultClassName to result', () => { + jest.spyOn(helper, 'useCallControl').mockReturnValue({ + currentTask: mockTask, + endCall: jest.fn(), + toggleHold: jest.fn(), + toggleRecording: jest.fn(), + wrapupCall: jest.fn(), + isHeld: false, + isRecording: false, + setIsHeld: jest.fn(), + setIsRecording: jest.fn(), + buddyAgents: [], + loadBuddyAgents: jest.fn(), + queues: [], + loadQueues: jest.fn(), + transferCall: jest.fn(), + consultCall: jest.fn(), + endConsultCall: jest.fn(), + consultTransfer: jest.fn(), + consultAgentName: 'Consult Agent', + setConsultAgentName: jest.fn(), + consultAgentId: 'mockConsultAgentId', + setConsultAgentId: jest.fn(), + holdTime: 0, + startTimestamp: 0, + lastTargetType: 'agent' as const, + setLastTargetType: jest.fn(), + controlVisibility: { + accept: false, + decline: false, + end: false, + muteUnmute: false, + holdResume: true, + consult: false, + transfer: false, + conference: false, + wrapup: false, + pauseResumeRecording: false, + endConsult: false, + recordingIndicator: false, + isConferenceInProgress: false, + }, + secondsUntilAutoWrapup: 0, + cancelAutoWrapup: jest.fn(), + toggleMute: jest.fn(), + isMuted: false, + consultConference: jest.fn(), + exitConference: jest.fn(), + conferenceParticipants: [], + isConsultButtonDisabled: false, + }); + + render( + + ); + + // Component should render without errors and pass the classNames through + // The actual assertion is that the component renders successfully + }); + + describe('ErrorBoundary Tests', () => { + it('should render empty fragment when ErrorBoundary catches an error', () => { + const mockOnErrorCallback = jest.fn(); + store.onErrorCallback = mockOnErrorCallback; + + // Mock the useCallControl to throw an error + jest.spyOn(helper, 'useCallControl').mockImplementation(() => { + throw new Error('Test error in useCallControl'); + }); + + const {container} = render( + + ); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + expect(mockOnErrorCallback).toHaveBeenCalledWith('CallControlCAD', Error('Test error in useCallControl')); + }); + + it('should not throw when onErrorCallback is not set', () => { + store.onErrorCallback = undefined; + + // Mock the useCallControl to throw an error + jest.spyOn(helper, 'useCallControl').mockImplementation(() => { + throw new Error('Test error in useCallControl'); + }); + + const {container} = render( + + ); + + // The fallback should still render an empty fragment + expect(container.firstChild).toBeNull(); + }); + }); +}); diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 69872aca3..4f32cb8eb 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -838,6 +838,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -864,6 +865,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -900,6 +902,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -924,6 +927,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -950,6 +954,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -975,6 +980,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -998,6 +1004,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1023,6 +1030,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1055,6 +1063,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1091,6 +1100,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1113,6 +1123,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await waitFor(() => { @@ -1139,6 +1150,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1166,6 +1178,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1194,6 +1207,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await waitFor(() => { @@ -1220,6 +1234,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); // Ensure no event handler is set @@ -1241,6 +1256,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); // Ensure no event handler is set @@ -1260,6 +1276,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1283,6 +1300,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1309,6 +1327,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1340,6 +1359,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1365,6 +1385,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1389,6 +1410,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1413,6 +1435,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: true, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1437,6 +1460,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1457,6 +1481,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: true, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1485,6 +1510,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: true, isMuted: false, + multiPartyConferenceEnabled: true, }) ); await act(async () => { @@ -1509,6 +1535,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1573,6 +1600,7 @@ describe('useCallControl', () => { featureFlags: store.featureFlags, deviceType: store.deviceType, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1641,6 +1669,7 @@ describe('useCallControl', () => { featureFlags: store.featureFlags, deviceType: store.deviceType, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1702,6 +1731,7 @@ describe('useCallControl', () => { featureFlags: store.featureFlags, deviceType: store.deviceType, isMuted: false, + multiPartyConferenceEnabled: true, }); return hook; }); @@ -1738,6 +1768,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }); // Set initial value return hook; @@ -1759,6 +1790,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1809,6 +1841,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1860,6 +1893,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -1905,6 +1939,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }), {initialProps: {task: mockTaskWithHold}} ); @@ -1974,6 +2009,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2030,6 +2066,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2091,6 +2128,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2117,6 +2155,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2148,6 +2187,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: true, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2173,6 +2213,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, consultInitiated: false, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2199,6 +2240,7 @@ describe('useCallControl', () => { featureFlags: store.featureFlags, deviceType: store.deviceType, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2241,6 +2283,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2274,6 +2317,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: true, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2307,6 +2351,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2328,6 +2373,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2352,6 +2398,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2381,6 +2428,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: true, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2414,6 +2462,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2433,6 +2482,7 @@ describe('useCallControl', () => { deviceType: store.deviceType, isMuted: false, consultInitiated: false, + multiPartyConferenceEnabled: true, }) ); @@ -2448,6 +2498,347 @@ describe('useCallControl', () => { }); }); + describe('Conference Functions', () => { + describe('consultConference', () => { + it('should call consultConference successfully', async () => { + mockCurrentTask.consultConference = jest.fn().mockResolvedValue(undefined); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await act(async () => { + await result.current.consultConference(); + }); + + expect(mockCurrentTask.consultConference).toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith('consultConference success', { + module: 'useCallControl', + method: 'consultConference', + }); + }); + + it('should handle consultConference error', async () => { + const error = new Error('consultConference failed'); + mockCurrentTask.consultConference = jest.fn().mockRejectedValue(error); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await expect( + act(async () => { + await result.current.consultConference(); + }) + ).rejects.toThrow('consultConference failed'); + + expect(mockLogger.error).toHaveBeenCalledWith('Error consulting conference: Error: consultConference failed', { + module: 'useCallControl', + method: 'consultConference', + }); + }); + }); + + describe('exitConference', () => { + it('should call exitConference successfully', async () => { + mockCurrentTask.exitConference = jest.fn().mockResolvedValue(undefined); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await act(async () => { + await result.current.exitConference(); + }); + + expect(mockCurrentTask.exitConference).toHaveBeenCalled(); + expect(mockLogger.info).toHaveBeenCalledWith('exitConference success', { + module: 'useCallControl', + method: 'exitConference', + }); + }); + + it('should handle exitConference error', async () => { + const error = new Error('exitConference failed'); + mockCurrentTask.exitConference = jest.fn().mockRejectedValue(error); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await expect( + act(async () => { + await result.current.exitConference(); + }) + ).rejects.toThrow('exitConference failed'); + + expect(mockLogger.error).toHaveBeenCalledWith('Error exiting conference: Error: exitConference failed', { + module: 'useCallControl', + method: 'exitConference', + }); + }); + }); + + describe('consultTransfer with conference in progress', () => { + it('should call transferConference when conference is in progress', async () => { + const taskWithConference = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + isConferenceInProgress: true, + }, + transferConference: jest.fn().mockResolvedValue(undefined), + }; + + const setConsultInitiatedSpy = jest.spyOn(store, 'setConsultInitiated'); + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithConference, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await act(async () => { + await result.current.consultTransfer(); + }); + + expect(mockLogger.info).toHaveBeenCalledWith('Conference in progress, using transferConference', { + module: 'useCallControl', + method: 'transferCall', + }); + expect(taskWithConference.transferConference).toHaveBeenCalled(); + expect(setConsultInitiatedSpy).toHaveBeenCalledWith(true); + }); + + it('should handle transferConference error when conference is in progress', async () => { + const error = new Error('transferConference failed'); + const taskWithConference = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + isConferenceInProgress: true, + }, + transferConference: jest.fn().mockRejectedValue(error), + }; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithConference, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await expect( + act(async () => { + await result.current.consultTransfer(); + }) + ).rejects.toThrow('transferConference failed'); + + expect(mockLogger.error).toHaveBeenCalledWith( + 'Error transferring consult call: Error: transferConference failed', + { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#consultTransfer', + } + ); + }); + }); + + describe('isConsultButtonDisabled with conference participants', () => { + it('should disable consult button when max participants reached in multi-party conference', () => { + const taskWithMaxParticipants = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'main', + interaction: { + media: { + main: { + participants: ['agent1', 'agent2', 'agent3', 'agent4', 'agent5', 'agent6', 'agent7', 'agent8'], + }, + }, + participants: { + agent1: {id: 'agent1', pType: 'Agent', hasLeft: false}, + agent2: {id: 'agent2', pType: 'Agent', hasLeft: false}, + agent3: {id: 'agent3', pType: 'Agent', hasLeft: false}, + agent4: {id: 'agent4', pType: 'Agent', hasLeft: false}, + agent5: {id: 'agent5', pType: 'Agent', hasLeft: false}, + agent6: {id: 'agent6', pType: 'Agent', hasLeft: false}, + agent7: {id: 'agent7', pType: 'Agent', hasLeft: false}, + agent8: {id: 'agent8', pType: 'Agent', hasLeft: false}, + }, + }, + }, + }; + + store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithMaxParticipants, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + // Should be disabled when 7 other agents (8 total - current agent = 7 >= 7 max) + expect(result.current.isConsultButtonDisabled).toBe(true); + }); + + it('should disable consult button when max participants reached in three-party conference', () => { + const taskWithThreeParticipants = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'main', + interaction: { + media: { + main: { + participants: ['agent1', 'agent2', 'agent3', 'agent4'], + }, + }, + participants: { + agent1: {id: 'agent1', pType: 'Agent', hasLeft: false}, + agent2: {id: 'agent2', pType: 'Agent', hasLeft: false}, + agent3: {id: 'agent3', pType: 'Agent', hasLeft: false}, + agent4: {id: 'agent4', pType: 'Agent', hasLeft: false}, + }, + }, + }, + }; + + store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithThreeParticipants, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: false, + }) + ); + + // Should be disabled when 3 other agents (4 total - current agent = 3 >= 3 max for three-party) + expect(result.current.isConsultButtonDisabled).toBe(true); + }); + + it('should enable consult button when below max participants', () => { + const taskWithFewParticipants = { + ...mockCurrentTask, + data: { + ...mockCurrentTask.data, + interactionId: 'main', + interaction: { + media: { + main: { + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: {id: 'agent1', pType: 'Agent', hasLeft: false}, + agent2: {id: 'agent2', pType: 'Agent', hasLeft: false}, + }, + }, + }, + }; + + store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + + const {result} = renderHook(() => + useCallControl({ + currentTask: taskWithFewParticipants, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + // Should be enabled when only 1 other agent (2 - current agent = 1 < 7 max) + expect(result.current.isConsultButtonDisabled).toBe(false); + }); + }); + }); + describe('useCallControl Error Handling', () => { const onHoldResume = jest.fn(); const onEnd = jest.fn(); @@ -2496,6 +2887,7 @@ describe('useCallControl', () => { deviceType: 'BROWSER', featureFlags: {webRtcEnabled: true}, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2528,6 +2920,7 @@ describe('useCallControl', () => { deviceType: 'BROWSER', featureFlags: {webRtcEnabled: true}, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2568,6 +2961,7 @@ describe('useCallControl', () => { deviceType: 'BROWSER', featureFlags: {webRtcEnabled: true}, isMuted: false, + multiPartyConferenceEnabled: true, }) ); @@ -2600,6 +2994,7 @@ describe('useCallControl', () => { deviceType: 'BROWSER', featureFlags: {webRtcEnabled: true}, isMuted: false, + multiPartyConferenceEnabled: true, }) ); diff --git a/packages/contact-center/task/tests/utils/task-util.ts b/packages/contact-center/task/tests/utils/task-util.ts index 714145004..dacc2cfc1 100644 --- a/packages/contact-center/task/tests/utils/task-util.ts +++ b/packages/contact-center/task/tests/utils/task-util.ts @@ -1,5 +1,27 @@ import {mockTask} from '@webex/test-fixtures'; -import {findHoldTimestamp, getControlsVisibility} from '../../src/Utils/task-util'; +import { + findHoldTimestamp, + getControlsVisibility, + getIsConferenceInProgress, + getConferenceParticipants, +} from '../../src/Utils/task-util'; +import {ITask, TaskData} from '@webex/contact-center'; + +// Helper function to create properly typed partial task objects for testing +const createMockTask = (data: Partial): ITask => { + return { + ...mockTask, + data: { + ...mockTask.data, + ...data, + } as TaskData, + }; +}; + +// Helper to create partial interaction data with proper typing +const createPartialInteraction = (interaction: unknown): TaskData['interaction'] => { + return interaction as TaskData['interaction']; +}; describe('getControlsVisibility', () => { it('should show correct controls when station logis is BROWSER, all flags are enabled and media type is telehphony', () => { const deviceType = 'BROWSER'; @@ -364,3 +386,488 @@ describe('findHoldTimestamp', () => { expect(result).toBeNull(); }); }); + +describe('getIsConferenceInProgress', () => { + it('should return false when task data is missing', () => { + const task = {} as Partial as ITask; + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should return false when interaction media is missing', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({}), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should return false when interactionId is missing', () => { + const task = createMockTask({ + interaction: createPartialInteraction({ + media: {}, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should return false when there are no participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: [], + }, + }, + participants: {}, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should return false when there is only one agent participant', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should return true when there are two or more agent participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + hasLeft: false, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(true); + }); + + it('should exclude customer participants from agent count', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'customer1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + customer1: { + id: 'customer1', + pType: 'Customer', + hasLeft: false, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should exclude supervisor participants from agent count', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'supervisor1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + supervisor1: { + id: 'supervisor1', + pType: 'Supervisor', + hasLeft: false, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should exclude VVA participants from agent count', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'vva1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + vva1: { + id: 'vva1', + pType: 'VVA', + hasLeft: false, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); + + it('should exclude participants who have left from agent count', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + hasLeft: true, + }, + }, + }), + }); + expect(getIsConferenceInProgress(task)).toBe(false); + }); +}); + +describe('getConferenceParticipants', () => { + const currentAgentId = 'agent1'; + + it('should return empty array when task data is missing', () => { + const task = {} as Partial as ITask; + expect(getConferenceParticipants(task, currentAgentId)).toEqual([]); + }); + + it('should return empty array when interaction media is missing', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({}), + }); + expect(getConferenceParticipants(task, currentAgentId)).toEqual([]); + }); + + it('should return empty array when interactionId is missing', () => { + const task = createMockTask({ + interaction: createPartialInteraction({ + media: {}, + }), + }); + expect(getConferenceParticipants(task, currentAgentId)).toEqual([]); + }); + + it('should return empty array when there are no participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: [], + }, + }, + participants: {}, + }), + }); + expect(getConferenceParticipants(task, currentAgentId)).toEqual([]); + }); + + it('should return list of agent participants excluding current agent', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2', 'agent3'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + agent3: { + id: 'agent3', + pType: 'Agent', + name: 'Agent Three', + hasLeft: false, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(2); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + }); + expect(result).toContainEqual({ + id: 'agent3', + pType: 'Agent', + name: 'Agent Three', + }); + expect(result).not.toContainEqual( + expect.objectContaining({ + id: 'agent1', + }) + ); + }); + + it('should exclude customer participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2', 'customer1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + customer1: { + id: 'customer1', + pType: 'Customer', + name: 'Customer One', + hasLeft: false, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(1); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + }); + }); + + it('should exclude supervisor participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2', 'supervisor1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + supervisor1: { + id: 'supervisor1', + pType: 'Supervisor', + name: 'Supervisor One', + hasLeft: false, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(1); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + }); + }); + + it('should exclude VVA participants', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2', 'vva1'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + vva1: { + id: 'vva1', + pType: 'VVA', + name: 'VVA One', + hasLeft: false, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(1); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + }); + }); + + it('should exclude participants who have left', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2', 'agent3'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + name: 'Agent One', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + hasLeft: false, + }, + agent3: { + id: 'agent3', + pType: 'Agent', + name: 'Agent Three', + hasLeft: true, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(1); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: 'Agent Two', + }); + }); + + it('should handle participants without names', () => { + const task = createMockTask({ + interactionId: 'main', + interaction: createPartialInteraction({ + media: { + main: { + participants: ['agent1', 'agent2'], + }, + }, + participants: { + agent1: { + id: 'agent1', + pType: 'Agent', + hasLeft: false, + }, + agent2: { + id: 'agent2', + pType: 'Agent', + hasLeft: false, + }, + }, + }), + }); + + const result = getConferenceParticipants(task, currentAgentId); + + expect(result).toHaveLength(1); + expect(result).toContainEqual({ + id: 'agent2', + pType: 'Agent', + name: undefined, + }); + }); +}); From 1800b06e0c277bcab8db4256085c3d715a6c9253 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Fri, 17 Oct 2025 11:33:32 +0530 Subject: [PATCH 11/48] feat: added allow particpnats to talk checkbox --- .../call-control-custom.utils.ts | 10 ++++--- .../consult-transfer-popover.tsx | 29 +++++++++++++++---- .../task/CallControl/call-control.tsx | 25 ++++++++++++---- .../task/CallControl/call-control.utils.ts | 5 ++-- .../src/components/task/task.types.ts | 15 ++++++---- packages/contact-center/task/src/helper.ts | 7 ++++- 6 files changed, 68 insertions(+), 23 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts index c4ed054f6..18d136d20 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -324,7 +324,8 @@ export const handleTabSelection = (key: string, setSelectedTab: (tab: string) => export const handleAgentSelection = ( agentId: string, agentName: string, - onAgentSelect: ((agentId: string, agentName: string) => void) | undefined, + allowParticipantsToInteract: boolean, + onAgentSelect: ((agentId: string, agentName: string, allowParticipantsToInteract: boolean) => void) | undefined, logger: ILogger ): void => { try { @@ -333,7 +334,7 @@ export const handleAgentSelection = ( method: 'onAgentSelect', }); if (onAgentSelect) { - onAgentSelect(agentId, agentName); + onAgentSelect(agentId, agentName, allowParticipantsToInteract); } } catch (error) { logger.error(`CC-Widgets: CallControlCustom: Error in handleAgentSelection: ${error.message}`, { @@ -349,7 +350,8 @@ export const handleAgentSelection = ( export const handleQueueSelection = ( queueId: string, queueName: string, - onQueueSelect: ((queueId: string, queueName: string) => void) | undefined, + allowParticipantsToInteract: boolean, + onQueueSelect: ((queueId: string, queueName: string, allowParticipantsToInteract: boolean) => void) | undefined, logger: ILogger ): void => { try { @@ -358,7 +360,7 @@ export const handleQueueSelection = ( method: 'onQueueSelect', }); if (onQueueSelect) { - onQueueSelect(queueId, queueName); + onQueueSelect(queueId, queueName, allowParticipantsToInteract); } } catch (error) { logger.error(`CC-Widgets: CallControlCustom: Error in handleQueueSelection: ${error.message}`, { diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index b17e2158a..019093747 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -1,10 +1,12 @@ -import React from 'react'; +import React, {useState} from 'react'; import {Text, ListNext, TextInput, Button} from '@momentum-ui/react-collaboration'; import ConsultTransferListComponent from './consult-transfer-list-item'; import {ConsultTransferPopoverComponentProps} from '../../task.types'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; import {isAgentsEmpty, handleAgentSelection, handleQueueSelection} from './call-control-custom.utils'; import {useConsultTransferPopover} from './consult-transfer-popover-hooks'; +import {Checkbox} from '@momentum-design/components/dist/react'; + import { SEARCH_PLACEHOLDER, CLEAR_SEARCH, @@ -28,6 +30,7 @@ const ConsultTransferPopoverComponent: React.FC { const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; @@ -59,6 +62,7 @@ const ConsultTransferPopoverComponent: React.FC(false); const renderList = ( items: T[], @@ -162,14 +166,14 @@ const ConsultTransferPopoverComponent: React.FC ({id: agent.agentId, name: agent.agentName})), - (item) => handleAgentSelection(item.id, item.name, onAgentSelect, logger) + (item) => handleAgentSelection(item.id, item.name, allowParticipantsToInteract, onAgentSelect, logger) )} {selectedCategory === 'Queues' && !noQueues && (
{renderList( queuesData.map((q) => ({id: q.id, name: q.name})), - (item) => handleQueueSelection(item.id, item.name, onQueueSelect, logger) + (item) => handleQueueSelection(item.id, item.name, allowParticipantsToInteract, onQueueSelect, logger) )} {hasMoreQueues && (
@@ -194,7 +198,7 @@ const ConsultTransferPopoverComponent: React.FC { if (item.number) { if (onDialNumberSelect) { - onDialNumberSelect(item.number); + onDialNumberSelect(item.number, allowParticipantsToInteract); } } } @@ -221,7 +225,7 @@ const ConsultTransferPopoverComponent: React.FC ({id: e.id, name: e.name})), (item) => { if (onEntryPointSelect) { - onEntryPointSelect(item.id, item.name); + onEntryPointSelect(item.id, item.name, allowParticipantsToInteract); } } )} @@ -240,6 +244,21 @@ const ConsultTransferPopoverComponent: React.FC )} + {isConferenceInProgress && ( +
+ { + setAllowParticipantsToInteract(!allowParticipantsToInteract); + }} + /> +
+ )}
); }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index 4d00b8141..3edaebd6e 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -96,11 +96,17 @@ function CallControlComponent(props: CallControlComponentProps) { handleWrapupChangeUtil(text, value, setSelectedWrapupReason, setSelectedWrapupId, logger); }; - const handleTargetSelect = (id: string, name: string, type: DestinationType) => { + const handleTargetSelect = ( + id: string, + name: string, + type: DestinationType, + allowParticipantsToInteract: boolean + ) => { handleTargetSelectUtil( id, name, type, + allowParticipantsToInteract, agentMenuType, consultCall, transferCall, @@ -217,12 +223,18 @@ function CallControlComponent(props: CallControlComponentProps) { getAddressBookEntries={getAddressBookEntries} getEntryPoints={getEntryPoints} getQueues={getQueuesFetcher} - onAgentSelect={(agentId, agentName) => handleTargetSelect(agentId, agentName, 'agent')} - onQueueSelect={(queueId, queueName) => handleTargetSelect(queueId, queueName, 'queue')} - onEntryPointSelect={(entryPointId, entryPointName) => - handleTargetSelect(entryPointId, entryPointName, 'entryPoint') + onAgentSelect={(agentId, agentName, allowParticipantsToInteract) => + handleTargetSelect(agentId, agentName, 'agent', allowParticipantsToInteract) + } + onQueueSelect={(queueId, queueName, allowParticipantsToInteract) => + handleTargetSelect(queueId, queueName, 'queue', allowParticipantsToInteract) + } + onEntryPointSelect={(entryPointId, entryPointName, allowParticipantsToInteract) => + handleTargetSelect(entryPointId, entryPointName, 'entryPoint', allowParticipantsToInteract) + } + onDialNumberSelect={(dialNumber, allowParticipantsToInteract) => + handleTargetSelect(dialNumber, dialNumber, 'dialNumber', allowParticipantsToInteract) } - onDialNumberSelect={(dialNumber) => handleTargetSelect(dialNumber, dialNumber, 'dialNumber')} allowConsultToQueue={allowConsultToQueue} consultTransferOptions={ isTelephony @@ -233,6 +245,7 @@ function CallControlComponent(props: CallControlComponentProps) { showEntryPointTab: false, } } + isConferenceInProgress={controlVisibility.isConferenceInProgress} logger={logger} /> ) : null} diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts index 6e206c9dc..07486ff57 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts @@ -120,8 +120,9 @@ export const handleTargetSelect = ( id: string, name: string, type: DestinationType, + allowParticipantsToInteract: boolean, agentMenuType: CallControlMenuType | null, - consultCall: (id: string, type: DestinationType) => void, + consultCall: (id: string, type: DestinationType, allowParticipantsToInteract: boolean) => void, transferCall: (id: string, type: DestinationType) => void, setConsultAgentId: (id: string) => void, setConsultAgentName: (name: string) => void, @@ -134,7 +135,7 @@ export const handleTargetSelect = ( }); if (agentMenuType === 'Consult') { try { - consultCall(id, type); + consultCall(id, type, allowParticipantsToInteract); setConsultAgentId(id); setConsultAgentName(name); setLastTargetType(type); diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index 32c7f3304..08d0b58c9 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -287,7 +287,11 @@ export interface ControlProps { * @param destinationType * @returns */ - consultCall: (consultDestination: string, destinationType: DestinationType) => void; + consultCall: ( + consultDestination: string, + destinationType: DestinationType, + allowParticipantsToInteract: boolean + ) => void; /** * Function to merge the consult call in a conference. @@ -573,13 +577,14 @@ export interface ConsultTransferPopoverComponentProps { getAddressBookEntries?: FetchPaginatedList; getEntryPoints?: FetchPaginatedList; getQueues?: FetchPaginatedList; - onAgentSelect?: (agentId: string, agentName: string) => void; - onQueueSelect?: (queueId: string, queueName: string) => void; - onEntryPointSelect?: (entryPointId: string, entryPointName: string) => void; - onDialNumberSelect?: (dialNumber: string) => void; + onAgentSelect?: (agentId: string, agentName: string, allowParticipantsToInteract: boolean) => void; + onQueueSelect?: (queueId: string, queueName: string, allowParticipantsToInteract: boolean) => void; + onEntryPointSelect?: (entryPointId: string, entryPointName: string, allowParticipantsToInteract: boolean) => void; + onDialNumberSelect?: (dialNumber: string, allowParticipantsToInteract: boolean) => void; allowConsultToQueue: boolean; /** Options governing popover visibility/behavior */ consultTransferOptions?: ConsultTransferOptions; + isConferenceInProgress?: boolean; logger: ILogger; } diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index ace2b891a..85a55be6d 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -777,10 +777,15 @@ export const useCallControl = (props: useCallControlProps) => { } }; - const consultCall = async (consultDestination: string, destinationType: DestinationType) => { + const consultCall = async ( + consultDestination: string, + destinationType: DestinationType, + allowParticipantsToInteract: boolean + ) => { const consultPayload = { to: consultDestination, destinationType: destinationType, + holdParticipants: !allowParticipantsToInteract, }; if (destinationType === 'queue') { From ef5804eb6f58ce682ffeb73fc55f731e448e2243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Fri, 17 Oct 2025 15:54:08 +0530 Subject: [PATCH 12/48] fix(multy-party-conference): fix-conference-uts --- .../consult-transfer-popover.tsx | 162 +- .../task/CallControl/call-control.styles.scss | 14 + ...consult-transfer-popover.snapshot.tsx.snap | 2871 +++++++++-------- .../call-control-custom.util.tsx | 12 +- .../consult-transfer-popover.tsx | 4 +- .../task/CallControl/call-control.utils.tsx | 7 +- .../task/tests/IncomingTask/index.tsx | 15 +- .../task/tests/TaskList/index.tsx | 18 +- packages/contact-center/task/tests/helper.ts | 100 +- 9 files changed, 1702 insertions(+), 1501 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 019093747..5760565b0 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -160,96 +160,98 @@ const ConsultTransferPopoverComponent: React.FC - {!hasAnyData && } +
+ {!hasAnyData && } - {selectedCategory === 'Agents' && - !noAgents && - renderList( - buddyAgents.map((agent) => ({id: agent.agentId, name: agent.agentName})), - (item) => handleAgentSelection(item.id, item.name, allowParticipantsToInteract, onAgentSelect, logger) - )} - - {selectedCategory === 'Queues' && !noQueues && ( -
- {renderList( - queuesData.map((q) => ({id: q.id, name: q.name})), - (item) => handleQueueSelection(item.id, item.name, allowParticipantsToInteract, onQueueSelect, logger) - )} - {hasMoreQueues && ( -
- {loadingQueues ? ( - - {LOADING_MORE_QUEUES} - - ) : ( - - {SCROLL_TO_LOAD_MORE} - - )} -
+ {selectedCategory === 'Agents' && + !noAgents && + renderList( + buddyAgents.map((agent) => ({id: agent.agentId, name: agent.agentName})), + (item) => handleAgentSelection(item.id, item.name, allowParticipantsToInteract, onAgentSelect, logger) )} -
- )} - {showDialNumberTab && selectedCategory === 'Dial Number' && !noDialNumbers && ( -
- {renderList( - dialNumbers.map((d) => ({id: d.id, name: d.name, number: d.number})), - (item) => { - if (item.number) { - if (onDialNumberSelect) { - onDialNumberSelect(item.number, allowParticipantsToInteract); + {selectedCategory === 'Queues' && !noQueues && ( +
+ {renderList( + queuesData.map((q) => ({id: q.id, name: q.name})), + (item) => handleQueueSelection(item.id, item.name, allowParticipantsToInteract, onQueueSelect, logger) + )} + {hasMoreQueues && ( +
+ {loadingQueues ? ( + + {LOADING_MORE_QUEUES} + + ) : ( + + {SCROLL_TO_LOAD_MORE} + + )} +
+ )} +
+ )} + + {showDialNumberTab && selectedCategory === 'Dial Number' && !noDialNumbers && ( +
+ {renderList( + dialNumbers.map((d) => ({id: d.id, name: d.name, number: d.number})), + (item) => { + if (item.number) { + if (onDialNumberSelect) { + onDialNumberSelect(item.number, allowParticipantsToInteract); + } } } - } - )} - {hasMoreDialNumbers && ( -
- {loadingDialNumbers ? ( - - {LOADING_MORE_DIAL_NUMBERS} - - ) : ( - - {SCROLL_TO_LOAD_MORE} - - )} -
- )} -
- )} + )} + {hasMoreDialNumbers && ( +
+ {loadingDialNumbers ? ( + + {LOADING_MORE_DIAL_NUMBERS} + + ) : ( + + {SCROLL_TO_LOAD_MORE} + + )} +
+ )} +
+ )} - {showEntryPointTab && selectedCategory === 'Entry Point' && !noEntryPoints && ( -
- {renderList( - entryPoints.map((e) => ({id: e.id, name: e.name})), - (item) => { - if (onEntryPointSelect) { - onEntryPointSelect(item.id, item.name, allowParticipantsToInteract); + {showEntryPointTab && selectedCategory === 'Entry Point' && !noEntryPoints && ( +
+ {renderList( + entryPoints.map((e) => ({id: e.id, name: e.name})), + (item) => { + if (onEntryPointSelect) { + onEntryPointSelect(item.id, item.name, allowParticipantsToInteract); + } } - } - )} - {hasMoreEntryPoints && ( -
- {loadingEntryPoints ? ( - - {LOADING_MORE_ENTRY_POINTS} - - ) : ( - - {SCROLL_TO_LOAD_MORE} - - )} -
- )} -
- )} + )} + {hasMoreEntryPoints && ( +
+ {loadingEntryPoints ? ( + + {LOADING_MORE_ENTRY_POINTS} + + ) : ( + + {SCROLL_TO_LOAD_MORE} + + )} +
+ )} +
+ )} +
{isConferenceInProgress && ( -
+
-
    -
    -
  • +
    -
    -
    -
    - - Agent One - -
    -
    + + Agent One + +
    - + +
  • -
- -
-
-
  • +
  • +
    -
    -
    -
    - - Agent Two - -
    -
    + + Agent Two + +
    - + +
    -
    - -
    - + +
    + +
    `; @@ -379,86 +383,90 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co
    -
    -
      -
      +
      +
        -
      • -
        -
      -
      - - Queue One - -
      -
      + + Queue One + +
      - + +
      -
    - -
    - + +
    + +
    `; @@ -574,86 +582,90 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co
    -
    -
      -
      +
      +
        -
      • -
        -
      -
      - - Queue One - -
      -
      + + Queue One + +
      - + +
      -
    - -
    - + +
    + + `; @@ -769,160 +781,164 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -1037,235 +1053,239 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Available Agent - -
      -
      - + Available Agent +
      -
      -
    • -
      -
      -
    • -
      -
      -
      - - Busy Agent - -
      -
      + + +
      +
    • + + +
      +
    • - + +
    • - - - -
      -
    • -
      -
      -
      - - Idle Agent - -
      -
      + +
      +
    • + + +
      +
    • -
      +
    • +
      + - - + Idle Agent +
      - - - -
    +
    +
    + +
    +
    + + + + `; @@ -1380,160 +1400,164 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -1649,15 +1673,19 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
    - No data available for consult transfer. +
    +
    + No data available for consult transfer. +
    @@ -1774,160 +1802,164 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -2043,15 +2075,19 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
    - No data available for consult transfer. +
    +
    + No data available for consult transfer. +
    @@ -2168,6 +2204,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    +
    `; @@ -2282,85 +2321,89 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Single Agent - -
      -
      + + Single Agent + +
      - + +
    • - - - -
    + + + + `; @@ -2471,90 +2514,94 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem class="md-button__children" style="opacity: 1;" > - Entry Point - - - -
    -
      -
      -
    • + +
    • +
      +
      +
        +
        -
        -
        -
        - - Single Queue - -
        -
        + + Single Queue + +
        - + +
      -
      - -
    - + + + + `; @@ -2670,160 +2717,164 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -2938,81 +2989,85 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat -
      -
      -
    • +
      -
      +
      +
      + + New Agent One +
      -
      -
      - - New Agent One - -
      -
      - + +
      -
    • - - -
    + + + + `; @@ -3127,160 +3182,164 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -3395,86 +3454,90 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat -
    -
      -
      +
      +
        -
      • -
        -
      -
      - - New Queue One - -
      -
      + + New Queue One + +
      - + +
      -
    - - - + + + + `; diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx index 368731055..92a3b0ba6 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-custom.util.tsx @@ -463,18 +463,18 @@ describe('Call Control Custom Utils', () => { const agentId = 'agent1'; const agentName = 'John Doe'; - handleAgentSelection(agentId, agentName, mockOnAgentSelect, loggerMock); + handleAgentSelection(agentId, agentName, false, mockOnAgentSelect, loggerMock); expect(loggerMock.info).toHaveBeenCalledWith(`CC-Widgets: ConsultTransferPopover: agent selected: ${agentId}`, { module: 'consult-transfer-popover.tsx', method: 'onAgentSelect', }); - expect(mockOnAgentSelect).toHaveBeenCalledWith(agentId, agentName); + expect(mockOnAgentSelect).toHaveBeenCalledWith(agentId, agentName, false); }); it('should not call onAgentSelect when not provided', () => { expect(() => { - handleAgentSelection('agent1', 'John Doe', undefined, loggerMock); + handleAgentSelection('agent1', 'John Doe', false, undefined, loggerMock); }).not.toThrow(); expect(loggerMock.info).toHaveBeenCalled(); @@ -487,18 +487,18 @@ describe('Call Control Custom Utils', () => { const queueId = 'queue1'; const queueName = 'Support Queue'; - handleQueueSelection(queueId, queueName, mockOnQueueSelect, loggerMock); + handleQueueSelection(queueId, queueName, false, mockOnQueueSelect, loggerMock); expect(loggerMock.log).toHaveBeenCalledWith(`CC-Widgets: ConsultTransferPopover: queue selected: ${queueId}`, { module: 'consult-transfer-popover.tsx', method: 'onQueueSelect', }); - expect(mockOnQueueSelect).toHaveBeenCalledWith(queueId, queueName); + expect(mockOnQueueSelect).toHaveBeenCalledWith(queueId, queueName, false); }); it('should not call onQueueSelect when not provided', () => { expect(() => { - handleQueueSelection('queue1', 'Support Queue', undefined, loggerMock); + handleQueueSelection('queue1', 'Support Queue', false, undefined, loggerMock); }).not.toThrow(); expect(loggerMock.log).toHaveBeenCalled(); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 704c816f7..c27dc89b8 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -118,7 +118,7 @@ describe('ConsultTransferPopoverComponent', () => { // Test agent selection - click on the button inside the first agent item const firstAgentButton = screen.container.querySelectorAll('.call-control-list-item button')[0]; fireEvent.click(firstAgentButton); - expect(mockOnAgentSelect).toHaveBeenCalledWith('agent1', 'Agent One'); + expect(mockOnAgentSelect).toHaveBeenCalledWith('agent1', 'Agent One', false); // Test onMouseDown event handler (covers line 39) - just trigger the event const listItemContainer = screen.container.querySelector('.consult-list-item-wrapper'); @@ -133,7 +133,7 @@ describe('ConsultTransferPopoverComponent', () => { ); const firstQueueButton = screen.container.querySelectorAll('.call-control-list-item button')[0]; fireEvent.click(firstQueueButton); - expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One'); + expect(mockOnQueueSelect).toHaveBeenCalledWith('queue1', 'Queue One', false); }); it('hides Dial Number tab when consultTransferOptions.showDialNumberTab is false', async () => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx index 8c6950e65..cc7e7d622 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx @@ -273,6 +273,7 @@ describe('CallControl Utils', () => { 'agent-123', 'John Doe', 'agent', + false, 'Consult', mockConsultCall, mockTransferCall, @@ -286,7 +287,7 @@ describe('CallControl Utils', () => { module: 'call-control.tsx', method: 'handleTargetSelect', }); - expect(mockConsultCall).toHaveBeenCalledWith('agent-123', 'agent'); + expect(mockConsultCall).toHaveBeenCalledWith('agent-123', 'agent', false); expect(mockSetConsultAgentId).toHaveBeenCalledWith('agent-123'); expect(mockSetConsultAgentName).toHaveBeenCalledWith('John Doe'); expect(mockSetLastTargetType).toHaveBeenCalledWith('agent'); @@ -298,6 +299,7 @@ describe('CallControl Utils', () => { 'queue-456', 'Support Queue', 'queue', + false, 'Transfer', mockConsultCall, mockTransferCall, @@ -328,6 +330,7 @@ describe('CallControl Utils', () => { 'agent-123', 'John Doe', 'agent', + false, 'Consult', mockConsultCall, mockTransferCall, @@ -354,6 +357,7 @@ describe('CallControl Utils', () => { 'queue-456', 'Support Queue', 'queue', + false, 'Transfer', mockConsultCall, mockTransferCall, @@ -375,6 +379,7 @@ describe('CallControl Utils', () => { 'agent-123', 'John Doe', 'agent', + false, null, mockConsultCall, mockTransferCall, diff --git a/packages/contact-center/task/tests/IncomingTask/index.tsx b/packages/contact-center/task/tests/IncomingTask/index.tsx index 4658367e7..44f312907 100644 --- a/packages/contact-center/task/tests/IncomingTask/index.tsx +++ b/packages/contact-center/task/tests/IncomingTask/index.tsx @@ -49,7 +49,7 @@ describe('IncomingTask Component', () => { }); describe('ErrorBoundary Tests', () => { - it('should render empty fragment when ErrorBoundary catches an error', () => { + it('should render empty fragment when ErrorBoundary catches an error and onErrorCallback is defined', () => { jest.spyOn(helper, 'useIncomingTask').mockImplementation(() => { throw new Error('Test error in useIncomingTask'); }); @@ -63,5 +63,18 @@ describe('IncomingTask Component', () => { expect(mockOnErrorCallback).toHaveBeenCalledWith('IncomingTask', Error('Test error in useIncomingTask')); expect(mockOnErrorCallback).toHaveBeenCalledTimes(1); }); + + it('should render empty fragment when ErrorBoundary catches an error and onErrorCallback is undefined', () => { + jest.spyOn(helper, 'useIncomingTask').mockImplementation(() => { + throw new Error('Test error without callback'); + }); + store.onErrorCallback = undefined; + const {container} = render( + + ); + + expect(container.firstChild).toBeNull(); + // Should not throw, just render empty + }); }); }); diff --git a/packages/contact-center/task/tests/TaskList/index.tsx b/packages/contact-center/task/tests/TaskList/index.tsx index 78d7fd71b..1b03c075d 100644 --- a/packages/contact-center/task/tests/TaskList/index.tsx +++ b/packages/contact-center/task/tests/TaskList/index.tsx @@ -63,7 +63,7 @@ describe('TaskList Component', () => { }); describe('ErrorBoundary Tests', () => { - it('should render empty fragment when ErrorBoundary catches an error', () => { + it('should render empty fragment when ErrorBoundary catches an error and onErrorCallback is defined', () => { const mockOnErrorCallback = jest.fn(); store.onErrorCallback = mockOnErrorCallback; // Mock the useTaskList to throw an error @@ -80,5 +80,21 @@ describe('TaskList Component', () => { expect(mockOnErrorCallback).toHaveBeenCalledWith('TaskList', expect.any(Error)); expect(mockOnErrorCallback).toHaveBeenCalledTimes(1); }); + + it('should render empty fragment when ErrorBoundary catches an error and onErrorCallback is undefined', () => { + store.onErrorCallback = undefined; + // Mock the useTaskList to throw an error + jest.spyOn(helper, 'useTaskList').mockImplementation(() => { + throw new Error('Test error without callback'); + }); + + const {container} = render( + + ); + + // The fallback should render an empty fragment (no content) + expect(container.firstChild).toBeNull(); + // Should not throw, just render empty + }); }); }); diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 27a2ce6f2..33ca8d567 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -1389,9 +1389,43 @@ describe('useCallControl', () => { }) ); await act(async () => { - await result.current.consultCall('dest123', 'agent'); + await result.current.consultCall('dest123', 'agent', false); + }); + expect(mockCurrentTask.consult).toHaveBeenCalledWith({ + to: 'dest123', + destinationType: 'agent', + holdParticipants: true, + }); + expect(setConsultInitiatedSpy).toHaveBeenCalledWith(true); + setConsultInitiatedSpy.mockRestore(); + }); + + it('should call consultCall with allowParticipantsToInteract set to true', async () => { + mockCurrentTask.consult = jest.fn().mockResolvedValue('Consulted'); + const setConsultInitiatedSpy = jest.spyOn(store, 'setConsultInitiated'); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + await act(async () => { + await result.current.consultCall('dest123', 'agent', true); + }); + expect(mockCurrentTask.consult).toHaveBeenCalledWith({ + to: 'dest123', + destinationType: 'agent', + holdParticipants: false, }); - expect(mockCurrentTask.consult).toHaveBeenCalledWith({to: 'dest123', destinationType: 'agent'}); expect(setConsultInitiatedSpy).toHaveBeenCalledWith(true); setConsultInitiatedSpy.mockRestore(); }); @@ -1414,8 +1448,12 @@ describe('useCallControl', () => { }) ); - await expect(result.current.consultCall('dest123', 'agent')).rejects.toThrow(consultError); - expect(mockCurrentTask.consult).toHaveBeenCalledWith({to: 'dest123', destinationType: 'agent'}); + await expect(result.current.consultCall('dest123', 'agent', false)).rejects.toThrow(consultError); + expect(mockCurrentTask.consult).toHaveBeenCalledWith({ + to: 'dest123', + destinationType: 'agent', + holdParticipants: true, + }); expect(mockLogger.error).toHaveBeenCalledWith('Error consulting call: Error: Consult failed', { module: 'widget-cc-task#helper.ts', method: 'useCallControl#consultCall', @@ -2160,14 +2198,64 @@ describe('useCallControl', () => { ); await act(async () => { - await result.current.consultCall('queueId123', 'queue'); + await result.current.consultCall('queueId123', 'queue', false); }); - expect(mockCurrentTask.consult).toHaveBeenCalledWith({to: 'queueId123', destinationType: 'queue'}); + expect(mockCurrentTask.consult).toHaveBeenCalledWith({ + to: 'queueId123', + destinationType: 'queue', + holdParticipants: true, + }); + expect(setIsQueueConsultInProgressSpy).toHaveBeenCalledWith(true); + expect(setCurrentConsultQueueIdSpy).toHaveBeenCalledWith('queueId123'); + expect(setIsQueueConsultInProgressSpy).toHaveBeenCalledWith(false); + expect(setCurrentConsultQueueIdSpy).toHaveBeenCalledWith(null); + + setIsQueueConsultInProgressSpy.mockRestore(); + setCurrentConsultQueueIdSpy.mockRestore(); + setConsultInitiatedSpy.mockRestore(); + }); + + it('should handle errors when calling consultCall with queue destination type', async () => { + const consultError = new Error('Queue consult failed'); + mockCurrentTask.consult = jest.fn().mockRejectedValue(consultError); + const setIsQueueConsultInProgressSpy = jest.spyOn(store, 'setIsQueueConsultInProgress'); + const setCurrentConsultQueueIdSpy = jest.spyOn(store, 'setCurrentConsultQueueId'); + const setConsultInitiatedSpy = jest.spyOn(store, 'setConsultInitiated'); + + const {result} = renderHook(() => + useCallControl({ + currentTask: mockCurrentTask, + onHoldResume: mockOnHoldResume, + onEnd: mockOnEnd, + onWrapUp: mockOnWrapUp, + logger: mockLogger, + featureFlags: store.featureFlags, + deviceType: store.deviceType, + consultInitiated: false, + isMuted: false, + multiPartyConferenceEnabled: true, + }) + ); + + await expect(result.current.consultCall('queueId123', 'queue', false)).rejects.toThrow(consultError); + + expect(mockCurrentTask.consult).toHaveBeenCalledWith({ + to: 'queueId123', + destinationType: 'queue', + holdParticipants: true, + }); expect(setIsQueueConsultInProgressSpy).toHaveBeenCalledWith(true); expect(setCurrentConsultQueueIdSpy).toHaveBeenCalledWith('queueId123'); + expect(setConsultInitiatedSpy).toHaveBeenCalledWith(true); + // Check that cleanup happened on error expect(setIsQueueConsultInProgressSpy).toHaveBeenCalledWith(false); expect(setCurrentConsultQueueIdSpy).toHaveBeenCalledWith(null); + expect(setConsultInitiatedSpy).toHaveBeenCalledWith(false); + expect(mockLogger.error).toHaveBeenCalledWith('Error consulting call: Error: Queue consult failed', { + module: 'widget-cc-task#helper.ts', + method: 'useCallControl#consultCall', + }); setIsQueueConsultInProgressSpy.mockRestore(); setCurrentConsultQueueIdSpy.mockRestore(); From ab389accd8fb12acfa27d2966cd7a1a4820ae235 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 21 Oct 2025 12:02:30 +0530 Subject: [PATCH 13/48] fix: task selection --- .../cc-components/src/components/task/TaskList/task-list.tsx | 2 +- .../src/components/task/TaskList/task-list.utils.ts | 3 ++- .../tests/components/task/TaskList/task-list.utils.tsx | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx index 795fb808f..c0f2fa29d 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx @@ -45,7 +45,7 @@ const TaskListComponent: React.FunctionComponent = (prop acceptTask={() => acceptTask(task)} declineTask={() => declineTask(task)} ronaTimeout={taskData.ronaTimeout} - onTaskSelect={createTaskSelectHandler(task, currentTask, onTaskSelect)} + onTaskSelect={createTaskSelectHandler(task, currentTask, onTaskSelect, agentId)} acceptText={taskData.acceptText} disableAccept={taskData.disableAccept} declineText={taskData.declineText} diff --git a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts index 99c275b38..5635fad57 100644 --- a/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts @@ -198,12 +198,13 @@ export const createTaskSelectHandler = ( task: ITask, currentTask: ITask | null, onTaskSelect: (task: ITask) => void, + agentId: string, logger? ) => { return () => { try { // Logging moved to helper.ts - const taskData = extractTaskListItemData(task, true, logger); // Use browser=true for selection logic + const taskData = extractTaskListItemData(task, true, agentId, logger); // Use browser=true for selection logic if (isTaskSelectable(task, currentTask, taskData, logger)) { onTaskSelect(task); diff --git a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx index ff88e04f9..ac93a7a84 100644 --- a/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx @@ -446,7 +446,7 @@ describe('task-list.utils', () => { }, }; - const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect); + const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect, mockTask.agentId); handler(); expect(mockOnTaskSelect).toHaveBeenCalledWith(task); @@ -477,7 +477,7 @@ describe('task-list.utils', () => { }, }; - const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect); + const handler = createTaskSelectHandler(task, currentTask, mockOnTaskSelect, mockTask.agentId); handler(); expect(mockOnTaskSelect).not.toHaveBeenCalled(); From c54f9ec44070b6008cfb20b69e200a54bf094804 Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Tue, 21 Oct 2025 13:26:46 +0530 Subject: [PATCH 14/48] fix: max count --- .../task/src/Utils/constants.ts | 2 +- .../task/src/Utils/task-util.ts | 28 +++++++++++++++++++ packages/contact-center/task/src/helper.ts | 11 ++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/task/src/Utils/constants.ts b/packages/contact-center/task/src/Utils/constants.ts index 4b6f92735..81ce4f1d2 100644 --- a/packages/contact-center/task/src/Utils/constants.ts +++ b/packages/contact-center/task/src/Utils/constants.ts @@ -3,4 +3,4 @@ export const CUSTOMER = 'Customer'; export const SUPERVISOR = 'Supervisor'; export const VVA = 'VVA'; export const MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE = 7; -export const MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE = 3; +export const MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE = 2; diff --git a/packages/contact-center/task/src/Utils/task-util.ts b/packages/contact-center/task/src/Utils/task-util.ts index 0935cc37c..3f1775e1b 100644 --- a/packages/contact-center/task/src/Utils/task-util.ts +++ b/packages/contact-center/task/src/Utils/task-util.ts @@ -156,3 +156,31 @@ export const getConferenceParticipants = (task: ITask, agentId: string): Partici return participantsList; }; + +export const getConferenceParticipantsCount = (task: ITask): number => { + const participantsList: Participant[] = []; + + // Early return if required data is missing + if (!task?.data?.interaction?.media || !task?.data?.interactionId) { + return 0; + } + + const mediaMainCall = task.data.interaction.media[task.data.interactionId]; + const participantsInMainCall = new Set(mediaMainCall?.participants); + const participants = task?.data?.interaction?.participants; + + if (participantsInMainCall.size > 0 && participants) { + participantsInMainCall.forEach((participantId: string) => { + const participant = participants[participantId]; + if (participant && participant.pType !== SUPERVISOR && !participant.hasLeft && participant.pType !== VVA) { + participantsList.push({ + id: participant.id, + pType: participant.pType, + name: participant.name, + }); + } + }); + } + + return participantsList.length; +}; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 85a55be6d..9af21734e 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -9,7 +9,12 @@ import store, { PaginatedListParams, } from '@webex/cc-store'; import {Participant} from '@webex/cc-components'; -import {findHoldTimestamp, getConferenceParticipants, getControlsVisibility} from './Utils/task-util'; +import { + findHoldTimestamp, + getConferenceParticipants, + getConferenceParticipantsCount, + getControlsVisibility, +} from './Utils/task-util'; import {MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE, MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE} from './Utils/constants'; const ENGAGED_LABEL = 'ENGAGED'; @@ -294,6 +299,7 @@ export const useCallControl = (props: useCallControlProps) => { const workerRef = useRef(null); const [lastTargetType, setLastTargetType] = useState<'agent' | 'queue'>('agent'); const [conferenceParticipants, setConferenceParticipants] = useState([]); + const [conferenceParticipantsCount, setConferenceParticipantsCount] = useState(0); const workerScript = ` let intervalId = null; @@ -364,6 +370,7 @@ export const useCallControl = (props: useCallControlProps) => { const participants = getConferenceParticipants(currentTask, store.cc.agentConfig.agentId); setConferenceParticipants(participants); } + setConferenceParticipantsCount(getConferenceParticipantsCount(currentTask)); }, [currentTask]); // Function to extract consulting agent information const extractConsultingAgent = useCallback(() => { @@ -900,7 +907,7 @@ export const useCallControl = (props: useCallControlProps) => { ? MAX_PARTICIPANTS_IN_MULTIPARTY_CONFERENCE : MAX_PARTICIPANTS_IN_THREE_PARTY_CONFERENCE; - const isConsultButtonDisabled = conferenceParticipants.length >= maxParticipantsInConference; + const isConsultButtonDisabled = conferenceParticipantsCount >= maxParticipantsInConference; return { currentTask, endCall, From 6c59bc15d7795ca3406faf239aff70e8ec9f4ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Thu, 23 Oct 2025 13:26:15 +0530 Subject: [PATCH 15/48] fix(multy-party-conference): update-cc-sdk-version --- packages/contact-center/store/package.json | 2 +- yarn.lock | 315 +++++++++++++++++++-- 2 files changed, 297 insertions(+), 20 deletions(-) diff --git a/packages/contact-center/store/package.json b/packages/contact-center/store/package.json index b0b914321..744f134f3 100644 --- a/packages/contact-center/store/package.json +++ b/packages/contact-center/store/package.json @@ -22,7 +22,7 @@ "test:styles": "eslint" }, "dependencies": { - "@webex/contact-center": "3.9.0-next.18", + "@webex/contact-center": "3.9.0-next.26", "mobx": "6.13.5", "typescript": "5.6.3" }, diff --git a/yarn.lock b/yarn.lock index c55e8071a..36c0dcce0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9265,21 +9265,21 @@ __metadata: languageName: node linkType: hard -"@webex/calling@npm:3.9.0-next.10": - version: 3.9.0-next.10 - resolution: "@webex/calling@npm:3.9.0-next.10" +"@webex/calling@npm:3.9.0-next.16": + version: 3.9.0-next.16 + resolution: "@webex/calling@npm:3.9.0-next.16" dependencies: "@types/platform": "npm:1.3.4" - "@webex/internal-media-core": "npm:2.19.0" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" - "@webex/media-helpers": "npm:3.9.0-next.1" + "@webex/internal-media-core": "npm:2.20.0" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.6" + "@webex/media-helpers": "npm:3.9.0-next.2" async-mutex: "npm:0.4.0" buffer: "npm:6.0.3" jest-html-reporters: "npm:3.0.11" platform: "npm:1.3.6" uuid: "npm:8.3.2" xstate: "npm:4.30.6" - checksum: 10c0/d1d1a203b1172c672e6d13ec29e2224356a59c740514e579c090850cdc756cb3394d1034d95f719b2ae4d0228743afb4e5080a44cba08bbb38ce8b7c8bc94b14 + checksum: 10c0/ea1f29b4c03782046896410c8e2a5a770dd2bdd96d18697e32cdd81d3bbceb0e48f4dd3f372528dec5acfe55316f0341ae2589e264c75b1ee21de3bd51da0a6d languageName: node linkType: hard @@ -9409,7 +9409,7 @@ __metadata: "@testing-library/react": "npm:16.0.1" "@types/jest": "npm:29.5.14" "@types/react-test-renderer": "npm:18" - "@webex/contact-center": "npm:3.9.0-next.18" + "@webex/contact-center": "npm:3.9.0-next.26" "@webex/test-fixtures": "workspace:*" babel-jest: "npm:29.7.0" babel-loader: "npm:9.2.1" @@ -9744,21 +9744,21 @@ __metadata: languageName: node linkType: hard -"@webex/contact-center@npm:3.9.0-next.18": - version: 3.9.0-next.18 - resolution: "@webex/contact-center@npm:3.9.0-next.18" +"@webex/contact-center@npm:3.9.0-next.26": + version: 3.9.0-next.26 + resolution: "@webex/contact-center@npm:3.9.0-next.26" dependencies: "@types/platform": "npm:1.3.4" - "@webex/calling": "npm:3.9.0-next.10" - "@webex/internal-plugin-mercury": "npm:3.9.0-next.3" - "@webex/internal-plugin-metrics": "npm:3.9.0-next.3" - "@webex/internal-plugin-support": "npm:3.9.0-next.5" - "@webex/plugin-authorization": "npm:3.9.0-next.3" - "@webex/plugin-logger": "npm:3.9.0-next.3" - "@webex/webex-core": "npm:3.9.0-next.3" + "@webex/calling": "npm:3.9.0-next.16" + "@webex/internal-plugin-mercury": "npm:3.9.0-next.6" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.6" + "@webex/internal-plugin-support": "npm:3.9.0-next.8" + "@webex/plugin-authorization": "npm:3.9.0-next.6" + "@webex/plugin-logger": "npm:3.9.0-next.6" + "@webex/webex-core": "npm:3.9.0-next.6" jest-html-reporters: "npm:3.0.11" lodash: "npm:^4.17.21" - checksum: 10c0/ba6e06d9fb3bda054dd311627b63a5f70b9b2c04847741a3fdc45febe1496c0a453c810a9a7ed3c9fd1fd0baab03901dcc08c3e4815083c02c2f5efe6713b4f1 + checksum: 10c0/6418ae11e90afd1d4ad31a5271f455e83674aa868f60e8c86e201783c5dbf65c4f4c21fda26b1617ef8bad10c4ae1e5d204338a4583d585f35ef9285cd622c42 languageName: node linkType: hard @@ -9970,6 +9970,26 @@ __metadata: languageName: node linkType: hard +"@webex/internal-media-core@npm:2.20.0": + version: 2.20.0 + resolution: "@webex/internal-media-core@npm:2.20.0" + dependencies: + "@babel/runtime": "npm:^7.18.9" + "@babel/runtime-corejs2": "npm:^7.25.0" + "@webex/rtcstats": "npm:^1.5.5" + "@webex/ts-sdp": "npm:1.8.2" + "@webex/web-capabilities": "npm:^1.6.1" + "@webex/web-client-media-engine": "npm:3.34.0" + events: "npm:^3.3.0" + ip-anonymize: "npm:^0.1.0" + typed-emitter: "npm:^2.1.0" + uuid: "npm:^8.3.2" + webrtc-adapter: "npm:^8.1.2" + xstate: "npm:^4.30.6" + checksum: 10c0/b8ce22d86a7273e2ef24a26129cc4eddbcbc6df756f2708df7000a28a258c7400e03023f42dbb9f057baa0fffc48cf20fa61d57dd5f824558a422ae947ce0022 + languageName: node + linkType: hard + "@webex/internal-plugin-calendar@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-calendar@npm:2.60.2" @@ -10066,6 +10086,24 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-conversation@npm:3.9.0-next.7": + version: 3.9.0-next.7 + resolution: "@webex/internal-plugin-conversation@npm:3.9.0-next.7" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/helper-html": "npm:3.8.1-next.11" + "@webex/helper-image": "npm:3.8.1-next.11" + "@webex/internal-plugin-encryption": "npm:3.9.0-next.7" + "@webex/internal-plugin-user": "npm:3.9.0-next.6" + "@webex/webex-core": "npm:3.9.0-next.6" + crypto-js: "npm:^4.1.1" + lodash: "npm:^4.17.21" + node-scr: "npm:^0.3.0" + uuid: "npm:^3.3.2" + checksum: 10c0/318dc83c3add1f88bd56b6972f87b436a0a50372bc0a83b3700cd8a80ad205b87651b233c698e8a8140a33d288a2b6b08c03aac79c3335d0625afed944863c7d + languageName: node + linkType: hard + "@webex/internal-plugin-device@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-device@npm:2.60.2" @@ -10115,6 +10153,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-device@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/internal-plugin-device@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/common-timers": "npm:3.8.1-next.11" + "@webex/http-core": "npm:3.8.1-next.11" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.6" + "@webex/webex-core": "npm:3.9.0-next.6" + ampersand-collection: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/f8e14bbbc9ff6907e3d9ca8addc0b6d6ce18af666ddfb0c6820170d69d4d4459d264b0cd0c05708b7f034c734ac1a9414efc902199a87d5637aadbf7413a4d1b + languageName: node + linkType: hard + "@webex/internal-plugin-dss@npm:3.9.0-next.3": version: 3.9.0-next.3 resolution: "@webex/internal-plugin-dss@npm:3.9.0-next.3" @@ -10207,6 +10262,32 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-encryption@npm:3.9.0-next.7": + version: 3.9.0-next.7 + resolution: "@webex/internal-plugin-encryption@npm:3.9.0-next.7" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/common-timers": "npm:3.8.1-next.11" + "@webex/http-core": "npm:3.8.1-next.11" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/internal-plugin-mercury": "npm:3.9.0-next.6" + "@webex/test-helper-file": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + asn1js: "npm:^2.0.26" + debug: "npm:^4.3.4" + isomorphic-webcrypto: "npm:^2.3.8" + lodash: "npm:^4.17.21" + node-jose: "npm:^2.2.0" + node-kms: "npm:^0.4.1" + node-scr: "npm:^0.3.0" + pkijs: "npm:^2.1.84" + safe-buffer: "npm:^5.2.0" + uuid: "npm:^3.3.2" + valid-url: "npm:^1.0.9" + checksum: 10c0/b67d770fd8f51552c62b2fb583644b17d82f7a75e8b6add820e9779e35a999e2b4ddeecb9dcfde70fae7d99257f231c9372308bc63520fa1ced0ca1292340128 + languageName: node + linkType: hard + "@webex/internal-plugin-feature@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-feature@npm:2.60.2" @@ -10242,6 +10323,17 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-feature@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/internal-plugin-feature@npm:3.9.0-next.6" + dependencies: + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/webex-core": "npm:3.9.0-next.6" + lodash: "npm:^4.17.21" + checksum: 10c0/d492c82e6e9fb653a1ca29e0490c14b7fa53d63c49342351638e9ec7ec9bbb24a13f94083dd010245edabdbaf76a8e9205e430285b1e2136a13043d450ef2bec + languageName: node + linkType: hard + "@webex/internal-plugin-llm@npm:3.9.0-next.3": version: 3.9.0-next.3 resolution: "@webex/internal-plugin-llm@npm:3.9.0-next.3" @@ -10416,6 +10508,30 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-mercury@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/internal-plugin-mercury@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/common-timers": "npm:3.8.1-next.11" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/internal-plugin-feature": "npm:3.9.0-next.6" + "@webex/internal-plugin-metrics": "npm:3.9.0-next.6" + "@webex/test-helper-chai": "npm:3.8.1-next.11" + "@webex/test-helper-mocha": "npm:3.8.1-next.11" + "@webex/test-helper-mock-web-socket": "npm:3.8.1-next.11" + "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" + "@webex/test-helper-refresh-callback": "npm:3.8.1-next.11" + "@webex/test-helper-test-users": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + backoff: "npm:^2.5.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + ws: "npm:^8.17.1" + checksum: 10c0/6c51a808c0271c5f4c07f3e9552a983c6e4d6dd9eb52a066cb249be514ef868840293c39e898339c3d549c164ea582360e417e8583aa699107a5b75ec518146e + languageName: node + linkType: hard + "@webex/internal-plugin-metrics@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-metrics@npm:2.60.2" @@ -10459,6 +10575,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-metrics@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/internal-plugin-metrics@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/common-timers": "npm:3.8.1-next.11" + "@webex/event-dictionary-ts": "npm:^1.0.1930" + "@webex/test-helper-chai": "npm:3.8.1-next.11" + "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + ip-anonymize: "npm:^0.1.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/f369d59adfcbbe8572a0e073a15c5d24609dd8a233a79e84d3b599677609c48be136d999414fb4c4440c3caeed01a511840d5d682a34d13e91c65f624da3fb35 + languageName: node + linkType: hard + "@webex/internal-plugin-presence@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-presence@npm:2.60.2" @@ -10552,6 +10685,21 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-search@npm:3.9.0-next.7": + version: 3.9.0-next.7 + resolution: "@webex/internal-plugin-search@npm:3.9.0-next.7" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/internal-plugin-conversation": "npm:3.9.0-next.7" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/internal-plugin-encryption": "npm:3.9.0-next.7" + "@webex/webex-core": "npm:3.9.0-next.6" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/5751d2519b42346d6614c559c14623676449815fc1b84cf9bc3cab08486e83887f69dbdbc68abf34cd798d965adac92074d87f5e5762e03f9347dbc5876242a6 + languageName: node + linkType: hard + "@webex/internal-plugin-support@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-support@npm:2.60.2" @@ -10603,6 +10751,23 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-support@npm:3.9.0-next.8": + version: 3.9.0-next.8 + resolution: "@webex/internal-plugin-support@npm:3.9.0-next.8" + dependencies: + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/internal-plugin-search": "npm:3.9.0-next.7" + "@webex/test-helper-chai": "npm:3.8.1-next.11" + "@webex/test-helper-file": "npm:3.8.1-next.11" + "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" + "@webex/test-helper-test-users": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/9e19a2e7048283c5f123b4177fc31b878773e1486fc3222de09b45046f09a006428d690465fe6ad0a77a82af9cb0c4428b0a35df697751a9c8d7f2720b8ddecd + languageName: node + linkType: hard + "@webex/internal-plugin-user@npm:2.60.2": version: 2.60.2 resolution: "@webex/internal-plugin-user@npm:2.60.2" @@ -10651,6 +10816,22 @@ __metadata: languageName: node linkType: hard +"@webex/internal-plugin-user@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/internal-plugin-user@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/test-helper-chai": "npm:3.8.1-next.11" + "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" + "@webex/test-helper-test-users": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/4f07e7454be0405b5591ff16d58cb47f0e38adb2fffa93a5d15ed9ad34b94a969f84c791bd62506bbd560cb5543aad9670f32e9a3f0bfd81aec18d34b9e4fb98 + languageName: node + linkType: hard + "@webex/internal-plugin-voicea@npm:3.9.0-next.4": version: 3.9.0-next.4 resolution: "@webex/internal-plugin-voicea@npm:3.9.0-next.4" @@ -10706,6 +10887,17 @@ __metadata: languageName: node linkType: hard +"@webex/media-helpers@npm:3.9.0-next.2": + version: 3.9.0-next.2 + resolution: "@webex/media-helpers@npm:3.9.0-next.2" + dependencies: + "@webex/internal-media-core": "npm:2.20.0" + "@webex/ts-events": "npm:^1.1.0" + "@webex/web-media-effects": "npm:2.27.1" + checksum: 10c0/6c7ef33de0b9fe6583570ba880c8788e5f0488345f6e01425dced81a36e53c0cbc0d0007767838e192274ade2430beb84731f72618f9aa4abebc9766408ead70 + languageName: node + linkType: hard + "@webex/plugin-attachment-actions@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-attachment-actions@npm:2.60.2" @@ -10813,6 +11005,23 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-browser@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/plugin-authorization-browser@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/plugin-authorization-node": "npm:3.9.0-next.6" + "@webex/storage-adapter-local-storage": "npm:3.9.0-next.6" + "@webex/storage-adapter-spec": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + jsonwebtoken: "npm:^9.0.2" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/9e0fc18cb50ddac0036b716380d8396b1908ee02681fd990024fe4cb0a0d6dc9c4e95cec53f7494252ec9ce79e6b73f6a992ec5d49b3c982452721f778674d18 + languageName: node + linkType: hard + "@webex/plugin-authorization-node@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization-node@npm:2.60.2" @@ -10852,6 +11061,19 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization-node@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/plugin-authorization-node@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/internal-plugin-device": "npm:3.9.0-next.6" + "@webex/webex-core": "npm:3.9.0-next.6" + jsonwebtoken: "npm:^9.0.0" + uuid: "npm:^3.3.2" + checksum: 10c0/c5321da4bb59a3f8639723cbf148c21140ac3254f7b63eed2a4abb59325e67e8263a9ea7bd867886205f4f19fa2226ba9d5b8d653ae108f59b99b1505d4b9053 + languageName: node + linkType: hard + "@webex/plugin-authorization@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-authorization@npm:2.60.2" @@ -10882,6 +11104,16 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-authorization@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/plugin-authorization@npm:3.9.0-next.6" + dependencies: + "@webex/plugin-authorization-browser": "npm:3.9.0-next.6" + "@webex/plugin-authorization-node": "npm:3.9.0-next.6" + checksum: 10c0/f1bd479372752f95b00e3d271272cf1285df41da12a11103a9e102a6dbd0f39bd08eeb6ee5a120fb18c5aa79f18e2ed699d82ae61348b5b334221033be0f9fb5 + languageName: node + linkType: hard + "@webex/plugin-device-manager@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-device-manager@npm:2.60.2" @@ -10988,6 +11220,20 @@ __metadata: languageName: node linkType: hard +"@webex/plugin-logger@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/plugin-logger@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/test-helper-chai": "npm:3.8.1-next.11" + "@webex/test-helper-mocha": "npm:3.8.1-next.11" + "@webex/test-helper-mock-webex": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + lodash: "npm:^4.17.21" + checksum: 10c0/319754d0f9dedb20f5e8333e22ec1bc5ca1bb61fe264c641603ec15117bacfb1ed5999e25af26719d787d3aea3753717843015d4ab02396823cf161dbc2a10c2 + languageName: node + linkType: hard + "@webex/plugin-meetings@npm:2.60.2": version: 2.60.2 resolution: "@webex/plugin-meetings@npm:2.60.2" @@ -11456,6 +11702,17 @@ __metadata: languageName: node linkType: hard +"@webex/storage-adapter-local-storage@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/storage-adapter-local-storage@npm:3.9.0-next.6" + dependencies: + "@webex/storage-adapter-spec": "npm:3.8.1-next.11" + "@webex/test-helper-mocha": "npm:3.8.1-next.11" + "@webex/webex-core": "npm:3.9.0-next.6" + checksum: 10c0/bac8eaf02f8f657e78604e5591f48c36e32ba3458d4ab4a51a6049b78dde76fc59682020236722ba6abe53b92d73a0f2714554cdf635d0041e6af68cc684e05e + languageName: node + linkType: hard + "@webex/storage-adapter-spec@npm:2.60.2": version: 2.60.2 resolution: "@webex/storage-adapter-spec@npm:2.60.2" @@ -12008,6 +12265,26 @@ __metadata: languageName: node linkType: hard +"@webex/webex-core@npm:3.9.0-next.6": + version: 3.9.0-next.6 + resolution: "@webex/webex-core@npm:3.9.0-next.6" + dependencies: + "@webex/common": "npm:3.8.1-next.11" + "@webex/common-timers": "npm:3.8.1-next.11" + "@webex/http-core": "npm:3.8.1-next.11" + "@webex/storage-adapter-spec": "npm:3.8.1-next.11" + ampersand-collection: "npm:^2.0.2" + ampersand-events: "npm:^2.0.2" + ampersand-state: "npm:^5.0.3" + core-decorators: "npm:^0.20.0" + crypto-js: "npm:^4.1.1" + jsonwebtoken: "npm:^9.0.0" + lodash: "npm:^4.17.21" + uuid: "npm:^3.3.2" + checksum: 10c0/0cd89491e65b98f8cc8e714da7d19c58132e5d4802892e48b409fb2e42b2674d8d931f5b76105911eed49896be408091660792d608a60d1b44caab781f48ab3f + languageName: node + linkType: hard + "@webex/webrtc-core@npm:2.13.3": version: 2.13.3 resolution: "@webex/webrtc-core@npm:2.13.3" From cdee5585517f2e9308b44fa9e2fcf271da631e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Thu, 23 Oct 2025 13:31:37 +0530 Subject: [PATCH 16/48] fix(tests): add missing outdialANIId property to agentConfig in test mocks --- packages/contact-center/task/tests/helper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index b8fb045f5..28640a66a 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -2883,7 +2883,7 @@ describe('useCallControl', () => { }, }; - store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + store.cc.agentConfig = {agentId: 'agent1', regexUS: '', outdialANIId: ''}; const {result} = renderHook(() => useCallControl({ @@ -2926,7 +2926,7 @@ describe('useCallControl', () => { }, }; - store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + store.cc.agentConfig = {agentId: 'agent1', regexUS: '', outdialANIId: ''}; const {result} = renderHook(() => useCallControl({ @@ -2967,7 +2967,7 @@ describe('useCallControl', () => { }, }; - store.cc.agentConfig = {agentId: 'agent1', regexUS: ''}; + store.cc.agentConfig = {agentId: 'agent1', regexUS: '', outdialANIId: ''}; const {result} = renderHook(() => useCallControl({ From a71c589a62169cce87129ac71d63dac281cf344e Mon Sep 17 00:00:00 2001 From: Ravi Chandra Sekhar Sarika Date: Mon, 27 Oct 2025 20:51:42 +0530 Subject: [PATCH 17/48] fix: refactor visibility to avoid flags incase of consult --- .../call-control-consult.tsx | 19 +- .../call-control-custom.utils.ts | 33 +- .../task/CallControl/call-control.tsx | 34 +- .../task/CallControl/call-control.utils.ts | 49 +- .../task/CallControlCAD/call-control-cad.tsx | 16 +- .../src/components/task/task.types.ts | 81 +- .../call-control-consult.snapshot.tsx.snap | 297 ++-- .../call-control-consult.snapshot.tsx | 87 +- .../call-control-consult.tsx | 47 +- .../call-control-custom.util.tsx | 102 +- .../call-control.snapshot.tsx.snap | 1061 +++++++++----- .../CallControl/call-control.snapshot.tsx | 63 +- .../task/CallControl/call-control.tsx | 44 +- .../task/CallControl/call-control.utils.tsx | 134 +- .../call-control-cad.snapshot.tsx.snap | 1219 +++++++++++------ .../call-control-cad.snapshot.tsx | 70 +- .../task/CallControlCAD/call-control-cad.tsx | 59 +- .../store/src/storeEventsWrapper.ts | 12 +- .../store/tests/storeEventsWrapper.ts | 6 +- .../task/src/CallControl/index.tsx | 11 +- .../task/src/CallControlCAD/index.tsx | 11 +- .../task/src/Utils/task-util.ts | 515 ++++++- packages/contact-center/task/src/helper.ts | 48 +- .../contact-center/task/src/task.types.ts | 8 +- .../task/tests/CallControl/index.tsx | 38 +- .../task/tests/CallControlCAD/index.tsx | 146 +- packages/contact-center/task/tests/helper.ts | 197 ++- .../task/tests/utils/task-util.ts | 288 ++-- .../test-fixtures/src/fixtures.ts | 31 +- 29 files changed, 2920 insertions(+), 1806 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx index 30bfdf175..5a42c5d6a 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import {ButtonCircle, TooltipNext, Text} from '@momentum-ui/react-collaboration'; import {Avatar, Icon} from '@momentum-design/components/dist/react'; import TaskTimer from '../../TaskTimer'; @@ -20,16 +20,11 @@ const CallControlConsultComponent: React.FC = onTransfer, endConsultCall, consultConference, - consultCompleted, - isAgentBeingConsulted, - isEndConsultEnabled, logger, - muteUnmute, isMuted, + controlVisibility, onToggleConsultMute, }) => { - const [isMuteDisabled, setIsMuteDisabled] = useState(false); - const timerKey = createTimerKey(startTimeStamp); const handleTransfer = () => { @@ -41,7 +36,7 @@ const CallControlConsultComponent: React.FC = }; const handleConsultMuteToggle = () => { - handleMuteToggle(onToggleConsultMute, setIsMuteDisabled, logger); + handleMuteToggle(onToggleConsultMute, logger); }; const handleConsultConference = () => { @@ -50,11 +45,7 @@ const CallControlConsultComponent: React.FC = const buttons = createConsultButtons( isMuted, - isMuteDisabled, - consultCompleted, - isAgentBeingConsulted, - isEndConsultEnabled, - muteUnmute, + controlVisibility, onTransfer ? handleTransfer : undefined, handleConsultMuteToggle, handleEndConsult, @@ -73,7 +64,7 @@ const CallControlConsultComponent: React.FC = {agentName} - {getConsultStatusText(consultCompleted)} •  + {getConsultStatusText(controlVisibility.isConsultInitiatedOrAccepted)} •  diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts index 18d136d20..0890c94aa 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-custom.utils.ts @@ -1,5 +1,6 @@ import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; +import {ControlVisibility} from '../../task.types'; /** * Interface for button configuration @@ -27,11 +28,7 @@ export interface ListItemData { */ export const createConsultButtons = ( isMuted: boolean, - isMuteDisabled: boolean, - consultCompleted: boolean, - isAgentBeingConsulted: boolean, - isEndConsultEnabled: boolean, - muteUnmute: boolean, + controlVisibility: ControlVisibility, onTransfer?: () => void, handleConsultMuteToggle?: () => void, handleEndConsult?: () => void, @@ -46,8 +43,8 @@ export const createConsultButtons = ( onClick: handleConsultMuteToggle || (() => {}), tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: isMuteDisabled, - shouldShow: muteUnmute, + disabled: !controlVisibility.muteUnmute.isEnabled, + shouldShow: controlVisibility.muteUnmute.isVisible, }, { key: 'transfer', @@ -55,8 +52,8 @@ export const createConsultButtons = ( tooltip: 'Transfer Consult', onClick: onTransfer || (() => {}), className: 'call-control-button', - disabled: !consultCompleted, - shouldShow: isAgentBeingConsulted && !!onTransfer, + disabled: !controlVisibility.consultTransfer.isEnabled, + shouldShow: controlVisibility.consultTransfer.isVisible && !!onTransfer, }, { key: 'conference', @@ -64,8 +61,8 @@ export const createConsultButtons = ( tooltip: 'Consult Conference', onClick: handleConsultConferencePress || (() => {}), className: 'call-control-button', - disabled: !consultCompleted, - shouldShow: isAgentBeingConsulted && !!handleConsultConferencePress, + disabled: !controlVisibility.mergeConference.isEnabled, + shouldShow: controlVisibility.mergeConference.isVisible && !!handleConsultConferencePress, }, { key: 'cancel', @@ -73,7 +70,7 @@ export const createConsultButtons = ( tooltip: 'End Consult', onClick: handleEndConsult || (() => {}), className: 'call-control-consult-button-cancel', - shouldShow: isEndConsultEnabled || isAgentBeingConsulted, + shouldShow: controlVisibility.endConsult.isVisible && !!handleEndConsult, }, ]; } catch (error) { @@ -195,13 +192,7 @@ export const handleConsultConferencePress = (consultConference: (() => void) | u /** * Handles mute toggle with disabled state management */ -export const handleMuteToggle = ( - onToggleConsultMute: (() => void) | undefined, - setIsMuteDisabled: (disabled: boolean) => void, - logger: ILogger -): void => { - setIsMuteDisabled(true); - +export const handleMuteToggle = (onToggleConsultMute: (() => void) | undefined, logger: ILogger): void => { try { if (onToggleConsultMute) { onToggleConsultMute(); @@ -213,9 +204,7 @@ export const handleMuteToggle = ( }); } finally { // Re-enable button after operation - setTimeout(() => { - setIsMuteDisabled(false); - }, 500); + setTimeout(() => {}, 500); } }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx index 3edaebd6e..d5b49805e 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx @@ -42,8 +42,6 @@ function CallControlComponent(props: CallControlComponentProps) { endCall, wrapupCall, wrapupCodes, - isHeld, - setIsHeld, isRecording, setIsRecording, buddyAgents, @@ -51,18 +49,14 @@ function CallControlComponent(props: CallControlComponentProps) { transferCall, consultCall, exitConference, - consultInitiated, - consultAccepted, callControlAudio, setConsultAgentName, - setConsultAgentId, allowConsultToQueue, setLastTargetType, controlVisibility, logger, secondsUntilAutoWrapup, cancelAutoWrapup, - isConsultButtonDisabled, getAddressBookEntries, getEntryPoints, getQueuesFetcher, @@ -70,11 +64,11 @@ function CallControlComponent(props: CallControlComponentProps) { } = props; useEffect(() => { - updateCallStateFromTask(currentTask, setIsHeld, setIsRecording, logger); + updateCallStateFromTask(currentTask, setIsRecording, logger); }, [currentTask, logger]); const handletoggleHold = () => { - handleToggleHoldUtil(isHeld, toggleHold, setIsHeld, logger); + handleToggleHoldUtil(controlVisibility.isHeld, toggleHold, logger); }; const handleMuteToggle = () => { @@ -110,7 +104,6 @@ function CallControlComponent(props: CallControlComponentProps) { agentMenuType, consultCall, transferCall, - setConsultAgentId, setConsultAgentName, setLastTargetType, logger @@ -128,10 +121,8 @@ function CallControlComponent(props: CallControlComponentProps) { const buttons = buildCallControlButtons( isMuted, - isHeld, isRecording, isMuteButtonDisabled, - isConsultButtonDisabled, currentMediaType, controlVisibility, handleMuteToggle, @@ -142,7 +133,12 @@ function CallControlComponent(props: CallControlComponentProps) { logger ); - const filteredButtons = filterButtonsForConsultation(buttons, consultInitiated, isTelephony, logger); + const filteredButtons = filterButtonsForConsultation( + buttons, + controlVisibility.isConsultInitiatedOrAccepted, + isTelephony, + logger + ); if (!currentTask) return null; @@ -154,7 +150,7 @@ function CallControlComponent(props: CallControlComponentProps) { autoPlay >
    - {!(consultAccepted && isTelephony) && !controlVisibility.wrapup && ( + {!controlVisibility.hideCallControls && !controlVisibility.wrapup.isVisible && (
    {filteredButtons.map((button, index) => { if (!button.isVisible) return null; @@ -198,7 +194,9 @@ function CallControlComponent(props: CallControlComponentProps) { @@ -259,11 +257,13 @@ function CallControlComponent(props: CallControlComponentProps) { @@ -282,7 +282,7 @@ function CallControlComponent(props: CallControlComponentProps) { })}
    )} - {controlVisibility.wrapup && ( + {controlVisibility.wrapup.isVisible && (
    void, - setIsHeld: (held: boolean) => void, - logger: ILogger -): void => { +export const handleToggleHold = (isHeld: boolean, toggleHold: (hold: boolean) => void, logger: ILogger): void => { try { logger.info(`CC-Widgets: CallControl: is Call On Hold status is ${isHeld}`, { module: 'call-control.tsx', method: 'handletoggleHold', }); toggleHold(!isHeld); - setIsHeld(!isHeld); } catch (error) { logger?.error(`CC-Widgets: CallControl: Error in handleToggleHold - ${error.message}`); } @@ -124,7 +118,6 @@ export const handleTargetSelect = ( agentMenuType: CallControlMenuType | null, consultCall: (id: string, type: DestinationType, allowParticipantsToInteract: boolean) => void, transferCall: (id: string, type: DestinationType) => void, - setConsultAgentId: (id: string) => void, setConsultAgentName: (name: string) => void, setLastTargetType: (type: DestinationType) => void, logger: ILogger @@ -136,7 +129,6 @@ export const handleTargetSelect = ( if (agentMenuType === 'Consult') { try { consultCall(id, type, allowParticipantsToInteract); - setConsultAgentId(id); setConsultAgentName(name); setLastTargetType(type); } catch (error) { @@ -199,10 +191,8 @@ export const isTelephonyMediaType = (mediaType: MediaChannelType, logger?): bool */ export const buildCallControlButtons = ( isMuted: boolean, - isHeld: boolean, isRecording: boolean, isMuteButtonDisabled: boolean, - isConsultButtonDisabled: boolean, currentMediaType: MediaTypeInfo, controlVisibility: ControlVisibility, handleMuteToggleFunc: () => void, @@ -221,17 +211,17 @@ export const buildCallControlButtons = ( tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, disabled: isMuteButtonDisabled, - isVisible: controlVisibility.muteUnmute, + isVisible: controlVisibility.muteUnmute.isVisible, dataTestId: 'call-control:mute-toggle', }, { id: 'hold', - icon: isHeld ? 'play-bold' : 'pause-bold', + icon: controlVisibility.isHeld ? 'play-bold' : 'pause-bold', onClick: handleToggleHoldFunc, - tooltip: isHeld ? RESUME_CALL : HOLD_CALL, + tooltip: controlVisibility.isHeld ? RESUME_CALL : HOLD_CALL, className: 'call-control-button', - disabled: controlVisibility.isConferenceInProgress, - isVisible: controlVisibility.holdResume, + disabled: !controlVisibility.holdResume.isEnabled, + isVisible: controlVisibility.holdResume.isVisible, dataTestId: 'call-control:hold-toggle', }, { @@ -239,9 +229,9 @@ export const buildCallControlButtons = ( icon: 'headset-bold', tooltip: CONSULT_AGENT, className: 'call-control-button', - disabled: isConsultButtonDisabled, + disabled: !controlVisibility.consult.isEnabled, menuType: 'Consult', - isVisible: controlVisibility.consult, + isVisible: controlVisibility.consult.isVisible, dataTestId: 'call-control:consult', }, { @@ -250,8 +240,8 @@ export const buildCallControlButtons = ( tooltip: 'Exit Conference', className: 'call-control-button-muted', onClick: exitConference, - disabled: !controlVisibility.isConferenceInProgress, - isVisible: controlVisibility.isConferenceInProgress, + disabled: !controlVisibility.exitConference.isEnabled, + isVisible: controlVisibility.exitConference.isVisible, dataTestId: 'call-control:exit-conference', }, { @@ -259,9 +249,9 @@ export const buildCallControlButtons = ( icon: 'next-bold', tooltip: `${TRANSFER} ${currentMediaType.labelName}`, className: 'call-control-button', - disabled: false, + disabled: !controlVisibility.transfer.isEnabled, menuType: 'Transfer', - isVisible: controlVisibility.transfer, + isVisible: controlVisibility.transfer.isVisible, dataTestId: 'call-control:transfer', }, { @@ -270,8 +260,8 @@ export const buildCallControlButtons = ( onClick: toggleRecording, tooltip: isRecording ? PAUSE_RECORDING : RESUME_RECORDING, className: 'call-control-button', - disabled: false, - isVisible: controlVisibility.pauseResumeRecording, + disabled: !controlVisibility.pauseResumeRecording.isEnabled, + isVisible: controlVisibility.pauseResumeRecording.isVisible, dataTestId: 'call-control:recording-toggle', }, { @@ -280,8 +270,8 @@ export const buildCallControlButtons = ( onClick: endCall, tooltip: `${END} ${currentMediaType.labelName}`, className: 'call-control-button-cancel', - disabled: isHeld, - isVisible: controlVisibility.end, + disabled: !controlVisibility.end.isEnabled, + isVisible: controlVisibility.end.isVisible, dataTestId: 'call-control:end-call', }, ]; @@ -325,17 +315,14 @@ export const filterButtonsForConsultation = ( */ export const updateCallStateFromTask = ( currentTask: ITask, - setIsHeld: (held: boolean) => void, setIsRecording: (recording: boolean) => void, logger? ): void => { try { if (!currentTask || !currentTask.data || !currentTask.data.interaction) return; - const {interaction, mediaResourceId} = currentTask.data; - const {media, callProcessingDetails} = interaction; - const isHold = media && media[mediaResourceId] && media[mediaResourceId].isHold; - setIsHeld(isHold); + const {interaction} = currentTask.data; + const {callProcessingDetails} = interaction; if (callProcessingDetails) { const {isPaused} = callProcessingDetails; diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index f688d1843..69fce552e 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -25,21 +25,16 @@ import {withMetrics} from '@webex/cc-ui-logging'; const CallControlCADComponent: React.FC = (props) => { const { currentTask, - isHeld, isRecording, holdTime, - consultAccepted, - consultInitiated, consultAgentName, consultStartTimeStamp, endConsultCall, - consultCompleted, consultTransfer, consultConference, callControlClassName, callControlConsultClassName, startTimestamp, - isEndConsultEnabled, controlVisibility, logger, isMuted, @@ -223,7 +218,7 @@ const CallControlCADComponent: React.FC = (props) => )}
    - {!controlVisibility.wrapup && isHeld && ( + {!controlVisibility.wrapup.isVisible && controlVisibility.isHeld && ( <> •
    @@ -238,7 +233,7 @@ const CallControlCADComponent: React.FC = (props) =>
    - {!controlVisibility.wrapup && controlVisibility.recordingIndicator && ( + {!controlVisibility.wrapup.isVisible && controlVisibility.recordingIndicator.isVisible && (
    @@ -268,7 +263,7 @@ const CallControlCADComponent: React.FC = (props) => - {(consultAccepted || consultInitiated) && !controlVisibility.wrapup && isTelephony && ( + {controlVisibility.isConsultInitiatedOrAccepted && (
    = (props) => endConsultCall={endConsultCall} onTransfer={consultTransfer} consultConference={consultConference} - consultCompleted={consultCompleted} - isAgentBeingConsulted={!consultAccepted} - isEndConsultEnabled={isEndConsultEnabled} logger={logger} - muteUnmute={controlVisibility.muteUnmute} isMuted={isMuted} + controlVisibility={controlVisibility} onToggleConsultMute={toggleMute} />
    diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index cc3e025cd..672171c83 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -339,17 +339,6 @@ export interface ControlProps { */ callControlAudio: MediaStream | null; - /** - * ID of the consulting agent - */ - consultAgentId: string; - - /** - * Function to set the consulting agent ID - * @param agentId - The ID of the consulting agent. - */ - setConsultAgentId: (agentId: string) => void; - /** * Name of the consulting agent. */ @@ -421,21 +410,7 @@ export interface ControlProps { */ setLastTargetType: (targetType: 'queue' | 'agent') => void; - controlVisibility: { - accept: boolean; - decline: boolean; - end: boolean; - muteUnmute: boolean; - holdResume: boolean; - consult: boolean; - transfer: boolean; - conference: boolean; - wrapup: boolean; - pauseResumeRecording: boolean; - endConsult: boolean; - recordingIndicator: boolean; - isConferenceInProgress: boolean; - }; + controlVisibility: ControlVisibility; secondsUntilAutoWrapup?: number; @@ -466,6 +441,11 @@ export interface ControlProps { * Options to configure consult/transfer popover behavior. */ consultTransferOptions?: ConsultTransferOptions; + + /** + * Agent ID of the logged-in user + */ + agentId: string; } export type CallControlComponentProps = Pick< @@ -478,8 +458,6 @@ export type CallControlComponentProps = Pick< | 'isMuted' | 'endCall' | 'wrapupCall' - | 'isHeld' - | 'setIsHeld' | 'isRecording' | 'setIsRecording' | 'buddyAgents' @@ -489,23 +467,15 @@ export type CallControlComponentProps = Pick< | 'consultConference' | 'exitConference' | 'endConsultCall' - | 'consultInitiated' | 'consultTransfer' - | 'consultCompleted' - | 'consultAccepted' | 'consultStartTimeStamp' | 'callControlAudio' | 'consultAgentName' | 'setConsultAgentName' - | 'consultAgentId' - | 'setConsultAgentId' | 'holdTime' | 'callControlClassName' | 'callControlConsultClassName' | 'startTimestamp' - | 'queues' - | 'loadQueues' - | 'isEndConsultEnabled' | 'allowConsultToQueue' | 'lastTargetType' | 'setLastTargetType' @@ -617,12 +587,9 @@ export interface CallControlConsultComponentsProps { onTransfer?: () => void; endConsultCall?: () => void; consultConference?: () => void; - consultCompleted: boolean; - isAgentBeingConsulted: boolean; - isEndConsultEnabled: boolean; logger: ILogger; - muteUnmute: boolean; isMuted: boolean; + controlVisibility: ControlVisibility; onToggleConsultMute?: () => void; } @@ -670,20 +637,30 @@ export interface CallControlButton { dataTestId?: string; } +export type Visibility = { + isVisible: boolean; + isEnabled: boolean; +}; export interface ControlVisibility { - accept: boolean; - decline: boolean; - end: boolean; - muteUnmute: boolean; - holdResume: boolean; - consult: boolean; - transfer: boolean; - conference: boolean; - wrapup: boolean; - pauseResumeRecording: boolean; - endConsult: boolean; - recordingIndicator: boolean; + accept: Visibility; + decline: Visibility; + end: Visibility; + muteUnmute: Visibility; + holdResume: Visibility; + consult: Visibility; + transfer: Visibility; + conference: Visibility; + wrapup: Visibility; + pauseResumeRecording: Visibility; + endConsult: Visibility; + recordingIndicator: Visibility; + exitConference: Visibility; + mergeConference: Visibility; + consultTransfer: Visibility; isConferenceInProgress: boolean; + isConsultInitiatedOrAccepted: boolean; + hideCallControls: boolean; + isHeld: boolean; } export interface MediaTypeInfo { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap index 6619e357a..e3cc35b47 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/__snapshots__/call-control-consult.snapshot.tsx.snap @@ -24,7 +24,7 @@ exports[`CallControlConsultComponent Snapshots Interactions should call mockEndC tagname="p" type="body-midsize-regular" > - Consulting + Consult requested  •Â