Skip to content

Commit 25b3025

Browse files
authored
fix(cc-widgets): Incoming Transfer showing Call control fix (#510)
1 parent 8da4378 commit 25b3025

24 files changed

Lines changed: 2349 additions & 1676 deletions

packages/contact-center/cc-components/src/components/task/TaskList/task-list.utils.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {MEDIA_CHANNEL, TaskListItemData} from '../task.types';
22
import {ITask} from '@webex/cc-store';
3-
3+
import {isIncomingTask} from '@webex/cc-store';
44
/**
55
* Extracts and processes data from a task for rendering in the task list
66
* @param task - The task object
@@ -20,7 +20,7 @@ export const extractTaskListItemData = (task: ITask, isBrowser: boolean): TaskLi
2020

2121
const taskState = task.data.interaction.state;
2222
const startTimeStamp = task.data.interaction.createdTimestamp;
23-
const isIncomingTask = !task?.data.wrapUpRequired && (taskState === 'new' || taskState === 'consult');
23+
const isTaskIncoming = isIncomingTask(task);
2424
const mediaType = task.data.interaction.mediaType;
2525
const mediaChannel = task.data.interaction.mediaChannel;
2626

@@ -29,21 +29,20 @@ export const extractTaskListItemData = (task: ITask, isBrowser: boolean): TaskLi
2929
const isSocial = mediaType === MEDIA_CHANNEL.SOCIAL;
3030

3131
// Compute button text based on conditions
32-
const acceptText =
33-
isIncomingTask && !task.data.wrapUpRequired ? (isTelephony && !isBrowser ? 'Ringing...' : 'Accept') : undefined;
32+
const acceptText = isTaskIncoming ? (isTelephony && !isBrowser ? 'Ringing...' : 'Accept') : undefined;
3433

35-
const declineText = isIncomingTask && !task.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined;
34+
const declineText = isTaskIncoming && isTelephony && isBrowser ? 'Decline' : undefined;
3635

3736
// Compute title based on media type
3837
const title = isSocial ? customerName : ani;
3938

4039
// Compute disable state for accept button
41-
const disableAccept = isIncomingTask && isTelephony && !isBrowser;
40+
const disableAccept = isTaskIncoming && isTelephony && !isBrowser;
4241

43-
const ronaTimeout = isIncomingTask ? rawRonaTimeout : null;
42+
const ronaTimeout = isTaskIncoming ? rawRonaTimeout : null;
4443

4544
// Compute display state
46-
const displayState = !isIncomingTask ? taskState : '';
45+
const displayState = !isTaskIncoming ? taskState : '';
4746

4847
return {
4948
ani,
@@ -52,7 +51,7 @@ export const extractTaskListItemData = (task: ITask, isBrowser: boolean): TaskLi
5251
ronaTimeout,
5352
taskState,
5453
startTimeStamp,
55-
isIncomingTask,
54+
isIncomingTask: isTaskIncoming,
5655
mediaType,
5756
mediaChannel,
5857
isTelephony,

packages/contact-center/cc-components/tests/components/task/TaskList/task-list.utils.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ import {
99
import {MEDIA_CHANNEL, TaskListItemData} from '../../../../src/components/task/task.types';
1010
import {mockTask} from '@webex/test-fixtures';
1111

12+
// Mock the store with a mockable isIncomingTask function
13+
jest.mock('@webex/cc-store', () => ({
14+
isIncomingTask: jest.fn(() => false),
15+
cc: {},
16+
deviceType: 'BROWSER',
17+
logger: {
18+
debug: jest.fn(),
19+
error: jest.fn(),
20+
info: jest.fn(),
21+
warn: jest.fn(),
22+
},
23+
}));
24+
25+
// Import the mocked functions
26+
import {isIncomingTask} from '@webex/cc-store';
27+
1228
describe('task-list.utils', () => {
1329
beforeEach(() => {
1430
jest.clearAllMocks();
@@ -93,6 +109,9 @@ describe('task-list.utils', () => {
93109

94110
describe('Incoming tasks', () => {
95111
it('should extract correct data for incoming telephony task on browser', () => {
112+
// Mock isIncomingTask to return true for this test
113+
(isIncomingTask as jest.Mock).mockReturnValueOnce(true);
114+
96115
const originalState = mockTask.data.interaction.state;
97116
const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails;
98117
const originalWrapUpRequired = mockTask.data.wrapUpRequired;
@@ -161,6 +180,9 @@ describe('task-list.utils', () => {
161180
});
162181

163182
it('should extract correct data for incoming telephony task on non-browser', () => {
183+
// Mock isIncomingTask to return true for this test
184+
(isIncomingTask as jest.Mock).mockReturnValueOnce(true);
185+
164186
const originalState = mockTask.data.interaction.state;
165187
const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails;
166188
const originalWrapUpRequired = mockTask.data.wrapUpRequired;
@@ -190,6 +212,9 @@ describe('task-list.utils', () => {
190212
});
191213

192214
it('should extract correct data for incoming social media task', () => {
215+
// Mock isIncomingTask to return true for this test
216+
(isIncomingTask as jest.Mock).mockReturnValueOnce(true);
217+
193218
const originalState = mockTask.data.interaction.state;
194219
const originalCallAssociatedDetails = mockTask.data.interaction.callAssociatedDetails;
195220
const originalWrapUpRequired = mockTask.data.wrapUpRequired;
@@ -428,6 +453,9 @@ describe('task-list.utils', () => {
428453
});
429454

430455
it('should not call onTaskSelect for non-selectable task', () => {
456+
// Mock isIncomingTask to return true so this task is non-selectable
457+
(isIncomingTask as jest.Mock).mockReturnValueOnce(true);
458+
431459
const task = {
432460
...mockTask,
433461
data: {

packages/contact-center/store/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"test:styles": "eslint"
2323
},
2424
"dependencies": {
25-
"@webex/plugin-cc": "3.8.1-next.12",
25+
"@webex/plugin-cc": "3.8.1-next.28",
2626
"mobx": "6.13.5",
2727
"typescript": "5.6.3"
2828
},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
import store from './storeEventsWrapper';
22
export * from './store.types';
3+
export * from './task-utils';
34
export default store;

packages/contact-center/store/src/storeEventsWrapper.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './store.types';
2121
import Store from './store';
2222
import {runInAction} from 'mobx';
23+
import {isIncomingTask} from './task-utils';
2324

2425
class StoreWrapper implements IStoreWrapper {
2526
store: IStore;
@@ -203,12 +204,8 @@ class StoreWrapper implements IStoreWrapper {
203204
};
204205

205206
setCurrentTask = (task: ITask | null, isClicked: boolean = false): void => {
206-
// Don't assign the task as current task if the interaction state is 'new' or 'consult'
207-
if (
208-
!task?.data.wrapUpRequired &&
209-
(task?.data.interaction.state === 'new' || task?.data.interaction.state === 'consult')
210-
)
211-
return;
207+
// Don't assign the task as current task is incoming
208+
if (isIncomingTask(task)) return;
212209

213210
runInAction(() => {
214211
// Determine if the new task is the same as the current task
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {ITask} from './store.types';
2+
3+
/**
4+
* Determines if a task is an incoming task
5+
* @param task - The task object
6+
* @returns Whether the task is incoming
7+
*/
8+
export const isIncomingTask = (task: ITask): boolean => {
9+
const taskData = task?.data;
10+
const taskState = taskData?.interaction?.state;
11+
const agentId = taskData?.agentId;
12+
const participants = taskData?.interaction?.participants;
13+
const hasJoined = agentId && participants?.[agentId]?.hasJoined;
14+
15+
return (
16+
!taskData?.wrapUpRequired &&
17+
!hasJoined &&
18+
(taskState === 'new' || taskState === 'consult' || taskState === 'connected')
19+
);
20+
};

packages/contact-center/store/tests/storeEventsWrapper.ts

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,13 @@ describe('storeEventsWrapper', () => {
171171
interactionId: 'mockInteractionId',
172172
interaction: {
173173
state: 'connected',
174+
participants: {
175+
agent1: {
176+
hasJoined: true,
177+
},
178+
},
174179
},
180+
agentId: 'agent1',
175181
},
176182
} as ITask;
177183
storeWrapper.setCurrentTask(mockCurrentTask);
@@ -470,7 +476,13 @@ describe('storeEventsWrapper', () => {
470476
interactionId: 'interaction1',
471477
interaction: {
472478
state: 'connected',
479+
participants: {
480+
agent1: {
481+
hasJoined: true,
482+
},
483+
},
473484
},
485+
agentId: 'agent1',
474486
},
475487
on: jest.fn(),
476488
off: jest.fn(),
@@ -510,13 +522,32 @@ describe('storeEventsWrapper', () => {
510522
interaction: {
511523
state: 'new',
512524
},
525+
agentId: 'agent1',
526+
// Note: mockTask2 doesn't have hasJoined: true to simulate an incoming task
513527
},
514528
on: jest.fn(),
515529
off: jest.fn(),
516530
} as unknown as ITask;
517531

518-
storeWrapper['store'].taskList = {interaction2: mockTask};
519-
storeWrapper.setCurrentTask(mockTask);
532+
// Set up mockTask with hasJoined: true so it can be set as current task
533+
const mockTaskWithJoined = {
534+
...mockTask,
535+
data: {
536+
...mockTask.data,
537+
interaction: {
538+
...mockTask.data.interaction,
539+
participants: {
540+
agent1: {
541+
hasJoined: true,
542+
},
543+
},
544+
},
545+
agentId: 'agent1',
546+
},
547+
};
548+
549+
storeWrapper['store'].taskList = {interaction2: mockTaskWithJoined};
550+
storeWrapper.setCurrentTask(mockTaskWithJoined);
520551

521552
// Call the method under test
522553
storeWrapper.handleIncomingTask(mockTask2);
@@ -1647,10 +1678,32 @@ describe('storeEventsWrapper', () => {
16471678

16481679
beforeEach(() => {
16491680
mockTaskA = {
1650-
data: {interactionId: 'taskA', interaction: {state: 'connected'}},
1681+
data: {
1682+
interactionId: 'taskA',
1683+
interaction: {
1684+
state: 'connected',
1685+
participants: {
1686+
agent1: {
1687+
hasJoined: true,
1688+
},
1689+
},
1690+
},
1691+
agentId: 'agent1',
1692+
},
16511693
} as ITask;
16521694
mockTaskB = {
1653-
data: {interactionId: 'taskB', interaction: {state: 'connected'}},
1695+
data: {
1696+
interactionId: 'taskB',
1697+
interaction: {
1698+
state: 'connected',
1699+
participants: {
1700+
agent1: {
1701+
hasJoined: true,
1702+
},
1703+
},
1704+
},
1705+
agentId: 'agent1',
1706+
},
16541707
} as ITask;
16551708
storeWrapper['store'].consultCompleted = true;
16561709
storeWrapper['store'].consultInitiated = true;
@@ -1695,5 +1748,59 @@ describe('storeEventsWrapper', () => {
16951748
storeWrapper.setCurrentTask(null);
16961749
expect(storeWrapper.currentTask).toBeNull();
16971750
});
1751+
1752+
it('should not change currentTask when task is incoming (hasJoined is false)', () => {
1753+
// Set an initial task that can be set as current task
1754+
storeWrapper.setCurrentTask(mockTaskA);
1755+
expect(storeWrapper.currentTask).toEqual(mockTaskA);
1756+
1757+
// Create an incoming task (without hasJoined: true)
1758+
const incomingTask: ITask = {
1759+
data: {
1760+
interactionId: 'incomingTask',
1761+
interaction: {
1762+
state: 'new',
1763+
// Note: no participants or hasJoined property to simulate incoming task
1764+
},
1765+
agentId: 'agent1',
1766+
},
1767+
} as ITask;
1768+
1769+
// Try to set the incoming task as current task
1770+
storeWrapper.setCurrentTask(incomingTask);
1771+
1772+
// Current task should remain unchanged (still mockTaskA)
1773+
expect(storeWrapper.currentTask).toEqual(mockTaskA);
1774+
expect(storeWrapper.currentTask).not.toEqual(incomingTask);
1775+
});
1776+
1777+
it('should not change currentTask when task has hasJoined false', () => {
1778+
// This is the case where we transfer the call but agent has not accepted it yet.
1779+
storeWrapper.setCurrentTask(mockTaskA);
1780+
expect(storeWrapper.currentTask).toEqual(mockTaskA);
1781+
1782+
// Create a task with explicitly hasJoined: false
1783+
const taskWithoutJoined: ITask = {
1784+
data: {
1785+
interactionId: 'taskWithoutJoined',
1786+
interaction: {
1787+
state: 'connected',
1788+
participants: {
1789+
agent1: {
1790+
hasJoined: false,
1791+
},
1792+
},
1793+
},
1794+
agentId: 'agent1',
1795+
},
1796+
} as ITask;
1797+
1798+
// Try to set the task without joined as current task
1799+
storeWrapper.setCurrentTask(taskWithoutJoined);
1800+
1801+
// Current task should remain unchanged (still mockTaskA)
1802+
expect(storeWrapper.currentTask).toEqual(mockTaskA);
1803+
expect(storeWrapper.currentTask).not.toEqual(taskWithoutJoined);
1804+
});
16981805
});
16991806
});

0 commit comments

Comments
 (0)