From de67abf1bb1fd596c4432ad3cfcba226def8e3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Tue, 5 Aug 2025 14:46:36 +0530 Subject: [PATCH 1/3] fix(user-state): fix-rona-state --- .../src/components/UserState/user-state.tsx | 2 +- .../components/UserState/user-state.utils.ts | 89 +++++++++++++------ .../tests/components/UserState/user-state.tsx | 12 ++- .../components/UserState/user-state.utils.tsx | 57 +++++++++--- 4 files changed, 118 insertions(+), 42 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 6375a9825..7f4cd3bc3 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -32,7 +32,7 @@ const UserStateComponent: React.FunctionComponent = (p const previousSelectableState = useMemo(() => getPreviousSelectableState(idleCodes), [idleCodes]); const selectedKey = getSelectedKey(customState, currentState, idleCodes); - const items = buildDropdownItems(customState, idleCodes); + const items = buildDropdownItems(customState, idleCodes, currentState); const sortedItems = sortDropdownItems(items); return ( diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts index 0b54b0443..b6253fe2f 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts @@ -23,12 +23,20 @@ export const getDropdownClass = (customState: ICustomState, currentState: string /** * Gets the icon style configuration for a given item */ -export const getIconStyle = (item: {id: string; name: string}): {class: string; iconName: string} => { +export const getIconStyle = (item: { + id: string; + name: string; + developerName?: string; +}): {class: string; iconName: string} => { + if (item.developerName) { + return {class: 'custom', iconName: 'busy-presence-light'}; + } + switch (item.name) { case AgentUserState.Available: - return {class: 'available', iconName: 'recents-presence-filled'}; + return {class: 'available', iconName: 'active-presence-small-filled'}; case AgentUserState.RONA: - return {class: 'rona', iconName: 'warning-filled'}; + return {class: 'rona', iconName: 'dnd-presence-filled'}; default: return {class: 'idle', iconName: 'recents-presence-filled'}; } @@ -57,12 +65,15 @@ export const handleSelectionChange = ( setAgentStatus: (auxCodeId: string) => void, logger ): void => { - if (key !== currentState) { - logger?.info(`CC-Widgets: UserState: state changed to: ${key}`, { + // Remove 'hide-' prefix if present (for RONA states) + const cleanKey = key.startsWith('hide-') ? key.substring(5) : key; + + if (cleanKey !== currentState) { + logger?.info(`CC-Widgets: UserState: state changed to: ${cleanKey}`, { module: 'cc-components#user-state.tsx', method: 'handleSelectionChange', }); - setAgentStatus(key); + setAgentStatus(cleanKey); } }; @@ -70,11 +81,11 @@ export const handleSelectionChange = ( * Sorts dropdown items with Available first, then others */ export const sortDropdownItems = (items: Array<{id: string; name: string}>): Array<{id: string; name: string}> => { - return [...items].sort((a, b) => { - if (a.name === AgentUserState.Available) return -1; - if (b.name === AgentUserState.Available) return 1; - return a.name.localeCompare(b.name); - }); + // Keep Available at the top, sort the rest alphabetically + return [ + ...items.filter((item) => item.name === AgentUserState.Available), + ...items.filter((item) => item.name !== AgentUserState.Available).sort((a, b) => a.name.localeCompare(b.name)), + ]; }; /** @@ -92,15 +103,18 @@ export const getPreviousSelectableState = (idleCodes: IdleCode[]): string => { */ export const getSelectedKey = (customState: ICustomState, currentState: string, idleCodes: IdleCode[]): string => { if (customState && 'developerName' in customState) { - return `custom-${customState.developerName}`; + return `hide-${customState.developerName}`; } + // Check if current state is RONA const currentIdleCode = idleCodes.find((code) => code.id === currentState); - const isRonaOrEngaged = [AgentUserState.RONA, AgentUserState.Engaged].includes( - currentIdleCode?.name as AgentUserState - ); + if (currentIdleCode?.name === AgentUserState.RONA) { + return `hide-${currentState}`; + } - if (isRonaOrEngaged) { + // Check if current state is Engaged + const isEngaged = currentIdleCode?.name === AgentUserState.Engaged; + if (isEngaged) { return getPreviousSelectableState(idleCodes); } @@ -108,23 +122,46 @@ export const getSelectedKey = (customState: ICustomState, currentState: string, }; /** - * Builds the dropdown items including custom state if present + * Builds the dropdown items including custom state and conditional RONA */ export const buildDropdownItems = ( customState: ICustomState, - idleCodes: IdleCode[] -): Array<{id: string; name: string}> => { - const items = idleCodes - .filter((code) => ![AgentUserState.RONA, AgentUserState.Engaged].includes(code.name as AgentUserState)) - .map((code) => ({ - id: code.id, - name: code.name, - })); + idleCodes: IdleCode[], + currentState: string +): Array<{id: string; name: string; developerName?: string}> => { + const items: Array<{id: string; name: string; developerName?: string}> = []; + // Add custom state if present if (customState && 'developerName' in customState) { items.push({ - id: `custom-${customState.developerName}`, name: customState.name, + id: `hide-${customState.developerName}`, + developerName: customState.developerName, + }); + } + + // Add regular idle codes, but handle RONA specially + for (const code of idleCodes) { + // Skip Engaged states entirely + if (code.name === AgentUserState.Engaged) { + continue; + } + + // For RONA: only include if it's the current state + if (code.name === AgentUserState.RONA) { + if (code.id === currentState) { + items.push({ + ...code, + id: `hide-${code.id}`, // Use hide- prefix for RONA + }); + } + continue; + } + + // Add all other states normally + items.push({ + id: code.id, + name: code.name, }); } diff --git a/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx b/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx index 3a0a09604..a046816da 100644 --- a/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/tests/components/UserState/user-state.tsx @@ -172,7 +172,11 @@ describe('UserStateComponent', () => { render(); }); - expect(buildDropdownItemsSpy).toHaveBeenCalledWith(defaultProps.customState, defaultProps.idleCodes); + expect(buildDropdownItemsSpy).toHaveBeenCalledWith( + defaultProps.customState, + defaultProps.idleCodes, + defaultProps.currentState + ); }); it('should call sortDropdownItems with items from buildDropdownItems', async () => { @@ -242,7 +246,11 @@ describe('UserStateComponent', () => { }); expect(getSelectedKeySpy).toHaveBeenCalledWith(customState, defaultProps.currentState, defaultProps.idleCodes); - expect(buildDropdownItemsSpy).toHaveBeenCalledWith(customState, defaultProps.idleCodes); + expect(buildDropdownItemsSpy).toHaveBeenCalledWith( + customState, + defaultProps.idleCodes, + defaultProps.currentState + ); }); }); diff --git a/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx b/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx index ca00b4b7a..4a866056e 100644 --- a/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx +++ b/packages/contact-center/cc-components/tests/components/UserState/user-state.utils.tsx @@ -65,7 +65,7 @@ describe('UserState Utils', () => { const result = getIconStyle(item); expect(result).toEqual({ class: 'available', - iconName: 'recents-presence-filled', + iconName: 'active-presence-small-filled', }); }); @@ -74,7 +74,7 @@ describe('UserState Utils', () => { const result = getIconStyle(item); expect(result).toEqual({ class: 'rona', - iconName: 'warning-filled', + iconName: 'dnd-presence-filled', }); }); @@ -86,6 +86,15 @@ describe('UserState Utils', () => { iconName: 'recents-presence-filled', }); }); + + it('should return custom style for items with developerName', () => { + const item = {id: '1', name: 'Custom State', developerName: 'CUSTOM'}; + const result = getIconStyle(item); + expect(result).toEqual({ + class: 'custom', + iconName: 'busy-presence-light', + }); + }); }); describe('getTooltipText', () => { @@ -224,7 +233,7 @@ describe('UserState Utils', () => { it('should return custom key when customState is present', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; const result = getSelectedKey(customState, '1', mockIdleCodes); - expect(result).toBe('custom-CUSTOM'); + expect(result).toBe('hide-CUSTOM'); }); it('should return currentState for normal states', () => { @@ -232,14 +241,14 @@ describe('UserState Utils', () => { expect(result).toBe('1'); }); - it('should return previous selectable state for RONA', () => { + it('should return hide-prefixed key for RONA when it is current state', () => { const result = getSelectedKey(null, '3', mockIdleCodes); - expect(result).toBe('0'); // Available is the first selectable state + expect(result).toBe('hide-3'); }); it('should return previous selectable state for ENGAGED', () => { const result = getSelectedKey(null, '4', mockIdleCodes); - expect(result).toBe('0'); // Available is the first selectable state + expect(result).toBe('0'); }); it('should handle missing currentState in idleCodes', () => { @@ -250,7 +259,7 @@ describe('UserState Utils', () => { describe('buildDropdownItems', () => { it('should filter out RONA and ENGAGED states', () => { - const result = buildDropdownItems(null, mockIdleCodes); + const result = buildDropdownItems(null, mockIdleCodes, '1'); expect(result).toEqual([ {id: '0', name: 'Available'}, @@ -261,26 +270,26 @@ describe('UserState Utils', () => { it('should include custom state when present', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = buildDropdownItems(customState, mockIdleCodes); + const result = buildDropdownItems(customState, mockIdleCodes, '1'); expect(result).toEqual([ + {id: 'hide-CUSTOM', name: 'Custom State', developerName: 'CUSTOM'}, {id: '0', name: 'Available'}, {id: '1', name: 'Break'}, {id: '2', name: 'Training'}, - {id: 'custom-CUSTOM', name: 'Custom State'}, ]); }); it('should handle empty idleCodes array', () => { - const result = buildDropdownItems(null, []); + const result = buildDropdownItems(null, [], '1'); expect(result).toEqual([]); }); it('should handle empty idleCodes array with custom state', () => { const customState = {name: 'Custom State', developerName: 'CUSTOM'}; - const result = buildDropdownItems(customState, []); + const result = buildDropdownItems(customState, [], '1'); - expect(result).toEqual([{id: 'custom-CUSTOM', name: 'Custom State'}]); + expect(result).toEqual([{id: 'hide-CUSTOM', name: 'Custom State', developerName: 'CUSTOM'}]); }); it('should handle idleCodes with only RONA and ENGAGED', () => { @@ -289,8 +298,30 @@ describe('UserState Utils', () => { {id: '4', name: 'ENGAGED', isSystem: true, isDefault: false}, ]; - const result = buildDropdownItems(null, ronaEngagedOnly); + const result = buildDropdownItems(null, ronaEngagedOnly, '1'); expect(result).toEqual([]); }); + + it('should include RONA when it is the current state', () => { + const result = buildDropdownItems(null, mockIdleCodes, '3'); + + expect(result).toEqual([ + {id: '0', name: 'Available'}, + {id: '1', name: 'Break'}, + {id: '2', name: 'Training'}, + {id: 'hide-3', name: 'RONA', isSystem: true, isDefault: false}, + ]); + }); + + it('should use correct prefix for custom state', () => { + const customState = {name: 'Custom State', developerName: 'CUSTOM'}; + const result = buildDropdownItems(customState, mockIdleCodes, '1'); + + expect(result[0]).toEqual({ + id: 'hide-CUSTOM', + name: 'Custom State', + developerName: 'CUSTOM', + }); + }); }); }); From cb03bde78c6d98d6f7b45707163e5e8a7a35d47a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Tue, 5 Aug 2025 16:33:19 +0530 Subject: [PATCH 2/3] fix(user-state): fix-other-user-states --- .../src/components/UserState/user-state.tsx | 5 ++-- .../components/UserState/user-state.utils.ts | 29 ++++++++++++++----- .../contact-center/store/src/store.types.ts | 21 +++++++++++++- .../store/src/storeEventsWrapper.ts | 3 +- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx index 7f4cd3bc3..704127aa6 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import {AgentUserState, UserStateComponentsProps} from './user-state.types'; import {formatTime} from '../../utils'; - +import {ERROR_TRIGGERING_IDLE_CODES} from '@webex/cc-store'; import './user-state.scss'; import {SelectNext, Text} from '@momentum-ui/react-collaboration'; import {Item} from '@react-stately/collections'; @@ -50,8 +50,7 @@ const UserStateComponent: React.FunctionComponent = (p data-testid="state-select" > {(item) => { - const isRonaOrEngaged = [AgentUserState.RONA, AgentUserState.Engaged].includes( - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + const isRonaOrEngaged = [...Object.values(ERROR_TRIGGERING_IDLE_CODES), AgentUserState.Engaged].includes( idleCodes.find((code) => code.id === currentState)?.name || '' ); const shouldHighlight = currentState === item.id || (isRonaOrEngaged && item.id === previousSelectableState); diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts index b6253fe2f..adba4e038 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts @@ -1,4 +1,4 @@ -import {ICustomState, IdleCode} from '@webex/cc-store'; +import {ICustomState, IdleCode, ERROR_TRIGGERING_IDLE_CODES} from '@webex/cc-store'; import {AgentUserState} from './user-state.types'; import {userStateLabels} from './constant'; @@ -13,7 +13,7 @@ export const getDropdownClass = (customState: ICustomState, currentState: string return ''; } for (const item of idleCodes) { - if (item.id === currentState && item.name === AgentUserState.RONA) { + if (item.id === currentState && Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(item.name)) { return 'rona'; } } @@ -35,7 +35,12 @@ export const getIconStyle = (item: { switch (item.name) { case AgentUserState.Available: return {class: 'available', iconName: 'active-presence-small-filled'}; - case AgentUserState.RONA: + case ERROR_TRIGGERING_IDLE_CODES.RONA: + case ERROR_TRIGGERING_IDLE_CODES.INVALID_NUMBER: + case ERROR_TRIGGERING_IDLE_CODES.UNAVAILABLE: + case ERROR_TRIGGERING_IDLE_CODES.DECLINED: + case ERROR_TRIGGERING_IDLE_CODES.BUSY: + case ERROR_TRIGGERING_IDLE_CODES.CHANNEL_FAILURE: return {class: 'rona', iconName: 'dnd-presence-filled'}; default: return {class: 'idle', iconName: 'recents-presence-filled'}; @@ -93,7 +98,8 @@ export const sortDropdownItems = (items: Array<{id: string; name: string}>): Arr */ export const getPreviousSelectableState = (idleCodes: IdleCode[]): string => { const selectableState = idleCodes.find( - (code) => ![AgentUserState.RONA, AgentUserState.Engaged].includes(code.name as AgentUserState) + (code) => + ![...Object.values(ERROR_TRIGGERING_IDLE_CODES), AgentUserState.Engaged].includes(code.name as AgentUserState) ); return selectableState?.id || '0'; }; @@ -106,14 +112,21 @@ export const getSelectedKey = (customState: ICustomState, currentState: string, return `hide-${customState.developerName}`; } - // Check if current state is RONA + // Check if current state exists in idleCodes first const currentIdleCode = idleCodes.find((code) => code.id === currentState); - if (currentIdleCode?.name === AgentUserState.RONA) { + + // If currentIdleCode is not found, return currentState as-is + if (!currentIdleCode) { + return currentState; + } + + // Check if current state is an error-triggering idle code (like RONA) + if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(currentIdleCode.name)) { return `hide-${currentState}`; } // Check if current state is Engaged - const isEngaged = currentIdleCode?.name === AgentUserState.Engaged; + const isEngaged = currentIdleCode.name === AgentUserState.Engaged; if (isEngaged) { return getPreviousSelectableState(idleCodes); } @@ -148,7 +161,7 @@ export const buildDropdownItems = ( } // For RONA: only include if it's the current state - if (code.name === AgentUserState.RONA) { + if (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name)) { if (code.id === currentState) { items.push({ ...code, diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index ba7c71e8a..bca5792e3 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -220,6 +220,15 @@ const LoginOptions: {[key: string]: string} = { [DESKTOP]: 'Desktop', }; +const ERROR_TRIGGERING_IDLE_CODES = { + INVALID_NUMBER: 'Invalid_Number', + UNAVAILABLE: 'Agent_Unavailable', + DECLINED: 'Agent_Declined', + BUSY: 'Agent_Busy', + CHANNEL_FAILURE: 'Channel_Failure', + RONA: 'RONA', +}; + export type { IContactCenter, ITask, @@ -241,4 +250,14 @@ export type { IWebex, }; -export {CC_EVENTS, TASK_EVENTS, ENGAGED_LABEL, ENGAGED_USERNAME, DIALNUMBER, EXTENSION, DESKTOP, LoginOptions}; +export { + CC_EVENTS, + TASK_EVENTS, + ENGAGED_LABEL, + ENGAGED_USERNAME, + DIALNUMBER, + EXTENSION, + DESKTOP, + LoginOptions, + ERROR_TRIGGERING_IDLE_CODES, +}; diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index 1d39d6391..59dd8df3f 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -16,6 +16,7 @@ import { ContactServiceQueue, Profile, AgentLoginProfile, + ERROR_TRIGGERING_IDLE_CODES, } from './store.types'; import Store from './store'; import {runInAction} from 'mobx'; @@ -50,7 +51,7 @@ class StoreWrapper implements IStoreWrapper { } get idleCodes() { return this.store.idleCodes.filter((code) => { - return code.name === 'RONA' || !code.isSystem; + return Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name) || !code.isSystem; }); } get agentId() { From ce728d2b8c384d99053fd1877641eb95ed57a885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CAkula?= Date: Tue, 5 Aug 2025 21:30:26 +0530 Subject: [PATCH 3/3] fix(user-state): fix-style-changes-for-error-trigger-idelcodes --- .../src/components/UserState/user-state.utils.ts | 5 +++-- widgets-samples/cc/samples-cc-react-app/src/App.tsx | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts index adba4e038..c0dfcf1bc 100644 --- a/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts +++ b/packages/contact-center/cc-components/src/components/UserState/user-state.utils.ts @@ -13,7 +13,7 @@ export const getDropdownClass = (customState: ICustomState, currentState: string return ''; } for (const item of idleCodes) { - if (item.id === currentState && Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(item.name)) { + if (item.id === currentState && item.name === ERROR_TRIGGERING_IDLE_CODES.RONA) { return 'rona'; } } @@ -36,12 +36,13 @@ export const getIconStyle = (item: { case AgentUserState.Available: return {class: 'available', iconName: 'active-presence-small-filled'}; case ERROR_TRIGGERING_IDLE_CODES.RONA: + return {class: 'rona', iconName: 'dnd-presence-filled'}; case ERROR_TRIGGERING_IDLE_CODES.INVALID_NUMBER: case ERROR_TRIGGERING_IDLE_CODES.UNAVAILABLE: case ERROR_TRIGGERING_IDLE_CODES.DECLINED: case ERROR_TRIGGERING_IDLE_CODES.BUSY: case ERROR_TRIGGERING_IDLE_CODES.CHANNEL_FAILURE: - return {class: 'rona', iconName: 'dnd-presence-filled'}; + return {class: 'idle', iconName: 'dnd-presence-filled'}; default: return {class: 'idle', iconName: 'recents-presence-filled'}; } diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index 7c12ac289..245a23cf1 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -10,6 +10,7 @@ import { OutdialCall, } from '@webex/cc-widgets'; import {StationLogoutResponse} from '@webex/plugin-cc'; +import {ERROR_TRIGGERING_IDLE_CODES} from '@webex/cc-store'; import Webex from 'webex'; import {ThemeProvider, IconProvider, Icon, Button, Checkbox, Text, Select, Option} from '@momentum-design/components/dist/react'; import {PopoverNext} from '@momentum-ui/react-collaboration'; @@ -360,7 +361,7 @@ const onToggleMute = ({isMuted, task}) => { //adding a log to be used for automation console.log('onStateChange invoked with state name:', status?.name); if (!status || !status.name) return; - if (status.name !== 'RONA') { + if (!Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(status.name)) { setShowRejectedPopup(false); setRejectedReason(''); }