Skip to content

fix(call-control): add-call-control-widget #362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jan 29, 2025
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
4 changes: 2 additions & 2 deletions packages/contact-center/cc-widgets/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {StationLogin} from '@webex/cc-station-login';
import {UserState} from '@webex/cc-user-state';
import {IncomingTask, TaskList} from '@webex/cc-task';
import {IncomingTask, TaskList, CallControl} from '@webex/cc-task';
import store from '@webex/cc-store';

export {StationLogin, UserState, IncomingTask, TaskList, store};
export {StationLogin, UserState, IncomingTask, CallControl, TaskList, store};
38 changes: 23 additions & 15 deletions packages/contact-center/cc-widgets/src/wc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import r2wc from '@r2wc/react-to-web-component';
import {StationLogin} from '@webex/cc-station-login';
import {UserState} from '@webex/cc-user-state';
import store from '@webex/cc-store';
import {TaskList, IncomingTask} from '@webex/cc-task';
import {TaskList, IncomingTask, CallControl} from '@webex/cc-task';

const WebUserState = r2wc(UserState);
const WebIncomingTask = r2wc(IncomingTask, {
Expand All @@ -26,20 +26,28 @@ const WebStationLogin = r2wc(StationLogin, {
},
});

if (!customElements.get('widget-cc-user-state')) {
customElements.define('widget-cc-user-state', WebUserState);
}

if (!customElements.get('widget-cc-station-login')) {
customElements.define('widget-cc-station-login', WebStationLogin);
}

if (!customElements.get('widget-cc-incoming-task')) {
customElements.define('widget-cc-incoming-task', WebIncomingTask);
}
const WebCallControl = r2wc(CallControl, {
props: {
onHoldResume: 'function',
onEnd: 'function',
onWrapup: 'function',
},
});

if (!customElements.get('widget-cc-task-list')) {
customElements.define('widget-cc-task-list', WebTaskList);
}
// Whenever there is a new component, add the name of the component
// and the web-component to the components object
const components = [
{name: 'widget-cc-user-state', component: WebUserState},
{name: 'widget-cc-station-login', component: WebStationLogin},
{name: 'widget-cc-incoming-task', component: WebIncomingTask},
{name: 'widget-cc-task-list', component: WebTaskList},
{name: 'widget-cc-call-control', component: WebCallControl},
];

components.forEach(({name, component}) => {
if (!customElements.get(name)) {
customElements.define(name, component);
}
});

export {store};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import StationLoginPresentational from './station-login.presentational';
import {useStationLogin} from '../helper';
import {StationLoginProps} from './station-login.types';

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

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

const StationLogin = observer(StationLoginComponent);
export {StationLogin};
45 changes: 30 additions & 15 deletions packages/contact-center/store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
IdleCode,
InitParams,
IStore,
ILogger
ILogger,
IWrapupCode,
} from './store.types';
import {ITask} from '@webex/plugin-cc';

