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
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
Comment thread
rarajes2 marked this conversation as resolved.
});
}

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)});
Comment thread
rarajes2 marked this conversation as resolved.
});

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';
Comment thread
rarajes2 marked this conversation as resolved.

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 && (
Comment thread
rarajes2 marked this conversation as resolved.
<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