Skip to content

Commit a677f5e

Browse files
fix(call-control): add-call-control-widget (#362)
1 parent 20c9197 commit a677f5e

File tree

23 files changed

+1200
-263
lines changed

23 files changed

+1200
-263
lines changed
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {StationLogin} from '@webex/cc-station-login';
22
import {UserState} from '@webex/cc-user-state';
3-
import {IncomingTask, TaskList} from '@webex/cc-task';
3+
import {IncomingTask, TaskList, CallControl} from '@webex/cc-task';
44
import store from '@webex/cc-store';
55

6-
export {StationLogin, UserState, IncomingTask, TaskList, store};
6+
export {StationLogin, UserState, IncomingTask, CallControl, TaskList, store};

packages/contact-center/cc-widgets/src/wc.ts

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import r2wc from '@r2wc/react-to-web-component';
22
import {StationLogin} from '@webex/cc-station-login';
33
import {UserState} from '@webex/cc-user-state';
44
import store from '@webex/cc-store';
5-
import {TaskList, IncomingTask} from '@webex/cc-task';
5+
import {TaskList, IncomingTask, CallControl} from '@webex/cc-task';
66

77
const WebUserState = r2wc(UserState);
88
const WebIncomingTask = r2wc(IncomingTask, {
@@ -26,20 +26,28 @@ const WebStationLogin = r2wc(StationLogin, {
2626
},
2727
});
2828

29-
if (!customElements.get('widget-cc-user-state')) {
30-
customElements.define('widget-cc-user-state', WebUserState);
31-
}
32-
33-
if (!customElements.get('widget-cc-station-login')) {
34-
customElements.define('widget-cc-station-login', WebStationLogin);
35-
}
36-
37-
if (!customElements.get('widget-cc-incoming-task')) {
38-
customElements.define('widget-cc-incoming-task', WebIncomingTask);
39-
}
29+
const WebCallControl = r2wc(CallControl, {
30+
props: {
31+
onHoldResume: 'function',
32+
onEnd: 'function',
33+
onWrapup: 'function',
34+
},
35+
});
4036

41-
if (!customElements.get('widget-cc-task-list')) {
42-
customElements.define('widget-cc-task-list', WebTaskList);
43-
}
37+
// Whenever there is a new component, add the name of the component
38+
// and the web-component to the components object
39+
const components = [
40+
{name: 'widget-cc-user-state', component: WebUserState},
41+
{name: 'widget-cc-station-login', component: WebStationLogin},
42+
{name: 'widget-cc-incoming-task', component: WebIncomingTask},
43+
{name: 'widget-cc-task-list', component: WebTaskList},
44+
{name: 'widget-cc-call-control', component: WebCallControl},
45+
];
46+
47+
components.forEach(({name, component}) => {
48+
if (!customElements.get(name)) {
49+
customElements.define(name, component);
50+
}
51+
});
4452

4553
export {store};

packages/contact-center/station-login/src/station-login/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import StationLoginPresentational from './station-login.presentational';
66
import {useStationLogin} from '../helper';
77
import {StationLoginProps} from './station-login.types';
88

9-
const StationLogin: React.FunctionComponent<StationLoginProps> = observer(({onLogin, onLogout}) => {
9+
const StationLoginComponent: React.FunctionComponent<StationLoginProps> = ({onLogin, onLogout}) => {
1010
const {cc, teams, loginOptions, logger, deviceType, isAgentLoggedIn} = store;
1111
const result = useStationLogin({cc, onLogin, onLogout, logger, isAgentLoggedIn});
1212

@@ -17,6 +17,7 @@ const StationLogin: React.FunctionComponent<StationLoginProps> = observer(({onLo
1717
deviceType,
1818
};
1919
return <StationLoginPresentational {...props} />;
20-
});
20+
};
2121

22+
const StationLogin = observer(StationLoginComponent);
2223
export {StationLogin};

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

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
IdleCode,
99
InitParams,
1010
IStore,
11-
ILogger
11+
ILogger,
12+
IWrapupCode,
1213
} from './store.types';
14+
import {ITask} from '@webex/plugin-cc';
1315

1416
class Store implements IStore {
1517
private static instance: Store;
@@ -20,11 +22,20 @@ class Store implements IStore {
2022
idleCodes: IdleCode[] = [];
2123
agentId: string = '';
2224
selectedLoginOption: string = '';
25+
wrapupCodes: IWrapupCode[] = [];
26+
currentTask: ITask = null;
2327
isAgentLoggedIn = false;
2428
deviceType: string = '';
2529

2630
constructor() {
27-
makeAutoObservable(this, {cc: observable.ref});
31+
makeAutoObservable(this, {
32+
cc: observable.ref,
33+
currentTask: observable, // Make currentTask observable
34+
});
35+
}
36+
37+
setCurrentTask(task: ITask): void {
38+
this.currentTask = task;
2839
}
2940

3041
public static getInstance(): Store {
@@ -44,20 +55,24 @@ class Store implements IStore {
4455
registerCC(webex: WithWebex['webex']): Promise<void> {
4556
this.cc = webex.cc;
4657
this.logger = this.cc.LoggerProxy;
47-
return this.cc.register().then((response: Profile) => {
48-
this.teams = response.teams;
49-
this.loginOptions = response.loginVoiceOptions;
50-
this.idleCodes = response.idleCodes;
51-
this.agentId = response.agentId;
52-
this.isAgentLoggedIn = response.isAgentLoggedIn;
53-
this.deviceType = response.deviceType;
54-
}).catch((error) => {
55-
this.logger.error(`Error registering contact center: ${error}`, {
56-
module: 'cc-store#store.ts',
57-
method: 'registerCC',
58+
return this.cc
59+
.register()
60+
.then((response: Profile) => {
61+
this.teams = response.teams;
62+
this.loginOptions = response.loginVoiceOptions;
63+
this.idleCodes = response.idleCodes;
64+
this.agentId = response.agentId;
65+
this.wrapupCodes = response.wrapupCodes;
66+
this.isAgentLoggedIn = response.isAgentLoggedIn;
67+
this.deviceType = response.deviceType;
68+
})
69+
.catch((error) => {
70+
this.logger.error(`Error registering contact center: ${error}`, {
71+
module: 'cc-store#store.ts',
72+
method: 'registerCC',
73+
});
74+
return Promise.reject(error);
5875
});
59-
return Promise.reject(error);
60-
});
6176
}
6277

6378
init(options: InitParams): Promise<void> {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ interface IStore {
3737
init(params: InitParams): Promise<void>;
3838
}
3939

40+
interface IWrapupCode {
41+
id: string;
42+
name: string;
43+
}
44+
4045

4146
export type {
4247
IContactCenter,
@@ -48,4 +53,5 @@ export type {
4853
InitParams,
4954
IStore,
5055
ILogger,
56+
IWrapupCode
5157
}

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

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { makeAutoObservable } from 'mobx';
1+
import {makeAutoObservable} from 'mobx';
22
import Webex from 'webex';
33
import store from '../src/store'; // Adjust the import path as necessary
44

55
let mockShouldCallback = true;
66

77
jest.mock('mobx', () => ({
88
makeAutoObservable: jest.fn(),
9-
observable: { ref: jest.fn() }
9+
observable: {ref: jest.fn()},
1010
}));
1111

1212
jest.mock('webex', () => ({
@@ -19,10 +19,10 @@ jest.mock('webex', () => ({
1919
cc: {
2020
register: jest.fn(),
2121
LoggerProxy: {
22-
error: jest.fn()
23-
}
24-
}
25-
}))
22+
error: jest.fn(),
23+
},
24+
},
25+
})),
2626
}));
2727

2828
describe('Store', () => {
@@ -45,18 +45,18 @@ describe('Store', () => {
4545
expect(store.loginOptions).toEqual([]);
4646
expect(store.isAgentLoggedIn).toBe(false);
4747
expect(store.deviceType).toBe('');
48-
expect(makeAutoObservable).toHaveBeenCalledWith(store, { cc: expect.any(Function) });
48+
expect(makeAutoObservable).toHaveBeenCalledWith(store, {cc: expect.any(Function), currentTask: expect.any(Object)});
4949
});
5050

5151
describe('registerCC', () => {
5252
it('should initialise store values on successful register', async () => {
5353
const mockResponse = {
54-
teams: [{ id: 'team1', name: 'Team 1' }],
54+
teams: [{id: 'team1', name: 'Team 1'}],
5555
loginVoiceOptions: ['option1', 'option2'],
56-
idleCodes: [{ id: 'code1', name: 'Code 1', isSystem: false, isDefault: false }],
56+
idleCodes: [{id: 'code1', name: 'Code 1', isSystem: false, isDefault: false}],
5757
agentId: 'agent1',
5858
isAgentLoggedIn: true,
59-
deviceType: 'BROWSER'
59+
deviceType: 'BROWSER',
6060
};
6161
mockWebex.cc.register.mockResolvedValue(mockResponse);
6262

@@ -76,20 +76,19 @@ describe('Store', () => {
7676

7777
try {
7878
await store.registerCC(mockWebex);
79-
}
80-
catch (error) {
79+
} catch (error) {
8180
expect(error).toEqual(mockError);
82-
expect(store.logger.error).toHaveBeenCalledWith("Error registering contact center: Error: Register failed", {
83-
"method": "registerCC",
84-
"module": "cc-store#store.ts",
81+
expect(store.logger.error).toHaveBeenCalledWith('Error registering contact center: Error: Register failed', {
82+
method: 'registerCC',
83+
module: 'cc-store#store.ts',
8584
});
8685
}
8786
});
8887
});
8988

9089
describe('init', () => {
9190
it('should call registerCC if webex is in options', async () => {
92-
const initParams = { webex: mockWebex };
91+
const initParams = {webex: mockWebex};
9392
jest.spyOn(store, 'registerCC').mockResolvedValue();
9493
Webex.init.mockClear();
9594

@@ -101,24 +100,24 @@ describe('Store', () => {
101100

102101
it('should initialize webex and call registerCC on ready event', async () => {
103102
const initParams = {
104-
webexConfig: { anyConfig: true },
105-
access_token: 'fake_token'
103+
webexConfig: {anyConfig: true},
104+
access_token: 'fake_token',
106105
};
107106
jest.spyOn(store, 'registerCC').mockResolvedValue();
108107

109108
await store.init(initParams);
110109

111110
expect(Webex.init).toHaveBeenCalledWith({
112111
config: initParams.webexConfig,
113-
credentials: { access_token: initParams.access_token }
112+
credentials: {access_token: initParams.access_token},
114113
});
115114
expect(store.registerCC).toHaveBeenCalledWith(expect.any(Object));
116115
});
117116

118117
it('should reject the promise if registerCC fails in init method', async () => {
119118
const initParams = {
120-
webexConfig: { anyConfig: true },
121-
access_token: 'fake_token'
119+
webexConfig: {anyConfig: true},
120+
access_token: 'fake_token',
122121
};
123122

124123
jest.spyOn(store, 'registerCC').mockRejectedValue(new Error('registerCC failed'));
@@ -128,8 +127,8 @@ describe('Store', () => {
128127

129128
it('should reject the promise if Webex SDK fails to initialize', async () => {
130129
const initParams = {
131-
webexConfig: { anyConfig: true },
132-
access_token: 'fake_token'
130+
webexConfig: {anyConfig: true},
131+
access_token: 'fake_token',
133132
};
134133

135134
mockShouldCallback = false;
@@ -143,4 +142,4 @@ describe('Store', () => {
143142
await expect(initPromise).rejects.toThrow('Webex SDK failed to initialize');
144143
});
145144
});
146-
});
145+
});
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import React, {useState} from 'react';
2+
import {WrapupCodes} from '@webex/cc-store';
3+
4+
import {CallControlPresentationalProps} from '../task.types';
5+
import './call-control.styles.scss';
6+
7+
function CallControlPresentational(props: CallControlPresentationalProps) {
8+
const [isHeld, setIsHeld] = useState(false);
9+
const [isRecording, setIsRecording] = useState(true);
10+
const [selectedWrapupReason, setSelectedWrapupReason] = useState<string | null>(null);
11+
const [selectedWrapupId, setSelectedWrapupId] = useState<string | null>(null);
12+
13+
const {currentTask, audioRef, toggleHold, toggleRecording, endCall, wrapupCall, wrapupCodes, wrapupRequired} = props;
14+
15+
const handletoggleHold = () => {
16+
toggleHold(!isHeld);
17+
setIsHeld(!isHeld);
18+
};
19+
20+
const handletoggleRecording = () => {
21+
toggleRecording(isRecording);
22+
setIsRecording(!isRecording);
23+
};
24+
25+
const handleWrapupCall = () => {
26+
if (selectedWrapupReason && selectedWrapupId) {
27+
wrapupCall(selectedWrapupReason, selectedWrapupId);
28+
setSelectedWrapupReason('');
29+
}
30+
};
31+
32+
const handleWrapupChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
33+
const {text, value} = event.target.options[event.target.selectedIndex];
34+
setSelectedWrapupReason(text);
35+
setSelectedWrapupId(value);
36+
};
37+
38+
return (
39+
<>
40+
<audio ref={audioRef} id="remote-audio" autoPlay></audio>
41+
{currentTask && (
42+
<div className="box">
43+
<section className="section-box">
44+
<fieldset className="fieldset">
45+
<legend className="legend-box">Call Control</legend>
46+
<div style={{display: 'flex', flexDirection: 'column', flexGrow: 1}}>
47+
<div style={{display: 'flex', gap: '1rem'}}>
48+
<button className="btn" onClick={handletoggleHold} disabled={wrapupRequired}>
49+
{isHeld ? 'Resume' : 'Hold'}
50+
</button>
51+
<button className="btn" onClick={handletoggleRecording} disabled={wrapupRequired}>
52+
{isRecording ? 'Pause Recording' : 'Resume Recording'}
53+
</button>
54+
<button className="btn" onClick={endCall} disabled={wrapupRequired}>
55+
End
56+
</button>
57+
</div>
58+
<div style={{display: 'flex', gap: '1rem', marginTop: '1rem'}}>
59+
<select className="select" onChange={handleWrapupChange} disabled={!wrapupRequired}>
60+
<option value="">Select the wrap-up reason</option>
61+
{wrapupCodes.map((wrapup: WrapupCodes) => (
62+
<option key={wrapup.id} value={wrapup.id}>
63+
{wrapup.name}
64+
</option>
65+
))}
66+
</select>
67+
<button
68+
className="btn"
69+
onClick={handleWrapupCall}
70+
disabled={!wrapupRequired && !selectedWrapupReason}
71+
>
72+
Wrap Up
73+
</button>
74+
</div>
75+
</div>
76+
</fieldset>
77+
</section>
78+
</div>
79+
)}
80+
</>
81+
);
82+
}
83+
84+
export default CallControlPresentational;

0 commit comments

Comments
 (0)