Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -32,7 +32,7 @@ const UserStateComponent: React.FunctionComponent<UserStateComponentsProps> = (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 (
Expand All @@ -50,8 +50,7 @@ const UserStateComponent: React.FunctionComponent<UserStateComponentsProps> = (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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 && item.name === ERROR_TRIGGERING_IDLE_CODES.RONA) {
return 'rona';
}
}
Expand All @@ -23,12 +23,26 @@ 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'};
case AgentUserState.RONA:
return {class: 'rona', iconName: 'warning-filled'};
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: 'idle', iconName: 'dnd-presence-filled'};
default:
return {class: 'idle', iconName: 'recents-presence-filled'};
}
Expand Down Expand Up @@ -57,32 +71,36 @@ 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);
}
};

/**
* 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)),
];
};

/**
* Gets the previous selectable state (first non-RONA/Engaged state)
*/
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';
};
Expand All @@ -92,39 +110,72 @@ 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 exists in idleCodes first
const currentIdleCode = idleCodes.find((code) => code.id === currentState);
const isRonaOrEngaged = [AgentUserState.RONA, AgentUserState.Engaged].includes(
currentIdleCode?.name as AgentUserState
);

if (isRonaOrEngaged) {
// 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;
if (isEngaged) {
return getPreviousSelectableState(idleCodes);
}

return currentState;
};

/**
* 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 (Object.values(ERROR_TRIGGERING_IDLE_CODES).includes(code.name)) {
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,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ describe('UserStateComponent', () => {
render(<UserStateComponent {...defaultProps} />);
});

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 () => {
Expand Down Expand Up @@ -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
);
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
});

Expand All @@ -74,7 +74,7 @@ describe('UserState Utils', () => {
const result = getIconStyle(item);
expect(result).toEqual({
class: 'rona',
iconName: 'warning-filled',
iconName: 'dnd-presence-filled',
});
});

Expand All @@ -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', () => {
Expand Down Expand Up @@ -224,22 +233,22 @@ 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', () => {
const result = getSelectedKey(null, '1', mockIdleCodes);
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', () => {
Expand All @@ -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'},
Expand All @@ -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', () => {
Expand All @@ -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',
});
});
});
});
21 changes: 20 additions & 1 deletion packages/contact-center/store/src/store.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
};
Loading
Loading