diff --git a/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx b/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx index 80b0ecd3c..dac2e10c9 100644 --- a/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx +++ b/packages/contact-center/cc-components/src/components/StationLogin/station-login.utils.tsx @@ -1,6 +1,6 @@ import {useRef} from 'react'; import {StationLoginLabels} from './constants'; -import {LoginOptions, DESKTOP, DIALNUMBER} from '@webex/cc-store'; +import {LoginOptions, DESKTOP, DIAL_NUMBER} from '@webex/cc-store'; const handleModals = ( modalRef, @@ -270,7 +270,7 @@ const handleDNInputChanged = ( // show error for empty string setDNErrorText(`${LoginOptions[selectedDeviceType]} ${StationLoginLabels.IS_REQUIRED}`); setShowDNError(true); - } else if (selectedDeviceType === DIALNUMBER) { + } else if (selectedDeviceType === DIAL_NUMBER) { setShowDNError(validateDialNumber(input, dialNumberRegex, setDNErrorText, logger)); } else { setShowDNError(false); 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..1d1ef829e 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'; @@ -6,9 +6,6 @@ import {CallControlConsultComponentsProps} from '../../task.types'; import { createConsultButtons, getVisibleButtons, - handleTransferPress, - handleEndConsultPress, - handleMuteToggle, getConsultStatusText, createTimerKey, } from './call-control-custom.utils'; @@ -16,42 +13,26 @@ import { const CallControlConsultComponent: React.FC = ({ agentName, startTimeStamp, - onTransfer, + consultTransfer, endConsultCall, - consultCompleted, - isAgentBeingConsulted, - isEndConsultEnabled, + consultConference, + switchToMainCall, logger, - muteUnmute, isMuted, - onToggleConsultMute, + controlVisibility, + toggleConsultMute, }) => { - const [isMuteDisabled, setIsMuteDisabled] = useState(false); - const timerKey = createTimerKey(startTimeStamp); - const handleTransfer = () => { - handleTransferPress(onTransfer, logger); - }; - - const handleEndConsult = () => { - handleEndConsultPress(endConsultCall, logger); - }; - - const handleConsultMuteToggle = () => { - handleMuteToggle(onToggleConsultMute, setIsMuteDisabled, logger); - }; - const buttons = createConsultButtons( isMuted, - isMuteDisabled, - consultCompleted, - isAgentBeingConsulted, - isEndConsultEnabled, - muteUnmute, - onTransfer ? handleTransfer : undefined, - handleConsultMuteToggle, - handleEndConsult + controlVisibility, + consultTransfer, + toggleConsultMute, + endConsultCall, + consultConference, + switchToMainCall, + logger ); // Filter buttons that should be shown, then map them @@ -66,7 +47,7 @@ const CallControlConsultComponent: React.FC = {agentName} - {getConsultStatusText(consultCompleted)} •  + {getConsultStatusText(controlVisibility.isConsultInitiated)} •  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 3c0edccbf..3180414db 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,18 +1,6 @@ import {BuddyDetails, ContactServiceQueue, ILogger} from '@webex/cc-store'; import {MUTE_CALL, UNMUTE_CALL} from '../../constants'; - -/** - * Interface for button configuration - */ -export interface ButtonConfig { - key: string; - icon: string; - onClick: () => void; - tooltip: string; - className: string; - disabled?: boolean; - shouldShow: boolean; -} +import {ButtonConfig, ControlVisibility} from '../../task.types'; /** * Interface for list item data @@ -27,14 +15,12 @@ export interface ListItemData { */ export const createConsultButtons = ( isMuted: boolean, - isMuteDisabled: boolean, - consultCompleted: boolean, - isAgentBeingConsulted: boolean, - isEndConsultEnabled: boolean, - muteUnmute: boolean, - onTransfer?: () => void, - handleConsultMuteToggle?: () => void, - handleEndConsult?: () => void, + controlVisibility: ControlVisibility, + consultTransfer: () => void, + toggleConsultMute: () => void, + endConsultCall: () => void, + consultConference: () => void, + switchToMainCall: () => void, logger? ): ButtonConfig[] => { try { @@ -42,28 +28,46 @@ export const createConsultButtons = ( { key: 'mute', icon: isMuted ? 'microphone-muted-bold' : 'microphone-bold', - onClick: handleConsultMuteToggle || (() => {}), + onClick: toggleConsultMute, tooltip: isMuted ? UNMUTE_CALL : MUTE_CALL, className: `${isMuted ? 'call-control-button-muted' : 'call-control-button'}`, - disabled: isMuteDisabled, - shouldShow: muteUnmute, + disabled: !controlVisibility.muteUnmuteConsult.isEnabled, + isVisible: controlVisibility.muteUnmuteConsult.isVisible, + }, + { + key: 'switchToMainCall', + icon: 'call-swap-bold', + tooltip: controlVisibility.isConferenceInProgress ? 'Switch to Conference Call' : 'Switch to Call', + onClick: switchToMainCall, + className: 'call-control-button', + disabled: !controlVisibility.switchToMainCall.isEnabled, + isVisible: controlVisibility.switchToMainCall.isVisible, }, { key: 'transfer', icon: 'next-bold', - tooltip: 'Transfer Consult', - onClick: onTransfer || (() => {}), + tooltip: controlVisibility.isConferenceInProgress ? 'Transfer Conference' : 'Transfer', + onClick: consultTransfer, className: 'call-control-button', - disabled: !consultCompleted, - shouldShow: isAgentBeingConsulted && !!onTransfer, + disabled: !controlVisibility.consultTransferConsult.isEnabled, + isVisible: controlVisibility.consultTransferConsult.isVisible, + }, + { + key: 'conference', + icon: 'call-merge-bold', + tooltip: 'Merge', + onClick: consultConference, + className: 'call-control-button', + disabled: !controlVisibility.mergeConferenceConsult.isEnabled, + isVisible: controlVisibility.mergeConferenceConsult.isVisible, }, { key: 'cancel', icon: 'headset-muted-bold', tooltip: 'End Consult', - onClick: handleEndConsult || (() => {}), + onClick: endConsultCall, className: 'call-control-consult-button-cancel', - shouldShow: isEndConsultEnabled || isAgentBeingConsulted, + isVisible: controlVisibility.endConsult.isVisible, }, ]; } catch (error) { @@ -82,7 +86,7 @@ export const createConsultButtons = ( */ export const getVisibleButtons = (buttons: ButtonConfig[], logger?): ButtonConfig[] => { try { - return buttons.filter((button) => button.shouldShow); + return buttons.filter((button) => button.isVisible); } catch (error) { logger?.error('CC-Widgets: CallControlCustom: Error in getVisibleButtons', { module: 'cc-components#call-control-custom.utils.ts', @@ -116,83 +120,12 @@ export const createInitials = (name: string, logger?): string => { } }; -/** - * Handles transfer button press with logging - */ -export const handleTransferPress = (onTransfer: (() => void) | undefined, logger: ILogger): void => { - logger.info('CC-Widgets: CallControlConsult: transfer button clicked', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - - try { - if (onTransfer) { - onTransfer(); - logger.log('CC-Widgets: CallControlConsult: transfer completed', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - } - } catch (error) { - throw new Error(`Error transferring call: ${error}`); - } -}; - -/** - * Handles end consult button press with logging - */ -export const handleEndConsultPress = (endConsultCall: (() => void) | undefined, logger: ILogger): void => { - logger.info('CC-Widgets: CallControlConsult: end consult clicked', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - - try { - if (endConsultCall) { - endConsultCall(); - logger.log('CC-Widgets: CallControlConsult: end consult completed', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - } - } catch (error) { - throw new Error(`Error ending consult call: ${error}`); - } -}; - -/** - * Handles mute toggle with disabled state management - */ -export const handleMuteToggle = ( - onToggleConsultMute: (() => void) | undefined, - setIsMuteDisabled: (disabled: boolean) => void, - logger: ILogger -): void => { - setIsMuteDisabled(true); - - try { - if (onToggleConsultMute) { - onToggleConsultMute(); - } - } catch (error) { - logger.error(`Mute toggle failed: ${error}`, { - module: 'call-control-consult.tsx', - method: 'handleConsultMuteToggle', - }); - } finally { - // Re-enable button after operation - setTimeout(() => { - setIsMuteDisabled(false); - }, 500); - } -}; - /** * Gets the consult status text based on completion state */ -export const getConsultStatusText = (consultCompleted: boolean, logger?): string => { +export const getConsultStatusText = (consultInitiated: boolean, logger?): string => { try { - return consultCompleted ? 'Consulting' : 'Consult requested'; + return consultInitiated ? 'Consult requested' : 'Consulting'; } catch (error) { logger?.error('CC-Widgets: CallControlCustom: Error in getConsultStatusText', { module: 'cc-components#call-control-custom.utils.ts', @@ -292,7 +225,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 { @@ -301,7 +235,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}`, { @@ -317,7 +251,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 { @@ -326,7 +261,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}`, { @@ -684,10 +619,11 @@ export const debounce = unknown>( export const shouldAddConsultTransferAction = ( selectedCategory: string, isEntryPointTabVisible: boolean, + allowParticipantsToInteract: boolean, query: string, entryPoints: {id: string; name: string}[], - onDialNumberSelect: ((dialNumber: string) => void) | undefined, - onEntryPointSelect: ((entryPointId: string, entryPointName: string) => void) | undefined + onDialNumberSelect: (dialNumber: string, allowParticipantsToInteract: boolean) => void, + onEntryPointSelect: (entryPointId: string, entryPointName: string, allowParticipantsToInteract: boolean) => void ): {visible: boolean; onClick?: () => void; title?: string} => { const DN_REGEX = new RegExp('^[+1][0-9]{3,18}$|^[*#:][+1][0-9*#:]{3,18}$|^[0-9*#:]{3,18}$'); @@ -697,14 +633,18 @@ export const shouldAddConsultTransferAction = ( if (isDial) { return valid && onDialNumberSelect - ? {visible: true, onClick: () => onDialNumberSelect(query), title: query} + ? {visible: true, onClick: () => onDialNumberSelect(query, allowParticipantsToInteract), title: query} : {visible: false}; } if (isEntry) { const match = query ? entryPoints?.find((e) => e.name === query || e.id === query) : null; return valid && match && onEntryPointSelect - ? {visible: true, onClick: () => onEntryPointSelect(match.id, match.name), title: match.name} + ? { + visible: true, + onClick: () => onEntryPointSelect(match.id, match.name, allowParticipantsToInteract), + title: match.name, + } : {visible: false}; } 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 dd43968cf..822aba8ba 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,6 +1,6 @@ -import React from 'react'; +import React, {useState} from 'react'; import {Text, ListNext, TextInput, Button, ButtonCircle, TooltipNext} from '@momentum-ui/react-collaboration'; -import {Icon} from '@momentum-design/components/dist/react'; +import {Icon, Checkbox} from '@momentum-design/components/dist/react'; import ConsultTransferListComponent from './consult-transfer-list-item'; import {ConsultTransferPopoverComponentProps} from '../../task.types'; import ConsultTransferEmptyState from './consult-transfer-empty-state'; @@ -11,6 +11,7 @@ import { getAgentsForDisplay, } from './call-control-custom.utils'; import {useConsultTransferPopover} from './consult-transfer-popover-hooks'; + import { SEARCH_PLACEHOLDER, CLEAR_SEARCH, @@ -35,6 +36,7 @@ const ConsultTransferPopoverComponent: React.FC { const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; @@ -65,7 +67,7 @@ const ConsultTransferPopoverComponent: React.FC(false); const renderList = ( items: T[], onButtonPress: (item: T) => void @@ -97,6 +99,7 @@ const ConsultTransferPopoverComponent: React.FC - {selectedCategory === 'Agents' && - (getAgentsForDisplay(selectedCategory, buddyAgents, searchQuery).length === 0 ? ( - - ) : ( - renderList( - getAgentsForDisplay(selectedCategory, buddyAgents, searchQuery).map((agent) => ({ - id: agent.agentId, - name: agent.agentName, - })), - (item) => handleAgentSelection(item.id, item.name, onAgentSelect, logger) - ) - ))} +
+ {selectedCategory === 'Agents' && + (getAgentsForDisplay(selectedCategory, buddyAgents, searchQuery).length === 0 ? ( + + ) : ( + renderList( + getAgentsForDisplay(selectedCategory, buddyAgents, searchQuery).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, onQueueSelect, logger) - )} - {hasMoreQueues && ( -
- {loadingQueues ? ( - - {LOADING_MORE_QUEUES} - - ) : ( - - {SCROLL_TO_LOAD_MORE} - - )} -
- )} -
- ))} + {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 === CATEGORY_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); + {showDialNumberTab && + selectedCategory === CATEGORY_DIAL_NUMBER && + (noDialNumbers ? ( + + ) : ( +
+ {renderList( + dialNumbers.map((d) => ({id: d.id, name: d.name, number: d.number})), + (item) => { + if (item.number) { + onDialNumberSelect(item.number, allowParticipantsToInteract); } } - } - )} - {hasMoreDialNumbers && ( -
- {loadingDialNumbers ? ( - - {LOADING_MORE_DIAL_NUMBERS} - - ) : ( - - {SCROLL_TO_LOAD_MORE} - - )} -
- )} -
- ))} + )} + {hasMoreDialNumbers && ( +
+ {loadingDialNumbers ? ( + + {LOADING_MORE_DIAL_NUMBERS} + + ) : ( + + {SCROLL_TO_LOAD_MORE} + + )} +
+ )} +
+ ))} - {isEntryPointTabVisible && - selectedCategory === CATEGORY_ENTRY_POINT && - (noEntryPoints ? ( - - ) : ( -
- {renderList( - entryPoints.map((e) => ({id: e.id, name: e.name})), - (item) => { - if (onEntryPointSelect) { - onEntryPointSelect(item.id, item.name); + {isEntryPointTabVisible && + selectedCategory === CATEGORY_ENTRY_POINT && + (noEntryPoints ? ( + + ) : ( +
+ {renderList( + entryPoints.map((e) => ({id: e.id, name: e.name})), + (item) => { + 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 && ( +
+ { + setAllowParticipantsToInteract(!allowParticipantsToInteract); + }} + /> +
+ )} ); }; diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss index 4c362f71f..1b1df41b4 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss @@ -270,6 +270,20 @@ justify-content: center; align-items: center; } + + .consult-list-container { + flex: 1; + overflow-y: auto; + max-height: 15rem; /* Limit height to ensure checkbox remains visible */ + min-height: 15rem; + margin-bottom: 1rem; + } + + .consult-checkbox-container { + padding-top: 1rem; + margin-top: auto; /* Push to bottom if there's extra space */ + flex-shrink: 0; /* Don't allow shrinking */ + } } .agent-tablist { 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 ae4214fe8..9eb5c8f0d 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,19 +42,18 @@ function CallControlComponent(props: CallControlComponentProps) { endCall, wrapupCall, wrapupCodes, - isHeld, - setIsHeld, isRecording, setIsRecording, buddyAgents, loadBuddyAgents, transferCall, consultCall, - consultInitiated, - consultAccepted, + exitConference, + switchToConsult, + consultConference, + consultTransfer, callControlAudio, setConsultAgentName, - setConsultAgentId, allowConsultToQueue, setLastTargetType, controlVisibility, @@ -68,11 +67,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 = () => { @@ -94,15 +93,20 @@ 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, - setConsultAgentId, setConsultAgentName, setLastTargetType, logger @@ -120,7 +124,6 @@ function CallControlComponent(props: CallControlComponentProps) { const buttons = buildCallControlButtons( isMuted, - isHeld, isRecording, isMuteButtonDisabled, currentMediaType, @@ -129,10 +132,18 @@ function CallControlComponent(props: CallControlComponentProps) { handletoggleHold, toggleRecording, endCall, - logger + exitConference, + switchToConsult, + consultTransfer, + consultConference ); - const filteredButtons = filterButtonsForConsultation(buttons, consultInitiated, isTelephony, logger); + const filteredButtons = filterButtonsForConsultation( + buttons, + controlVisibility.isConsultInitiatedOrAccepted, + isTelephony, + logger + ); if (!currentTask) return null; @@ -144,7 +155,7 @@ function CallControlComponent(props: CallControlComponentProps) { autoPlay >
- {!(consultAccepted && isTelephony) && !controlVisibility.wrapup && ( + {!controlVisibility.isConsultReceived && !controlVisibility.wrapup.isVisible && (
{filteredButtons.map((button, index) => { if (!button.isVisible) return null; @@ -188,7 +199,7 @@ function CallControlComponent(props: CallControlComponentProps) { @@ -213,12 +224,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 @@ -229,6 +246,7 @@ function CallControlComponent(props: CallControlComponentProps) { showEntryPointTab: false, } } + isConferenceInProgress={controlVisibility.isConferenceInProgress} logger={logger} /> ) : null} @@ -240,13 +258,10 @@ function CallControlComponent(props: CallControlComponentProps) { key={index} triggerComponent={ @@ -265,7 +280,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}`); } @@ -120,10 +114,10 @@ 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, setLastTargetType: (type: DestinationType) => void, logger: ILogger @@ -134,8 +128,7 @@ export const handleTargetSelect = ( }); if (agentMenuType === 'Consult') { try { - consultCall(id, type); - setConsultAgentId(id); + consultCall(id, type, allowParticipantsToInteract); setConsultAgentName(name); setLastTargetType(type); } catch (error) { @@ -198,7 +191,6 @@ export const isTelephonyMediaType = (mediaType: MediaChannelType, logger?): bool */ export const buildCallControlButtons = ( isMuted: boolean, - isHeld: boolean, isRecording: boolean, isMuteButtonDisabled: boolean, currentMediaType: MediaTypeInfo, @@ -207,7 +199,11 @@ export const buildCallControlButtons = ( handleToggleHoldFunc: () => void, toggleRecording: () => void, endCall: () => void, - logger? + exitConference: () => void, + switchToConsult: () => void, + onTransferConsult: () => void, + handleConsultConferencePress: () => void, + logger?: ILogger ): CallControlButton[] => { try { return [ @@ -218,17 +214,28 @@ 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: 'switchToConsult', + icon: 'call-swap-bold', + tooltip: 'Switch to Consult Call', + className: 'call-control-button', + onClick: switchToConsult, + disabled: !controlVisibility.switchToConsult.isEnabled, + isVisible: controlVisibility.switchToConsult.isVisible, + dataTestId: 'call-control:switch-to-consult', + }, + { 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: false, - isVisible: controlVisibility.holdResume, + disabled: !controlVisibility.holdResume.isEnabled, + isVisible: controlVisibility.holdResume.isVisible, dataTestId: 'call-control:hold-toggle', }, { @@ -236,19 +243,37 @@ export const buildCallControlButtons = ( icon: 'headset-bold', tooltip: CONSULT_AGENT, className: 'call-control-button', - disabled: false, + disabled: !controlVisibility.consult.isEnabled, menuType: 'Consult', - isVisible: controlVisibility.consult, + isVisible: controlVisibility.consult.isVisible, dataTestId: 'call-control:consult', }, + { + id: 'transferConsult', + icon: 'next-bold', + tooltip: controlVisibility.isConferenceInProgress ? 'Transfer Conference' : 'Transfer', + onClick: onTransferConsult || (() => {}), + className: 'call-control-button', + disabled: !controlVisibility.consultTransfer.isEnabled, + isVisible: controlVisibility.consultTransfer.isVisible && !!onTransferConsult, + }, + { + id: 'conference', + icon: 'webex-teams-bold', + tooltip: 'conference', + onClick: handleConsultConferencePress || (() => {}), + className: 'call-control-button', + disabled: !controlVisibility.mergeConference.isEnabled, + isVisible: controlVisibility.mergeConference.isVisible && !!handleConsultConferencePress, + }, { id: 'transfer', 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', }, { @@ -257,18 +282,28 @@ 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', }, + { + id: 'exitConference', + icon: 'exit-room-bold', + tooltip: 'Exit Conference', + className: 'call-control-button-muted', + onClick: exitConference, + disabled: !controlVisibility.exitConference.isEnabled, + isVisible: controlVisibility.exitConference.isVisible, + dataTestId: 'call-control:exit-conference', + }, { id: 'end', icon: 'cancel-regular', 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', }, ]; @@ -312,17 +347,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.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss index abd032830..d6f5e2b57 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss @@ -79,11 +79,48 @@ } .call-details { + display: flex; + gap: 0.25rem; +} + +.call-details-row { display: flex; align-items: center; gap: 0.5rem; } +.vertical-divider { + width: 0.0625rem; + height: 0.75rem; + background-color: var(--mds-color-theme-outline-secondary-normal); + margin: 0 0.25rem; + opacity: 1; +} + +.participants-section { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.participants-indicator { + display: flex; + align-items: center; + gap: 0.25rem; +} + +.participants-icon { + color: var(--mds-color-theme-text-primary-normal); +} + +.participants-count { + font-size: 0.875rem; + color: var(--mds-color-theme-text-primary-normal); + white-space: nowrap; +} + + + .call-status { display: flex; align-items: center; @@ -211,4 +248,19 @@ width: 50%; text-align: center; } +} + +.participants-popover { + background: var(--mds-color-theme-background-solid-primary-normal, #FFFFFF) !important; + .participants-menu { + max-height: 12.5rem; + overflow-y: auto; + } + + .participant-menu-item { + padding: 0.2rem; + font-size: 0.875rem; + cursor: pointer; + color: var(--mds-color-theme-text-primary-normal, #000000F2); + } } \ No newline at end of file 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..a9d427cde 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 @@ -1,11 +1,12 @@ import React from 'react'; import CallControlComponent from '../CallControl/call-control'; -import {Text} from '@momentum-ui/react-collaboration'; -import {Brandvisual, Icon, Tooltip} from '@momentum-design/components/dist/react'; +import {Text, PopoverNext} from '@momentum-ui/react-collaboration'; +import {Brandvisual, Icon, Tooltip, Button} from '@momentum-design/components/dist/react'; 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 {getMediaTypeInfo} from '../../../utils'; import { NO_CUSTOMER_NAME, @@ -24,24 +25,22 @@ 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, + switchToMainCall, callControlClassName, callControlConsultClassName, startTimestamp, - isEndConsultEnabled, controlVisibility, logger, isMuted, toggleMute, + conferenceParticipants, } = props; const formatTime = (time: number): string => { @@ -164,26 +163,81 @@ const CallControlCADComponent: React.FC = (props) =>
{renderCustomerName()}
- - {currentMediaType.labelName} - - -
- {!controlVisibility.wrapup && isHeld && ( +
+ + {currentMediaType.labelName} - + + {controlVisibility.isConferenceInProgress && !controlVisibility.wrapup.isVisible && ( <> - -
- - - {ON_HOLD} {formatTime(holdTime)} - +
+
+
+ + +{Object.keys(conferenceParticipants).length || 1} + + +
+ + + + } + > +
+ {conferenceParticipants?.map((participant) => ( +
+ {participant.name} +
+ ))} +
+
)}
+
+ {!controlVisibility.wrapup.isVisible && + controlVisibility.isHeld && + !controlVisibility.isConsultReceived && + !controlVisibility.consultCallHeld && ( + <> + +
+ + + {ON_HOLD} {formatTime(holdTime)} + +
+ + )} +
- {!controlVisibility.wrapup && controlVisibility.recordingIndicator && ( + {!controlVisibility.wrapup.isVisible && controlVisibility.recordingIndicator.isVisible && (
@@ -213,20 +267,19 @@ const CallControlCADComponent: React.FC = (props) =>
- {(consultAccepted || consultInitiated) && !controlVisibility.wrapup && isTelephony && ( + {controlVisibility.isConsultInitiatedOrAccepted && !controlVisibility.wrapup.isVisible && (
)} 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..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 @@ -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', { @@ -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 d745b8b48..3668b3b09 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 @@ -1,5 +1,5 @@ import {MEDIA_CHANNEL, TaskListItemData} from '../task.types'; -import {ITask} from '@webex/cc-store'; +import {ILogger, ITask} from '@webex/cc-store'; import {isIncomingTask} from '@webex/cc-store'; /** * Extracts and processes data from a task for rendering in the task list @@ -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?: ILogger +): 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; @@ -193,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/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index d18aa9ca7..e78530c82 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 @@ -9,6 +9,7 @@ import { AddressBookEntry, EntryPointRecord, FetchPaginatedList, + Participant, } from '@webex/cc-store'; type Enum> = T[keyof T]; @@ -118,6 +119,11 @@ export interface TaskProps { * The logger instance from SDK */ logger: ILogger; + + /** + * Agent ID of the logged-in user + */ + agentId: string; } export type IncomingTaskComponentProps = Pick & @@ -125,7 +131,7 @@ export type IncomingTaskComponentProps = Pick & Partial>; @@ -276,32 +282,41 @@ export interface ControlProps { * @param destinationType * @returns */ - consultCall: (consultDestination: string, destinationType: DestinationType) => void; + consultCall: ( + consultDestination: string, + destinationType: DestinationType, + allowParticipantsToInteract: boolean + ) => void; /** - * Function to end the consult call. + * Function to merge the consult call in a conference. */ - endConsultCall: () => void; + consultConference: () => void; /** - * Function to transfer the consult call to a already established consult. + * Function to switch to conference call. */ - consultTransfer: () => void; + switchToMainCall: () => void; /** - * Flag to determine if the consult call is connecting. + * Function to switch to consult call. */ - consultInitiated: boolean; + switchToConsult: () => void; /** - * Flag to determine if the consult call is connecting. + * Function to exit Conference + */ + exitConference: () => void; + + /** + * Function to end the consult call. */ - consultCompleted: boolean; + endConsultCall: () => void; /** - * Flag to determine if the consult call is accepted. + * Function to transfer the consult call to a already established consult. */ - consultAccepted: boolean; + consultTransfer: () => void; /** * Timestamp when the consult call started. @@ -314,17 +329,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. */ @@ -381,6 +385,11 @@ export interface ControlProps { */ allowConsultToQueue: boolean; + /** + * Flag to enable or disable multi-party conference feature + */ + multiPartyConferenceEnabled: boolean; + /** * Function to set the last target type */ @@ -391,20 +400,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; - }; + controlVisibility: ControlVisibility; secondsUntilAutoWrapup?: number; @@ -413,6 +409,11 @@ export interface ControlProps { */ cancelAutoWrapup: () => void; + /** + * List of participants in the conference excluding the agent themselves. + */ + conferenceParticipants: Participant[]; + /** Fetch paginated address book entries for dial numbers */ getAddressBookEntries?: FetchPaginatedList; @@ -426,6 +427,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< @@ -438,32 +444,26 @@ export type CallControlComponentProps = Pick< | 'isMuted' | 'endCall' | 'wrapupCall' - | 'isHeld' - | 'setIsHeld' | 'isRecording' | 'setIsRecording' | 'buddyAgents' | 'loadBuddyAgents' | 'transferCall' | 'consultCall' + | 'consultConference' + | 'switchToMainCall' + | 'switchToConsult' + | 'exitConference' | 'endConsultCall' - | 'consultInitiated' | 'consultTransfer' - | 'consultCompleted' - | 'consultAccepted' | 'consultStartTimeStamp' | 'callControlAudio' | 'consultAgentName' | 'setConsultAgentName' - | 'consultAgentId' - | 'setConsultAgentId' | 'holdTime' | 'callControlClassName' | 'callControlConsultClassName' | 'startTimestamp' - | 'queues' - | 'loadQueues' - | 'isEndConsultEnabled' | 'allowConsultToQueue' | 'lastTargetType' | 'setLastTargetType' @@ -471,6 +471,7 @@ export type CallControlComponentProps = Pick< | 'logger' | 'secondsUntilAutoWrapup' | 'cancelAutoWrapup' + | 'conferenceParticipants' | 'getAddressBookEntries' | 'getEntryPoints' | 'getQueuesFetcher' @@ -553,13 +554,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; } @@ -569,21 +571,20 @@ export interface ConsultTransferPopoverComponentProps { export interface CallControlConsultComponentsProps { agentName: string; startTimeStamp: number; - onTransfer?: () => void; - endConsultCall?: () => void; - consultCompleted: boolean; - isAgentBeingConsulted: boolean; - isEndConsultEnabled: boolean; + consultTransfer: () => void; + endConsultCall: () => void; + consultConference: () => void; + switchToMainCall: () => void; logger: ILogger; - muteUnmute: boolean; isMuted: boolean; - onToggleConsultMute?: () => void; + controlVisibility: ControlVisibility; + toggleConsultMute: () => void; } /** * 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', @@ -624,19 +625,38 @@ 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; + muteUnmuteConsult: Visibility; + holdResume: Visibility; + consult: Visibility; + transfer: Visibility; + conference: Visibility; + wrapup: Visibility; + pauseResumeRecording: Visibility; + endConsult: Visibility; + recordingIndicator: Visibility; + exitConference: Visibility; + mergeConference: Visibility; + consultTransfer: Visibility; + mergeConferenceConsult: Visibility; + consultTransferConsult: Visibility; + switchToMainCall: Visibility; + switchToConsult: Visibility; + isConferenceInProgress: boolean; + isConsultInitiated: boolean; + isConsultInitiatedAndAccepted: boolean; + isConsultReceived: boolean; + isConsultInitiatedOrAccepted: boolean; + isHeld: boolean; + consultCallHeld: boolean; } export interface MediaTypeInfo { @@ -743,3 +763,16 @@ export interface ConsultTransferOptions { /** Show the Entry Point tab. Defaults to true. */ showEntryPointTab?: boolean; } + +/** + * Interface for button configuration + */ +export interface ButtonConfig { + key: string; + icon: string; + onClick: () => void; + tooltip: string; + className: string; + disabled?: boolean; + isVisible: boolean; +} diff --git a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx index c61bd416a..e62261fbd 100644 --- a/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/StationLogin/station-login.utils.tsx @@ -1,6 +1,6 @@ import React from 'react'; import '@testing-library/jest-dom'; -import {DESKTOP, DIALNUMBER, EXTENSION, LoginOptions} from '@webex/cc-store'; +import {DESKTOP, DIAL_NUMBER, EXTENSION, LoginOptions} from '@webex/cc-store'; import { handleModals, continueClicked, @@ -176,14 +176,14 @@ describe('Station Login Utils', () => { }); describe('updateDialNumberLabel', () => { - it('should update label and placeholder for DIALNUMBER option', () => { + it('should update label and placeholder for DIAL_NUMBER option', () => { const mockSetDialNumberLabel = jest.fn(); const mockSetDialNumberPlaceholder = jest.fn(); - updateDialNumberLabel(DIALNUMBER, mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); + updateDialNumberLabel(DIAL_NUMBER, mockSetDialNumberLabel, mockSetDialNumberPlaceholder, loggerMock); - expect(mockSetDialNumberLabel).toHaveBeenCalledWith(LoginOptions[DIALNUMBER]); - expect(mockSetDialNumberPlaceholder).toHaveBeenCalledWith(LoginOptions[DIALNUMBER]); + expect(mockSetDialNumberLabel).toHaveBeenCalledWith(LoginOptions[DIAL_NUMBER]); + expect(mockSetDialNumberPlaceholder).toHaveBeenCalledWith(LoginOptions[DIAL_NUMBER]); }); it('should update label and placeholder for EXTENSION option', () => { @@ -342,7 +342,7 @@ describe('Station Login Utils', () => { }); it('should handle valid login option change', () => { - const event = {detail: {value: DIALNUMBER}}; + const event = {detail: {value: DIAL_NUMBER}}; handleLoginOptionChanged( event, @@ -362,24 +362,27 @@ describe('Station Login Utils', () => { 'team456' ); - expect(loggerMock.info).toHaveBeenCalledWith(`CC-Widgets: StationLogin: login option changed to: ${DIALNUMBER}`, { - module: 'cc-components#station-login.tsx', - method: 'loginOptionChanged', - }); - expect(mockSetters.setDeviceType).toHaveBeenCalledWith(DIALNUMBER); - expect(mockSetters.setSelectedDeviceType).toHaveBeenCalledWith(DIALNUMBER); + expect(loggerMock.info).toHaveBeenCalledWith( + `CC-Widgets: StationLogin: login option changed to: ${DIAL_NUMBER}`, + { + module: 'cc-components#station-login.tsx', + method: 'loginOptionChanged', + } + ); + expect(mockSetters.setDeviceType).toHaveBeenCalledWith(DIAL_NUMBER); + expect(mockSetters.setSelectedDeviceType).toHaveBeenCalledWith(DIAL_NUMBER); expect(mockUpdateDialNumberLabel).toHaveBeenCalled(); expect(mockSetters.setDialNumber).toHaveBeenCalledWith(''); expect(mockSetters.setShowDNError).toHaveBeenCalledWith(false); expect(mockSetters.setCurrentLoginOptions).toHaveBeenCalledWith({ - deviceType: DIALNUMBER, + deviceType: DIAL_NUMBER, dialNumber: '', teamId: 'team456', }); }); it('should handle valid login option change without selectedTeamId', () => { - const event = {detail: {value: DIALNUMBER}}; + const event = {detail: {value: DIAL_NUMBER}}; handleLoginOptionChanged( event, @@ -399,17 +402,20 @@ describe('Station Login Utils', () => { null ); - expect(loggerMock.info).toHaveBeenCalledWith(`CC-Widgets: StationLogin: login option changed to: ${DIALNUMBER}`, { - module: 'cc-components#station-login.tsx', - method: 'loginOptionChanged', - }); - expect(mockSetters.setDeviceType).toHaveBeenCalledWith(DIALNUMBER); - expect(mockSetters.setSelectedDeviceType).toHaveBeenCalledWith(DIALNUMBER); + expect(loggerMock.info).toHaveBeenCalledWith( + `CC-Widgets: StationLogin: login option changed to: ${DIAL_NUMBER}`, + { + module: 'cc-components#station-login.tsx', + method: 'loginOptionChanged', + } + ); + expect(mockSetters.setDeviceType).toHaveBeenCalledWith(DIAL_NUMBER); + expect(mockSetters.setSelectedDeviceType).toHaveBeenCalledWith(DIAL_NUMBER); expect(mockUpdateDialNumberLabel).toHaveBeenCalled(); expect(mockSetters.setDialNumber).toHaveBeenCalledWith(''); expect(mockSetters.setShowDNError).toHaveBeenCalledWith(false); expect(mockSetters.setCurrentLoginOptions).toHaveBeenCalledWith({ - deviceType: DIALNUMBER, + deviceType: DIAL_NUMBER, dialNumber: '', teamId: '', }); @@ -530,7 +536,7 @@ describe('Station Login Utils', () => { mockSetters.setDNErrorText, dialNumberRegex, mockSetters.setCurrentLoginOptions, - DIALNUMBER, + DIAL_NUMBER, loggerMock ); @@ -565,7 +571,7 @@ describe('Station Login Utils', () => { expect(mockSetters.setShowDNError).toHaveBeenCalledWith(true); }); - it('should validate dial number format for DIALNUMBER type', () => { + it('should validate dial number format for DIAL_NUMBER type', () => { const event = {target: {value: '911'}}; const dialNumberRegex = null; // Use null to trigger default regex @@ -577,7 +583,7 @@ describe('Station Login Utils', () => { mockSetters.setDNErrorText, dialNumberRegex, mockSetters.setCurrentLoginOptions, - DIALNUMBER, + DIAL_NUMBER, loggerMock ); 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..7de54104d 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" > +
    +

    + Switch to Call +

    +
    + +
    +

    + Merge +

    +
    + +
    +

    + Switch to Call +

    +
    + +
    +

    + Merge +

    +
    +
@@ -296,7 +403,7 @@ exports[`CallControlConsultComponent Snapshots Interactions should call mockOnTr class="consult-buttons consult-buttons-container" > +
+

+ Switch to Call +

+
+ +
+

+ Merge +

+
+

- Transfer Consult + Switch to Call

-
-

- End Consult -

-
- - -`; - -exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when consultCompleted is false 1`] = ` -
-
- -
- - Alice - - - Consult requested -  •  - - -
-
-
-

- Mute + Transfer

- Transfer Consult + Merge

+

+ Switch to Call +

+
+ +
+

+ Transfer +

+
+ +
+

+ Merge +

+
+ +

End Consult @@ -728,7 +900,7 @@ exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements

`; -exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when isEndConsultEnabled is false 1`] = ` +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render when transfer is disabled 1`] = `
@@ -767,7 +939,7 @@ exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements class="consult-buttons consult-buttons-container" > +
+

+ Switch to Call +

+
+ +
+

+ Merge +

+
+ +
+

+ Switch to Call +

+
+ +
+

+ Merge +

+
+ +
+

+ Mute +

+
+ +
+

+ Switch to Call +

+
+ +
+

+ Transfer +

+
+ +
+

+ Merge +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render with muted state 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Unmute +

+
+ +
+

+ Switch to Call +

+
+ +
+

+ Transfer +

+
+ +
+

+ Merge +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render without mute button when muteUnmute is hidden 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Switch to Call +

+
+ +
+

+ Transfer +

+
+ +
+

+ Merge +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render without transfer button when onTransfer is undefined 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Switch to Call +

+
+ +
+

+ Transfer +

+
+ +
+

+ Merge +

+
+ +
+

+ End Consult +

+
+
+
+`; + +exports[`CallControlConsultComponent Snapshots State Management should handle combination of props: no mute, no transfer, disabled end consult 1`] = ` +
+
+ +
+ + Alice + + + Consulting +  •  + + +
+
+
+ +
+

+ Mute +

+
+ +
+

+ Switch to Call +

+
+

- Unmute + Transfer

- Transfer Consult + Merge

- Transfer Consult + Mute

- End Consult + Switch to Call

-
-
-`; - -exports[`CallControlConsultComponent Snapshots Rendering - Tests for UI elements and visual states of CallControlConsult component should render without transfer button when onTransfer is undefined 1`] = ` -
-
- -
- - Alice - - - Consulting -  •  - - + +
+

+ Transfer +

-
-

- Mute + Merge

- End Consult + Unmute

-
-
-`; - -exports[`CallControlConsultComponent Snapshots State Management should handle edge case with empty agent name 1`] = ` -
-
- -
- - - Consulting -  •  - - -
-
-

- Mute + Switch to Call

+
+

+ Merge +

+
+

- Unmute + Mute

- Transfer Consult + Switch to Call

- End Consult + Transfer

-
-
-`; - -exports[`CallControlConsultComponent Snapshots State Management should update when muteUnmute prop changes 1`] = ` -
-
- -
- - Alice - - - Consulting -  •  - - -
-
-

- Transfer Consult + Merge

-
    -
    -
  • +
    -
    -
    -
    - - Agent One - -
    -
    + + Agent One + +
    - + +
  • -
- -
-
-
  • +
  • +
    -
    -
    -
    - - Agent Two - -
    -
    + + Agent Two + +
    - + +
    - - - - + + + + `; @@ -347,86 +351,90 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co -
    -
      -
      +
      +
        -
      • -
        -
      -
      - - Queue One - -
      -
      + + Queue One + +
      - + +
      -
    - - - + + + + `; @@ -526,86 +534,90 @@ exports[`ConsultTransferPopoverComponent Snapshots Interactions should render co -
    -
      -
      +
      +
        -
      • -
        -
      -
      - - Queue One - -
      -
      + + Queue One + +
      - + +
      -
    - - - + + + + `; @@ -723,160 +735,164 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -975,239 +991,481 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem -
      -
      -
    • +
      -
      - -
      -
      - - Available Agent - -
      -
      - + +
      -
      -
    • -
      -
      -
    • -
      -
      -
      - - Busy Agent - -
      -
      - + +
      -
    • - - -
      -
    • +
    • +
      -
      +
      - + Busy Agent +
      -
      -
      +
      + +
      +
      + +
      +
      +
    • - - Idle Agent - -
    • - + +
      + + Idle Agent + +
      +
      +
      + +
      +
      + + +
    + + +`; + +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +
    + + Select an Agent + +
    +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
      +
      +
    • +
      + +
      +
      + + Agent One + +
      +
      +
      + +
      +
      +
    • +
      +
      +
    • + +
      +
      + + Agent Two + +
      +
      - + +
    • -
    - -
    - + +
    + + `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with custom empty message 1`] = `
    @@ -1251,9 +1509,9 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem alt="" aria-labelledby="" class="md-button md-button--36 consult-category-button-standard consult-category-button-active" - data-md-event-key="md-button-17" + data-md-event-key="md-button-19" data-md-keyboard-key="agents" - id="md-button-17" + id="md-button-19" tabindex="0" type="button" variant="primary" @@ -1265,13 +1523,31 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem Agents +
    -
      -
    • -
      - -
      -
      - - Agent One - -
      -
      -
      - -
      -
      -
    • -
      -
      -
    • +
      +
      -
      - -
      -
      - - Agent Two - -
      -
      -
      - -
      -
      -
    • + No data available for consult transfer. +
      -
    + `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with custom empty message 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with different heading 1`] = `
    @@ -1450,7 +1588,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="h3" type="body-large-bold" > - Select an Agent + Choose Transfer Target
    -
    -
    - No data available for consult transfer. -
    +
    +
  • +
    + +
    +
    + + Agent One + +
    +
    +
    + +
    +
    +
  • +
    +
    +
  • +
    + +
    +
    + + Agent Two + +
    +
    +
    + +
    +
    +
  • +
    +
    `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with different heading 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty agents list 1`] = `
    @@ -1560,7 +1844,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="h3" type="body-large-bold" > - Choose Transfer Target + Select an Agent
    -
      -
    • -
      - -
      -
      - - Agent One - -
      -
      -
      - -
      -
      -
    • -
      -
      -
    • +
      +
      -
      - -
      -
      - - Agent Two - -
      -
      -
      - -
      -
      -
    • + No data available for consult transfer. +
    - +
    `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty agents list 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty queues list 1`] = `
    @@ -1846,13 +1992,13 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
    - No data available for consult transfer. +
    +
    + No data available for consult transfer. +
    `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with empty queues list 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single agent 1`] = `
    @@ -1956,13 +2106,13 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
    -
    - No data available for consult transfer. -
    +
    +
  • +
    + +
    +
    + + Single Agent + +
    +
    +
    + +
    +
    +
  • +
    +
    `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single agent 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single queue 1`] = `
    @@ -2066,13 +2287,13 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
      -
      -
    • +
        - -
        -
        - - Single Agent - -
        -
        -
        -
        +
        +
        - - -
        + + Single Queue + +
    • +
      +
      + +
      +
      +
    - + - + `; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with single queue 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render without errors when rendered with minimal props 1`] = `
    @@ -2243,13 +2470,13 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem
    -
    +
      @@ -2303,7 +2532,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem class="consult-list-item-wrapper" >
    @@ -2343,7 +2572,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="div" type="body-large-regular" > - Single Queue + Agent One
    - - - -`; - -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render without errors when rendered with minimal props 1`] = ` -
    - - Select an Agent - -
    -
    -
    - -
    -
    -
    -
    - - - -
    -
      -
      -
    • -
      - -
      -
      - - Agent One - -
      -
      - + +
      -
    • - -
    -
    -
  • -
    -
    -
    - - Agent Two - -
    -
    - + +
    -
  • - -
    - + + + + `; @@ -2726,81 +2778,85 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat -
      -
      -
    • +
      -
      +
      - + New Agent One +
      -
      -
      - - New Agent One - -
      -
      - + +
      -
      -
    • -
      -
    + + + + `; @@ -2899,160 +2955,164 @@ exports[`ConsultTransferPopoverComponent Snapshots State Management should updat -
      -
      -
    • +
      -
      -
      -
      - - Agent One - -
      -
      + + Agent One + +
      - + +
    • - - - -
      -
    • +
    • +
      -
      -
      -
      - - Agent Two - -
      -
      + + Agent Two + +
      - + +
      - - - -
    + + + + `; @@ -3151,86 +3211,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-consult.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx index dbc8e4f04..dbc6414b4 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.snapshot.tsx @@ -72,19 +72,49 @@ describe('CallControlConsultComponent Snapshots', () => { const mockOnTransfer = jest.fn(); const mockEndConsultCall = jest.fn(); const mockOnToggleConsultMute = jest.fn(); + const mockConsultConference = jest.fn(); + + const mockControlVisibility = { + accept: {isVisible: true, isEnabled: true}, + decline: {isVisible: true, isEnabled: true}, + end: {isVisible: true, isEnabled: true}, + muteUnmute: {isVisible: true, isEnabled: true}, + muteUnmuteConsult: {isVisible: true, isEnabled: true}, + holdResume: {isVisible: true, isEnabled: true}, + consult: {isVisible: true, isEnabled: true}, + transfer: {isVisible: true, isEnabled: true}, + conference: {isVisible: true, isEnabled: true}, + wrapup: {isVisible: false, isEnabled: false}, + pauseResumeRecording: {isVisible: true, isEnabled: true}, + endConsult: {isVisible: true, isEnabled: true}, + recordingIndicator: {isVisible: true, isEnabled: true}, + exitConference: {isVisible: false, isEnabled: false}, + mergeConference: {isVisible: true, isEnabled: true}, + mergeConferenceConsult: {isVisible: true, isEnabled: true}, + consultTransfer: {isVisible: true, isEnabled: true}, + consultTransferConsult: {isVisible: true, isEnabled: true}, + switchToMainCall: {isVisible: true, isEnabled: true}, + switchToConsult: {isVisible: true, isEnabled: true}, + isConferenceInProgress: false, + isConsultInitiated: false, + isConsultInitiatedAndAccepted: false, + isConsultInitiatedOrAccepted: false, + isConsultReceived: false, + isHeld: false, + consultCallHeld: false, + }; const defaultProps = { agentName: 'Alice', startTimeStamp: Date.now(), - onTransfer: mockOnTransfer, + consultTransfer: mockOnTransfer, endConsultCall: mockEndConsultCall, - onToggleConsultMute: mockOnToggleConsultMute, - consultCompleted: true, - isAgentBeingConsulted: true, - isEndConsultEnabled: true, + toggleConsultMute: mockOnToggleConsultMute, + consultConference: mockConsultConference, + switchToMainCall: jest.fn(), logger: mockLogger, - muteUnmute: true, isMuted: false, + controlVisibility: mockControlVisibility, }; beforeEach(() => { @@ -115,8 +145,14 @@ describe('CallControlConsultComponent Snapshots', () => { expect(container).toMatchSnapshot(); }); - it('should render without mute button when muteUnmute is false', async () => { - const propsWithoutMute = {...defaultProps, muteUnmute: false}; + it('should render without mute button when muteUnmute is hidden', async () => { + const propsWithoutMute = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + muteUnmute: {isVisible: false, isEnabled: false}, + }, + }; let screen; await act(async () => { screen = render(); @@ -128,7 +164,7 @@ describe('CallControlConsultComponent Snapshots', () => { }); it('should render without transfer button when onTransfer is undefined', async () => { - const propsWithoutTransfer = {...defaultProps, onTransfer: undefined}; + const propsWithoutTransfer = {...defaultProps, consultTransfer: undefined}; let screen; await act(async () => { screen = render(); @@ -151,8 +187,14 @@ describe('CallControlConsultComponent Snapshots', () => { expect(container).toMatchSnapshot(); }); - it('should render when consultCompleted is false', async () => { - const propsIncompleteConsult = {...defaultProps, consultCompleted: false}; + it('should render when transfer is disabled', async () => { + const propsIncompleteConsult = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + consultTransfer: {isVisible: true, isEnabled: false}, + }, + }; let screen; await act(async () => { screen = render(); @@ -163,8 +205,14 @@ describe('CallControlConsultComponent Snapshots', () => { expect(container).toMatchSnapshot(); }); - it('should render when isAgentBeingConsulted is false', async () => { - const propsNotBeingConsulted = {...defaultProps, isAgentBeingConsulted: false}; + it('should render with end consult hidden', async () => { + const propsNotBeingConsulted = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + endConsult: {isVisible: false, isEnabled: false}, + }, + }; let screen; await act(async () => { screen = render(); @@ -175,8 +223,14 @@ describe('CallControlConsultComponent Snapshots', () => { expect(container).toMatchSnapshot(); }); - it('should render when isEndConsultEnabled is false', async () => { - const propsEndConsultDisabled = {...defaultProps, isEndConsultEnabled: false}; + it('should render when end consult is disabled', async () => { + const propsEndConsultDisabled = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + endConsult: {isVisible: true, isEnabled: false}, + }, + }; let screen; await act(async () => { screen = render(); @@ -233,13 +287,20 @@ describe('CallControlConsultComponent Snapshots', () => { }); describe('State Management', () => { - it('should update when muteUnmute prop changes', async () => { + it('should update when muteUnmute visibility changes', async () => { let screen; await act(async () => { screen = render(); }); - screen.rerender(); + const updatedProps = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + muteUnmute: {isVisible: false, isEnabled: false}, + }, + }; + screen.rerender(); const container = screen.container.querySelector('.call-control-consult'); mockUIDProps(container); expect(container).toMatchSnapshot(); @@ -260,9 +321,12 @@ describe('CallControlConsultComponent Snapshots', () => { it('should handle combination of props: no mute, no transfer, disabled end consult', async () => { const complexProps = { ...defaultProps, - muteUnmute: false, - onTransfer: undefined, - isEndConsultEnabled: false, + consultTransfer: undefined, + controlVisibility: { + ...mockControlVisibility, + muteUnmute: {isVisible: false, isEnabled: false}, + endConsult: {isVisible: true, isEnabled: false}, + }, }; let screen; await act(async () => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx index aa548b25d..9e0d73e90 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx @@ -57,19 +57,49 @@ describe('CallControlConsultComponent', () => { const mockOnTransfer = jest.fn(); const mockEndConsultCall = jest.fn(); const mockOnToggleConsultMute = jest.fn(); + const mockConsultConference = jest.fn(); + + const mockControlVisibility = { + accept: {isVisible: true, isEnabled: true}, + decline: {isVisible: true, isEnabled: true}, + end: {isVisible: true, isEnabled: true}, + muteUnmute: {isVisible: true, isEnabled: true}, + muteUnmuteConsult: {isVisible: true, isEnabled: true}, + holdResume: {isVisible: true, isEnabled: true}, + consult: {isVisible: true, isEnabled: true}, + transfer: {isVisible: true, isEnabled: true}, + conference: {isVisible: true, isEnabled: true}, + wrapup: {isVisible: false, isEnabled: false}, + pauseResumeRecording: {isVisible: true, isEnabled: true}, + endConsult: {isVisible: true, isEnabled: true}, + recordingIndicator: {isVisible: true, isEnabled: true}, + exitConference: {isVisible: false, isEnabled: false}, + mergeConference: {isVisible: true, isEnabled: true}, + mergeConferenceConsult: {isVisible: true, isEnabled: true}, + consultTransfer: {isVisible: true, isEnabled: true}, + consultTransferConsult: {isVisible: true, isEnabled: true}, + switchToMainCall: {isVisible: true, isEnabled: true}, + switchToConsult: {isVisible: true, isEnabled: true}, + isConferenceInProgress: false, + isConsultInitiated: false, + isConsultInitiatedAndAccepted: false, + isConsultInitiatedOrAccepted: false, + isConsultReceived: false, + isHeld: false, + consultCallHeld: false, + }; const defaultProps = { agentName: 'Alice', startTimeStamp: Date.now(), - onTransfer: mockOnTransfer, + consultTransfer: mockOnTransfer, endConsultCall: mockEndConsultCall, - onToggleConsultMute: mockOnToggleConsultMute, - consultCompleted: true, - isAgentBeingConsulted: true, - isEndConsultEnabled: true, + toggleConsultMute: mockOnToggleConsultMute, + consultConference: mockConsultConference, + switchToMainCall: jest.fn(), logger: loggerMock, - muteUnmute: true, isMuted: false, + controlVisibility: mockControlVisibility, }; beforeEach(() => { @@ -163,10 +193,16 @@ describe('CallControlConsultComponent', () => { }); it('conditionally renders buttons based on props', async () => { - const propsWithoutMute = {...defaultProps, muteUnmute: false}; + const propsWithoutMute = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + muteUnmuteConsult: {isVisible: false, isEnabled: false}, + }, + }; const screen = await render(); - // Mute button should not be rendered when muteUnmute is false + // Mute button should not be rendered when muteUnmuteConsult is false expect(screen.queryByTestId('mute-consult-btn')).not.toBeInTheDocument(); // Verify remaining buttons and their attributes @@ -183,8 +219,8 @@ describe('CallControlConsultComponent', () => { expect(cancelButton).toHaveClass('call-control-consult-button-cancel'); }); - it('handles case when onTransfer is undefined (covers line 52)', async () => { - const propsWithoutTransfer = {...defaultProps, onTransfer: undefined}; + it('handles case when consultTransfer is undefined (covers line 52)', async () => { + const propsWithoutTransfer = {...defaultProps, consultTransfer: undefined}; const screen = await render(); // Component should still render without transfer functionality @@ -203,8 +239,8 @@ describe('CallControlConsultComponent', () => { expect(cancelButton).toHaveAttribute('type', 'button'); expect(cancelButton).toHaveClass('call-control-consult-button-cancel'); - // Transfer button should NOT be present when onTransfer is undefined - expect(screen.queryByTestId('transfer-consult-btn')).not.toBeInTheDocument(); + // Transfer button should be present even when consultTransfer is undefined (it's now mandatory) + expect(screen.queryByTestId('transfer-consult-btn')).toBeInTheDocument(); }); it('renders with muted state correctly', async () => { @@ -225,13 +261,18 @@ describe('CallControlConsultComponent', () => { }); it('tests button disabled states and tooltips', async () => { - const propsWithIncompleteConsult = {...defaultProps, consultCompleted: false}; + const propsWithIncompleteConsult = { + ...defaultProps, + controlVisibility: { + ...mockControlVisibility, + consultTransferConsult: {isVisible: true, isEnabled: false}, + }, + }; const screen = await render(); - // Transfer button should be disabled when consultCompleted is false + // Transfer button should be disabled when consultTransferConsult.isEnabled is false const transferButton = screen.getByTestId('transfer-consult-btn'); expect(transferButton).toHaveAttribute('data-disabled', 'true'); - expect(transferButton).toHaveAttribute('disabled', ''); expect(transferButton).toHaveAttribute('type', 'button'); expect(transferButton).toHaveAttribute('data-color', 'primary'); @@ -271,11 +312,23 @@ describe('CallControlConsultComponent', () => { const cancelButton = screen.getByTestId('cancel-consult-btn'); expect(cancelButton).toHaveAttribute('aria-describedby'); + const mergeButton = screen.queryByTestId('conference-consult-btn'); + expect(mergeButton).toHaveAttribute('aria-describedby'); + expect(mergeButton).toHaveAttribute('data-disabled', 'false'); + expect(mergeButton).toHaveAttribute('data-ghost', 'false'); + expect(mergeButton).toHaveAttribute('data-inverted', 'false'); + expect(mergeButton).toHaveAttribute('type', 'button'); + expect(mergeButton).toHaveAttribute('data-color', 'primary'); + expect(mergeButton).toHaveAttribute('data-size', '40'); + expect(mergeButton).toHaveClass('call-control-button'); + // Verify tooltip labels exist and have content const tooltipLabels = screen.container.querySelectorAll('.md-tooltip-label p'); - expect(tooltipLabels.length).toBe(3); + expect(tooltipLabels.length).toBe(5); // Updated to 5 to include switchToMainCall button expect(tooltipLabels[0]).toHaveTextContent('Mute'); - expect(tooltipLabels[1]).toHaveTextContent('Transfer Consult'); - expect(tooltipLabels[2]).toHaveTextContent('End Consult'); + expect(tooltipLabels[1]).toHaveTextContent('Switch to Call'); + expect(tooltipLabels[2]).toHaveTextContent('Transfer'); + expect(tooltipLabels[3]).toHaveTextContent('Merge'); + expect(tooltipLabels[4]).toHaveTextContent('End Consult'); }); }); 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 d7e457f9c..4365f6e88 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 @@ -1,13 +1,11 @@ import '@testing-library/jest-dom'; -import {BuddyDetails} from '@webex/cc-store'; +import {BuddyDetails, ContactServiceQueue} from '@webex/cc-store'; import {mockAgents, mockQueueDetails} from '@webex/test-fixtures'; +import {ButtonConfig, ControlVisibility} from '../../../../../src/components/task/task.types'; import { createConsultButtons, getVisibleButtons, createInitials, - handleTransferPress, - handleEndConsultPress, - handleMuteToggle, getConsultStatusText, handleListItemPress, shouldShowTabs, @@ -47,47 +45,73 @@ describe('Call Control Custom Utils', () => { jest.clearAllMocks(); }); - describe('createConsultButtons', () => { - const defaultParams = { - isMuted: false, - isMuteDisabled: false, - consultCompleted: true, - isAgentBeingConsulted: true, - isEndConsultEnabled: true, - muteUnmute: true, - }; + const mockControlVisibility = { + accept: {isVisible: true, isEnabled: true}, + decline: {isVisible: true, isEnabled: true}, + end: {isVisible: true, isEnabled: true}, + muteUnmute: {isVisible: true, isEnabled: true}, + muteUnmuteConsult: {isVisible: true, isEnabled: true}, + holdResume: {isVisible: true, isEnabled: true}, + consult: {isVisible: true, isEnabled: true}, + transfer: {isVisible: true, isEnabled: true}, + conference: {isVisible: true, isEnabled: true}, + wrapup: {isVisible: false, isEnabled: false}, + pauseResumeRecording: {isVisible: true, isEnabled: true}, + endConsult: {isVisible: true, isEnabled: true}, + recordingIndicator: {isVisible: true, isEnabled: true}, + exitConference: {isVisible: false, isEnabled: false}, + mergeConference: {isVisible: false, isEnabled: false}, + mergeConferenceConsult: {isVisible: false, isEnabled: false}, + consultTransfer: {isVisible: false, isEnabled: false}, + consultTransferConsult: {isVisible: false, isEnabled: false}, + switchToMainCall: {isVisible: false, isEnabled: false}, + switchToConsult: {isVisible: false, isEnabled: false}, + isConferenceInProgress: false, + isConsultInitiated: false, + isConsultInitiatedAndAccepted: false, + isConsultInitiatedOrAccepted: false, + isConsultReceived: false, + isHeld: false, + consultCallHeld: false, + }; + describe('createConsultButtons', () => { it('should create button configuration array with all buttons visible', () => { const mockTransfer = jest.fn(); const mockMuteToggle = jest.fn(); const mockEndConsult = jest.fn(); + const mockConsultConference = jest.fn(); + const mockSwitchToMainCall = jest.fn(); const buttons = createConsultButtons( - defaultParams.isMuted, - defaultParams.isMuteDisabled, - defaultParams.consultCompleted, - defaultParams.isAgentBeingConsulted, - defaultParams.isEndConsultEnabled, - defaultParams.muteUnmute, + false, // isMuted + mockControlVisibility, mockTransfer, mockMuteToggle, - mockEndConsult + mockEndConsult, + mockConsultConference, + mockSwitchToMainCall, + loggerMock ); - expect(buttons).toHaveLength(3); + expect(buttons).toHaveLength(5); // Updated to 5 to include switchToMainCall button expect(buttons[0].key).toBe('mute'); - expect(buttons[1].key).toBe('transfer'); - expect(buttons[2].key).toBe('cancel'); + expect(buttons[1].key).toBe('switchToMainCall'); + expect(buttons[2].key).toBe('transfer'); + expect(buttons[3].key).toBe('conference'); + expect(buttons[4].key).toBe('cancel'); }); it('should configure mute button correctly when muted', () => { const buttons = createConsultButtons( true, // isMuted - false, - defaultParams.consultCompleted, - defaultParams.isAgentBeingConsulted, - defaultParams.isEndConsultEnabled, - defaultParams.muteUnmute + mockControlVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock ); const muteButton = buttons.find((b) => b.key === 'mute'); @@ -99,11 +123,13 @@ describe('Call Control Custom Utils', () => { it('should configure mute button correctly when not muted', () => { const buttons = createConsultButtons( false, // isMuted - false, - defaultParams.consultCompleted, - defaultParams.isAgentBeingConsulted, - defaultParams.isEndConsultEnabled, - defaultParams.muteUnmute + mockControlVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock ); const muteButton = buttons.find((b) => b.key === 'mute'); @@ -113,13 +139,16 @@ describe('Call Control Custom Utils', () => { }); it('should disable transfer button when consult not completed', () => { + const customVisibility = {...mockControlVisibility, consultTransferConsult: {isVisible: true, isEnabled: false}}; const buttons = createConsultButtons( - defaultParams.isMuted, - defaultParams.isMuteDisabled, - false, // consultCompleted - defaultParams.isAgentBeingConsulted, - defaultParams.isEndConsultEnabled, - defaultParams.muteUnmute + false, // isMuted + customVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock ); const transferButton = buttons.find((b) => b.key === 'transfer'); @@ -127,40 +156,46 @@ describe('Call Control Custom Utils', () => { }); it('should hide transfer button when not agent being consulted or no onTransfer', () => { + const customVisibility = {...mockControlVisibility, consultTransferConsult: {isVisible: false, isEnabled: false}}; const buttons = createConsultButtons( - defaultParams.isMuted, - defaultParams.isMuteDisabled, - defaultParams.consultCompleted, - false, // isAgentBeingConsulted - defaultParams.isEndConsultEnabled, - defaultParams.muteUnmute + false, // isMuted + customVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock ); const transferButton = buttons.find((b) => b.key === 'transfer'); - expect(transferButton?.shouldShow).toBe(false); + expect(transferButton?.isVisible).toBe(false); }); - it('should hide mute button when muteUnmute is false', () => { + it('should hide mute button when muteUnmuteConsult is false', () => { + const customVisibility = {...mockControlVisibility, muteUnmuteConsult: {isVisible: false, isEnabled: false}}; const buttons = createConsultButtons( - defaultParams.isMuted, - defaultParams.isMuteDisabled, - defaultParams.consultCompleted, - defaultParams.isAgentBeingConsulted, - defaultParams.isEndConsultEnabled, - false // muteUnmute + false, // isMuted + customVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock ); const muteButton = buttons.find((b) => b.key === 'mute'); - expect(muteButton?.shouldShow).toBe(false); + expect(muteButton?.isVisible).toBe(false); }); }); describe('getVisibleButtons', () => { it('should filter buttons that should be shown', () => { const buttons = [ - {key: 'btn1', shouldShow: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, - {key: 'btn2', shouldShow: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, - {key: 'btn3', shouldShow: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + {key: 'btn1', isVisible: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + {key: 'btn2', isVisible: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, + {key: 'btn3', isVisible: true, icon: '', onClick: jest.fn(), tooltip: '', className: ''}, ]; const visible = getVisibleButtons(buttons); @@ -170,7 +205,7 @@ describe('Call Control Custom Utils', () => { }); it('should return empty array when no buttons should be shown', () => { - const buttons = [{key: 'btn1', shouldShow: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}]; + const buttons = [{key: 'btn1', isVisible: false, icon: '', onClick: jest.fn(), tooltip: '', className: ''}]; const visible = getVisibleButtons(buttons); expect(visible).toHaveLength(0); @@ -203,129 +238,13 @@ describe('Call Control Custom Utils', () => { }); }); - describe('handleTransferPress', () => { - it('should call onTransfer and log when provided', () => { - const mockOnTransfer = jest.fn(); - - handleTransferPress(mockOnTransfer, loggerMock); - - expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: transfer button clicked', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - expect(mockOnTransfer).toHaveBeenCalled(); - expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: transfer completed', { - module: 'call-control-consult.tsx', - method: 'handleTransfer', - }); - }); - - it('should not call onTransfer when not provided', () => { - expect(() => { - handleTransferPress(undefined, loggerMock); - }).not.toThrow(); - - expect(loggerMock.info).toHaveBeenCalled(); - expect(loggerMock.log).not.toHaveBeenCalled(); - }); - - it('should throw error when onTransfer throws', () => { - const mockOnTransfer = jest.fn(() => { - throw new Error('Transfer failed'); - }); - - expect(() => { - handleTransferPress(mockOnTransfer, loggerMock); - }).toThrow('Error transferring call: Error: Transfer failed'); - }); - }); - - describe('handleEndConsultPress', () => { - it('should call endConsultCall and log when provided', () => { - const mockEndConsult = jest.fn(); - - handleEndConsultPress(mockEndConsult, loggerMock); - - expect(loggerMock.info).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: end consult clicked', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - expect(mockEndConsult).toHaveBeenCalled(); - expect(loggerMock.log).toHaveBeenCalledWith('CC-Widgets: CallControlConsult: end consult completed', { - module: 'call-control-consult.tsx', - method: 'handleEndConsult', - }); - }); - - it('should throw error when endConsultCall throws', () => { - const mockEndConsult = jest.fn(() => { - throw new Error('End consult failed'); - }); - - expect(() => { - handleEndConsultPress(mockEndConsult, loggerMock); - }).toThrow('Error ending consult call: Error: End consult failed'); - }); - }); - - describe('handleMuteToggle', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); - - it('should disable button, call toggle, and re-enable after timeout', () => { - const mockToggleMute = jest.fn(); - const mockSetDisabled = jest.fn(); - - handleMuteToggle(mockToggleMute, mockSetDisabled, loggerMock); - - expect(mockSetDisabled).toHaveBeenCalledWith(true); - expect(mockToggleMute).toHaveBeenCalled(); - - jest.advanceTimersByTime(500); - - expect(mockSetDisabled).toHaveBeenCalledWith(false); - }); - - it('should handle error and still re-enable button', () => { - const mockToggleMute = jest.fn(() => { - throw new Error('Mute failed'); - }); - const mockSetDisabled = jest.fn(); - - handleMuteToggle(mockToggleMute, mockSetDisabled, loggerMock); - - expect(loggerMock.error).toHaveBeenCalledWith('Mute toggle failed: Error: Mute failed', { - module: 'call-control-consult.tsx', - method: 'handleConsultMuteToggle', - }); - - jest.advanceTimersByTime(500); - expect(mockSetDisabled).toHaveBeenCalledWith(false); - }); - - it('should not call toggle when not provided', () => { - const mockSetDisabled = jest.fn(); - - expect(() => { - handleMuteToggle(undefined, mockSetDisabled, loggerMock); - }).not.toThrow(); - - expect(mockSetDisabled).toHaveBeenCalledWith(true); - }); - }); - describe('getConsultStatusText', () => { it('should return "Consulting" when completed', () => { - expect(getConsultStatusText(true)).toBe('Consulting'); + expect(getConsultStatusText(true)).toBe('Consult requested'); }); it('should return "Consult requested" when not completed', () => { - expect(getConsultStatusText(false)).toBe('Consult requested'); + expect(getConsultStatusText(false)).toBe('Consulting'); }); }); @@ -461,18 +380,66 @@ 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, false); + }); + + it('should not call onAgentSelect when not provided', () => { + expect(() => { + handleAgentSelection('agent1', 'John Doe', false, undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.info).toHaveBeenCalled(); + }); + }); + + describe('handleQueueSelection', () => { + it('should call onQueueSelect and log when provided', () => { + const mockOnQueueSelect = jest.fn(); + const queueId = 'queue1'; + const queueName = 'Support Queue'; + + 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, false); + }); + + it('should not call onQueueSelect when not provided', () => { + expect(() => { + handleQueueSelection('queue1', 'Support Queue', false, undefined, loggerMock); + }).not.toThrow(); + + expect(loggerMock.log).toHaveBeenCalled(); + }); + }); + + describe('handleAgentSelection', () => { + it('should call onAgentSelect and log when provided', () => { + const mockOnAgentSelect = jest.fn(); + const agentId = 'agent1'; + const agentName = 'John Doe'; + + 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(); @@ -485,18 +452,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(); @@ -774,7 +741,59 @@ describe('Call Control Custom Utils', () => { }); describe('isQueueAvailable', () => { + const validQueue: ContactServiceQueue = { + id: 'queue1', + name: 'Support Queue', + description: 'Support Queue Description', + queueType: 'INBOUND', + checkAgentAvailability: true, + channelType: 'TELEPHONY', + serviceLevelThreshold: 30, + maxActiveContacts: 10, + maxTimeInQueue: 600, + defaultMusicInQueueMediaFileId: 'music1', + active: true, + monitoringPermitted: true, + parkingPermitted: true, + recordingPermitted: true, + recordingAllCallsPermitted: true, + pauseRecordingPermitted: true, + controlFlowScriptUrl: 'https://example.com/script', + ivrRequeueUrl: 'https://example.com/ivr', + routingType: 'LONGEST_AVAILABLE_AGENT', + queueRoutingType: 'TEAM_BASED', + callDistributionGroups: [], + }; + it('should return true for valid queue', () => { + expect(isQueueAvailable(validQueue)).toBe(true); + }); + + it('should return false for missing id', () => { + expect( + isQueueAvailable({ + ...validQueue, + id: '', + }) + ).toBeFalsy(); + }); + + it('should return false for missing name', () => { + expect( + isQueueAvailable({ + ...validQueue, + name: '', + }) + ).toBeFalsy(); + }); + + it('should return false for whitespace-only name', () => { + expect( + isQueueAvailable({ + ...validQueue, + name: ' ', + }) + ).toBeFalsy(); expect(isQueueAvailable({...mockQueueDetails[0], id: 'queue1', name: 'Support Queue'})).toBe(true); }); @@ -900,4 +919,229 @@ describe('Call Control Custom Utils', () => { expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2', 'arg3'); }); }); + + describe('Error Handling in utility functions', () => { + it('should handle errors in createConsultButtons', () => { + // Create a mock that throws when accessed + const badControlVisibility = new Proxy( + {}, + { + get() { + throw new Error('Test error'); + }, + } + ); + + const buttons = createConsultButtons( + false, + badControlVisibility as unknown as ControlVisibility, + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + jest.fn(), + loggerMock + ); + + expect(buttons).toEqual([]); + expect(loggerMock.error).toHaveBeenCalledWith( + 'CC-Widgets: CallControlCustom: Error in createConsultButtons', + expect.objectContaining({ + module: 'cc-components#call-control-custom.utils.ts', + method: 'createConsultButtons', + }) + ); + }); + + it('should handle errors in getVisibleButtons', () => { + const badButtons = [ + { + get isVisible() { + throw new Error('Test error'); + }, + }, + ]; + + const result = getVisibleButtons(badButtons as unknown as ButtonConfig[], loggerMock); + + expect(result).toEqual([]); + expect(loggerMock.error).toHaveBeenCalledWith( + 'CC-Widgets: CallControlCustom: Error in getVisibleButtons', + expect.objectContaining({ + module: 'cc-components#call-control-custom.utils.ts', + method: 'getVisibleButtons', + }) + ); + }); + + it('should handle errors in createInitials', () => { + const badName = { + split() { + throw new Error('Test error'); + }, + }; + + const result = createInitials(badName as unknown as string, loggerMock); + + expect(result).toBe('??'); + expect(loggerMock.error).toHaveBeenCalledWith( + 'CC-Widgets: CallControlCustom: Error in createInitials', + expect.objectContaining({ + module: 'cc-components#call-control-custom.utils.ts', + method: 'createInitials', + }) + ); + }); + + it('should handle errors in getConsultStatusText', () => { + // getConsultStatusText doesn't actually throw errors with undefined, it just evaluates the boolean + const result = getConsultStatusText(undefined as unknown as boolean, loggerMock); + + // undefined is falsy, so it returns 'Consulting' + expect(result).toBe('Consulting'); + // This function doesn't actually log errors for undefined input + }); + + it('should handle errors in shouldShowTabs', () => { + const badAgents = { + get length() { + throw new Error('Test error'); + }, + }; + + const result = shouldShowTabs(badAgents as unknown as BuddyDetails[], [], loggerMock); + + expect(result).toBe(false); + expect(loggerMock.error).toHaveBeenCalled(); + }); + + it('should handle errors in isAgentsEmpty', () => { + const badAgents = { + get length() { + throw new Error('Test error'); + }, + }; + + const result = isAgentsEmpty(badAgents as unknown as BuddyDetails[], loggerMock); + + expect(result).toBe(true); + expect(loggerMock.error).toHaveBeenCalled(); + }); + + it('should handle errors in isQueuesEmpty', () => { + const badQueues = { + get length() { + throw new Error('Test error'); + }, + }; + + const result = isQueuesEmpty(badQueues as unknown as ContactServiceQueue[], loggerMock); + + expect(result).toBe(true); + expect(loggerMock.error).toHaveBeenCalled(); + }); + + it('should handle errors in handleTabSelection', () => { + const mockSetSelectedTab = jest.fn(() => { + throw new Error('Test error'); + }); + + handleTabSelection('Agents', mockSetSelectedTab, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith( + expect.stringContaining('Error in handleTabSelection'), + expect.any(Object) + ); + }); + + it('should handle errors in handleAgentSelection', () => { + const mockOnAgentSelect = jest.fn(() => { + throw new Error('Test error'); + }); + + handleAgentSelection('agent1', 'John Doe', false, mockOnAgentSelect, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith( + expect.stringContaining('Error in handleAgentSelection'), + expect.any(Object) + ); + }); + + it('should handle errors in handleQueueSelection', () => { + const mockOnQueueSelect = jest.fn(() => { + throw new Error('Test error'); + }); + + handleQueueSelection('queue1', 'Support Queue', false, mockOnQueueSelect, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith( + expect.stringContaining('Error in handleQueueSelection'), + expect.any(Object) + ); + }); + + it('should handle errors in handleAgentSelection', () => { + const mockOnAgentSelect = jest.fn(() => { + throw new Error('Test error'); + }); + + handleAgentSelection('agent1', 'John Doe', false, mockOnAgentSelect, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith( + expect.stringContaining('Error in handleAgentSelection'), + expect.any(Object) + ); + }); + + it('should handle errors in handleQueueSelection', () => { + const mockOnQueueSelect = jest.fn(() => { + throw new Error('Test error'); + }); + + handleQueueSelection('queue1', 'Support Queue', false, mockOnQueueSelect, loggerMock); + + expect(loggerMock.error).toHaveBeenCalledWith( + expect.stringContaining('Error in handleQueueSelection'), + expect.any(Object) + ); + }); + + it('should handle errors in getEmptyStateMessage', () => { + // When showTabs is true and selectedTab is undefined, it falls through to the default case (queues) + const result = getEmptyStateMessage(undefined as unknown as string, true, loggerMock); + + expect(result).toBe("We can't find any queue available for now."); + // This function doesn't throw errors for undefined selectedTab, it just returns the default message + }); + + it('should handle errors in createAgentListData', () => { + const badAgents = [ + { + get agentId() { + throw new Error('Test error'); + }, + }, + ]; + + const result = createAgentListData(badAgents as unknown as BuddyDetails[], loggerMock); + + expect(result).toEqual([]); + expect(loggerMock.error).toHaveBeenCalled(); + }); + + it('should handle errors in createQueueListData', () => { + const badQueues = [ + { + get id() { + throw new Error('Test error'); + }, + }, + ]; + + const result = createQueueListData(badQueues as unknown as ContactServiceQueue[], loggerMock); + + expect(result).toEqual([]); + expect(loggerMock.error).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx index c151ec6c7..fc1d451ba 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx @@ -79,6 +79,8 @@ describe('ConsultTransferPopoverComponent Snapshots', () => { getQueues: async () => ({data: [buildQueue('queue1', 'Queue One')], meta: {page: 0, totalPages: 1}}), onAgentSelect: mockOnAgentSelect, onQueueSelect: mockOnQueueSelect, + onDialNumberSelect: jest.fn(), + onEntryPointSelect: jest.fn(), allowConsultToQueue: true, logger: mockLogger, }; 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 e55c65850..eb6491f86 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 @@ -57,6 +57,8 @@ describe('ConsultTransferPopoverComponent', () => { }), onAgentSelect: mockOnAgentSelect, onQueueSelect: mockOnQueueSelect, + onDialNumberSelect: jest.fn(), + onEntryPointSelect: jest.fn(), allowConsultToQueue: true, logger: loggerMock, }; @@ -116,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'); @@ -131,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/__snapshots__/call-control.snapshot.tsx.snap b/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap index d6f51fa04..255426bdb 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/__snapshots__/call-control.snapshot.tsx.snap @@ -9,7 +9,7 @@ exports[`CallControlComponent Snapshots Interactions should handle end call butt class="button-group" >

    - Resume the call + Hold the call

    - End Call + Hold the call

    - - -`; - -exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with muted state 1`] = ` -
    -

    - Unmute + Consult with another agent

    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with limited control visibility 1`] = ` +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with muted state 1`] = ` +
    +
    + +
    +

    + Unmute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with recording state 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +
    +

    + Hold the call +

    +
    + +
    +

    + Consult with another agent +

    +
    + +
    +

    + Transfer Call +

    +
    + +
    +

    + Pause Recording +

    +
    + +
    +

    + End Call +

    +
    +
    +
    +`; + +exports[`CallControlComponent Snapshots Rendering - Tests for UI elements and visual states of CallControl component should render with wrapup mode 1`] = ` +
    +
    + +
    +

    + Mute +

    +
    + +

    Hold the call

    - Mute + Unmute

    - Unmute + Mute

    - Resume the call + Hold the call

    - Pause Recording + Resume Recording

    +
    +

    + Mute +

    +
    + +
    +

    + Transfer Voice +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Voice +

    +
    +
    +
    @@ -116,7 +235,7 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control - Consult requested + Consulting  •  -
    -

    - Mute -

    -
    - -
    -

    - End Voice -

    -
    -
    + />
    - - Voice - - - - 00:00 - - + Voice + - + + 00:00 + + +
    @@ -397,7 +469,7 @@ exports[`CallControlCADComponent Snapshots should handle edge cases and control class="button-group" >
    +
    +

    + Mute +

    +
    + +
    +

    + Transfer Voice +

    +
    + +
    +

    + Resume Recording +

    +
    + +
    +

    + End Voice +

    +
    + +
    @@ -1853,7 +2039,7 @@ exports[`CallControlCADComponent Snapshots should render consultation and wrapup - Consult requested + Consulting  • 
    +
    + +