class Store implements IStore {
private static instance: Store;
Expand All @@ -20,11 +22,20 @@ class Store implements IStore {
idleCodes: IdleCode[] = [];
agentId: string = '';
selectedLoginOption: string = '';
wrapupCodes: IWrapupCode[] = [];
currentTask: ITask = null;
isAgentLoggedIn = false;
deviceType: string = '';

constructor() {
makeAutoObservable(this, {cc: observable.ref});
makeAutoObservable(this, {
cc: observable.ref,
currentTask: observable, // Make currentTask observable
});
}

setCurrentTask(task: ITask): void {
this.currentTask = task;
}

public static getInstance(): Store {
Expand All @@ -44,20 +55,24 @@ class Store implements IStore {
registerCC(webex: WithWebex['webex']): Promise<void> {
this.cc = webex.cc;
this.logger = this.cc.LoggerProxy;
return this.cc.register().then((response: Profile) => {
this.teams = response.teams;
this.loginOptions = response.loginVoiceOptions;
this.idleCodes = response.idleCodes;
this.agentId = response.agentId;
this.isAgentLoggedIn = response.isAgentLoggedIn;
this.deviceType = response.deviceType;
}).catch((error) => {
this.logger.error(`Error registering contact center: ${error}`, {
module: 'cc-store#store.ts',
method: 'registerCC',
return this.cc
.register()
.then((response: Profile) => {
this.teams = response.teams;
this.loginOptions = response.loginVoiceOptions;
this.idleCodes = response.idleCodes;
this.agentId = response.agentId;
this.wrapupCodes = response.wrapupCodes;
this.isAgentLoggedIn = response.isAgentLoggedIn;
this.deviceType = response.deviceType;
})
.catch((error) => {
this.logger.error(`Error registering contact center: ${error}`, {
module: 'cc-store#store.ts',
method: 'registerCC',
});
return Promise.reject(error);
});
return Promise.reject(error);
});
}

init(options: InitParams): Promise<void> {
Expand Down
6 changes: 6 additions & 0 deletions packages/contact-center/store/src/store.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ interface IStore {
init(params: InitParams): Promise<void>;
}

interface IWrapupCode {
id: string;
name: string;
}


export type {
IContactCenter,
Expand All @@ -48,4 +53,5 @@ export type {
InitParams,
IStore,
ILogger,
IWrapupCode
}
47 changes: 23 additions & 24 deletions packages/contact-center/store/tests/store.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { makeAutoObservable } from 'mobx';
import {makeAutoObservable} from 'mobx';
import Webex from 'webex';
import store from '../src/store'; // Adjust the import path as necessary

let mockShouldCallback = true;

jest.mock('mobx', () => ({
makeAutoObservable: jest.fn(),
observable: { ref: jest.fn() }
observable: {ref: jest.fn()},
}));

jest.mock('webex', () => ({
Expand All @@ -19,10 +19,10 @@ jest.mock('webex', () => ({
cc: {
register: jest.fn(),
LoggerProxy: {
error: jest.fn()
}
}
}))
error: jest.fn(),
},
},
})),
}));

describe('Store', () => {
Expand All @@ -45,18 +45,18 @@ describe('Store', () => {
expect(store.loginOptions).toEqual([]);
expect(store.isAgentLoggedIn).toBe(false);
expect(store.deviceType).toBe('');
expect(makeAutoObservable).toHaveBeenCalledWith(store, { cc: expect.any(Function) });
expect(makeAutoObservable).toHaveBeenCalledWith(store, {cc: expect.any(Function), currentTask: expect.any(Object)});
});

describe('registerCC', () => {
it('should initialise store values on successful register', async () => {
const mockResponse = {
teams: [{ id: 'team1', name: 'Team 1' }],
teams: [{id: 'team1', name: 'Team 1'}],
loginVoiceOptions: ['option1', 'option2'],
idleCodes: [{ id: 'code1', name: 'Code 1', isSystem: false, isDefault: false }],
idleCodes: [{id: 'code1', name: 'Code 1', isSystem: false, isDefault: false}],
agentId: 'agent1',
isAgentLoggedIn: true,
deviceType: 'BROWSER'
deviceType: 'BROWSER',
};
mockWebex.cc.register.mockResolvedValue(mockResponse);

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

try {
await store.registerCC(mockWebex);
}
catch (error) {
} catch (error) {
expect(error).toEqual(mockError);
expect(store.logger.error).toHaveBeenCalledWith("Error registering contact center: Error: Register failed", {
"method": "registerCC",
"module": "cc-store#store.ts",
expect(store.logger.error).toHaveBeenCalledWith('Error registering contact center: Error: Register failed', {
method: 'registerCC',
module: 'cc-store#store.ts',
});
}
});
});

describe('init', () => {
it('should call registerCC if webex is in options', async () => {
const initParams = { webex: mockWebex };
const initParams = {webex: mockWebex};
jest.spyOn(store, 'registerCC').mockResolvedValue();
Webex.init.mockClear();

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

it('should initialize webex and call registerCC on ready event', async () => {
const initParams = {
webexConfig: { anyConfig: true },
access_token: 'fake_token'
webexConfig: {anyConfig: true},
access_token: 'fake_token',
};
jest.spyOn(store, 'registerCC').mockResolvedValue();

await store.init(initParams);

expect(Webex.init).toHaveBeenCalledWith({
config: initParams.webexConfig,
credentials: { access_token: initParams.access_token }
credentials: {access_token: initParams.access_token},
});
expect(store.registerCC).toHaveBeenCalledWith(expect.any(Object));
});

it('should reject the promise if registerCC fails in init method', async () => {
const initParams = {
webexConfig: { anyConfig: true },
access_token: 'fake_token'
webexConfig: {anyConfig: true},
access_token: 'fake_token',
};

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

it('should reject the promise if Webex SDK fails to initialize', async () => {
const initParams = {
webexConfig: { anyConfig: true },
access_token: 'fake_token'
webexConfig: {anyConfig: true},
access_token: 'fake_token',
};

mockShouldCallback = false;
Expand All @@ -143,4 +142,4 @@ describe('Store', () => {
await expect(initPromise).rejects.toThrow('Webex SDK failed to initialize');
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, {useState} from 'react';
import {WrapupCodes} from '@webex/cc-store';

import {CallControlPresentationalProps} from '../task.types';
import './call-control.styles.scss';

function CallControlPresentational(props: CallControlPresentationalProps) {
const [isHeld, setIsHeld] = useState(false);
const [isRecording, setIsRecording] = useState(true);
const [selectedWrapupReason, setSelectedWrapupReason] = useState<string | null>(null);
const [selectedWrapupId, setSelectedWrapupId] = useState<string | null>(null);

const {currentTask, audioRef, toggleHold, toggleRecording, endCall, wrapupCall, wrapupCodes, wrapupRequired} = props;

const handletoggleHold = () => {
toggleHold(!isHeld);
setIsHeld(!isHeld);
};

const handletoggleRecording = () => {
toggleRecording(isRecording);
setIsRecording(!isRecording);
};

const handleWrapupCall = () => {
if (selectedWrapupReason && selectedWrapupId) {
wrapupCall(selectedWrapupReason, selectedWrapupId);
setSelectedWrapupReason('');
}
};

const handleWrapupChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const {text, value} = event.target.options[event.target.selectedIndex];
setSelectedWrapupReason(text);
setSelectedWrapupId(value);
};

return (
<>
<audio ref={audioRef} id="remote-audio" autoPlay></audio>
{currentTask && (
<div className="box">
<section className="section-box">
<fieldset className="fieldset">
<legend className="legend-box">Call Control</legend>
<div style={{display: 'flex', flexDirection: 'column', flexGrow: 1}}>
<div style={{display: 'flex', gap: '1rem'}}>
<button className="btn" onClick={handletoggleHold} disabled={wrapupRequired}>
{isHeld ? 'Resume' : 'Hold'}
</button>
<button className="btn" onClick={handletoggleRecording} disabled={wrapupRequired}>
{isRecording ? 'Pause Recording' : 'Resume Recording'}
</button>
<button className="btn" onClick={endCall} disabled={wrapupRequired}>
End
</button>
</div>
<div style={{display: 'flex', gap: '1rem', marginTop: '1rem'}}>
<select className="select" onChange={handleWrapupChange} disabled={!wrapupRequired}>
<option value="">Select the wrap-up reason</option>
{wrapupCodes.map((wrapup: WrapupCodes) => (
<option key={wrapup.id} value={wrapup.id}>
{wrapup.name}
</option>
))}
</select>
<button
className="btn"
onClick={handleWrapupCall}
disabled={!wrapupRequired && !selectedWrapupReason}
>
Wrap Up
</button>
</div>
</div>
</fieldset>
</section>
</div>
)}
</>
);
}

export default CallControlPresentational;
Loading