Skip to content

feat(cc-components): setup and move user state sample ui comp #359

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 18 commits into from
Jan 31, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pids
*.pid
*.seed
*.out
docs/

#Logs and others
*.swp
Expand Down
3 changes: 3 additions & 0 deletions packages/contact-center/cc-components/.babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const baseConfig = require('../../../.babelrc');

module.exports = baseConfig;
70 changes: 70 additions & 0 deletions packages/contact-center/cc-components/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"name": "@webex/cc-components",
"description": "Webex Contact Center UI Components Library for your custom contact center solutions",
"version": "1.28.0-ccwidgets.9",
"main": "dist/index.js",
"publishConfig": {
"access": "public"
},
"files": [
"dist/",
"package.json"
],
"exports": {
".": "./dist/index.js",
"./wc": "./dist/wc.js"
},
"scripts": {
"clean": "rm -rf dist && rm -rf node_modules",
"clean:dist": "rm -rf dist",
"build": "yarn run -T tsc",
"build:src": "yarn run clean:dist && webpack",
"build:watch": "webpack --watch",
"test:unit": "jest"
},
"dependencies": {
"@r2wc/react-to-web-component": "2.0.3",
"@webex/cc-store": "workspace:*"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this dependency here? The presentational layer should not be directly using the outer states

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We need this for a type import. On bundle, the whole cc-store won't come in as the tree-shaking would happen.

},
"devDependencies": {
"@babel/core": "7.25.2",
"@babel/preset-env": "7.25.4",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.25.9",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.6.2",
"@testing-library/react": "16.0.1",
"@types/jest": "29.5.14",
"@types/react-test-renderer": "18",
"babel-jest": "29.7.0",
"babel-loader": "9.2.1",
"file-loader": "6.2.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"ts-loader": "9.5.1",
"typescript": "5.6.3",
"webpack": "5.94.0",
"webpack-cli": "5.1.4",
"webpack-merge": "6.0.1"
},
"jest": {
"testEnvironment": "jsdom",
"testMatch": [
"**/tests/**/*.ts",
"**/tests/**/*.tsx"
],
"moduleNameMapper": {
"^.+\\.(css|less|scss)$": "babel-jest"
}
},
"stableVersion": "1.28.0-ccwidgets.8",
"peerDependencies": {
"@momentum-ui/core": ">=19.16.0",
"@momentum-ui/icons": ">=8.28.5",
"@momentum-ui/tokens": ">=1.7.1",
"@momentum-ui/utils": ">=6.2.15",
"@momentum-ui/web-components": ">=2.16.16",
"react": ">=18.3.1",
"react-dom": ">=18.3.1"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
.light-theme {
&.box {
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 20px;
max-width: 800px;
margin: 0 auto;
}

.sectionBox {
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
}

.fieldset {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
margin-bottom: 20px;
position: relative;
}

.legendBox {
font-weight: bold;
color: #0052bf;
}

.btn {
padding: 10px 20px;
background-color: #0052bf;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
margin-right: 8px;
}

.select {
width: 100%;
padding: 8px;
margin-top: 8px;
margin-bottom: 12px;
border: 1px solid #ccc;
border-radius: 4px;
}

.input {
width: 97%;
padding: 8px;
margin-top: 8px;
margin-bottom: 12px;
border: 1px solid #ccc;
border-radius: 4px;
}

.elapsedTime {
position: absolute;
right: 30px;
top: 25px;
color: black; /* Replace with dynamic styling as needed */
}

.elapsedTime-disabled {
color: #ccc;
}
}

.dark-theme {
&.box {
background-color: #1e1e1e; /* Dark background */
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5); /* Darker shadow */
padding: 20px;
max-width: 800px;
margin: 0 auto;
}

.sectionBox {
padding: 10px;
border: 1px solid #444; /* Darker border */
border-radius: 8px;
}

.fieldset {
border: 1px solid #555; /* Darker border */
border-radius: 5px;
padding: 10px;
margin-bottom: 20px;
position: relative;
}

.legendBox {
font-weight: bold;
color: #79a6d2; /* Lighter blue for dark theme */
}

.btn {
padding: 10px 20px;
background-color: #79a6d2; /* Lighter blue */
color: #1e1e1e; /* Dark text on light button */
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
margin-right: 8px;
}

.select {
width: 100%;
padding: 8px;
margin-top: 8px;
margin-bottom: 12px;
border: 1px solid #555; /* Darker border */
border-radius: 4px;
background-color: #2a2a2a; /* Dark background */
color: #fff; /* Light text */
}

.input {
width: 97%;
padding: 8px;
margin-top: 8px;
margin-bottom: 12px;
border: 1px solid #555; /* Darker border */
border-radius: 4px;
background-color: #2a2a2a; /* Dark background */
color: #fff; /* Light text */
}

.elapsedTime {
position: absolute;
right: 30px;
top: 25px;
color: #ddd; /* Light text for visibility */
}

.elapsedTime-disabled {
color: #777; /* Darker gray for disabled state */
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React from 'react';

import {IUserState} from './user-state.types';
import {formatTime} from '../../utils'

import './user-state.scss';

const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
const {idleCodes,setAgentStatus,isSettingAgentStatus, errorMessage, elapsedTime, currentState, currentTheme} = props;

return (
<div className={`box ${currentTheme === 'DARK' ? 'dark-theme' : 'light-theme'}`}>
<section className='sectionBox'>
<fieldset className='fieldset'>
<legend data-testid='user-state-title' className='legendBox'>Agent State</legend>
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }} />
<select
id="idleCodes"
value={currentState.id}
className='select'
onChange={
(event) => {
const code = idleCodes?.filter(code => code.id === event.target.value)[0];
setAgentStatus(code);
}
}
disabled={isSettingAgentStatus}
>
{idleCodes?.filter(code => !code.isSystem).map((code) => {
return (
<option
key={code.id}
value={code.id}
>
{code.name}
</option>
);
})}
</select>
{/* @ts-ignore */}
<md-button color={`${currentTheme === 'DARK' ? 'white' : 'dark-grey'}`}>Test Button</md-button>
<div className={`elapsedTime ${isSettingAgentStatus ? 'elapsedTime-disabled' : ''}`}>{formatTime(elapsedTime)}</div>
{
errorMessage && <div style={{color: 'red'}}>{errorMessage}</div>
}
</fieldset>
</section>
</div>
);
};

export default UserStateComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ export interface IUserState {
* @returns void
*/
setCurrentState: (state: IdleCode) => void;

/**
* The preferred theme
*/
currentTheme: string;
}
7 changes: 7 additions & 0 deletions packages/contact-center/cc-components/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import UserStateComponent from './components/UserState/user-state';
import {IUserState} from './components/UserState/user-state.types';
import '@momentum-ui/icons/css/momentum-ui-icons.min.css';
import '@momentum-ui/core/css/momentum-ui.min.css';
import '@momentum-ui/web-components';

export {UserStateComponent, type IUserState};
6 changes: 6 additions & 0 deletions packages/contact-center/cc-components/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const formatTime = (time: number): string => {
const hours = Math.floor(time / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = time % 60;
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
11 changes: 11 additions & 0 deletions packages/contact-center/cc-components/src/wc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import r2wc from '@r2wc/react-to-web-component';
import UserStateComponent from './components/UserState/user-state';
import '@momentum-ui/web-components';
import '@momentum-ui/icons/css/momentum-ui-icons.min.css';
import '@momentum-ui/core/css/momentum-ui.min.css';

const WebUserState = r2wc(UserStateComponent);

if (!customElements.get('component-cc-user-state')) {
customElements.define('component-cc-user-state', WebUserState);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import UserStatePresentational from '../../src/user-state/user-state.presentational';
import UserStateComponent from '../../../src/components/UserState/user-state';

describe('UserStatePresentational Component', () => {
describe('UserStateComponent', () => {
const mockSetAgentStatus = jest.fn();
const mockSetCurrentState = jest.fn();
const defaultProps = {
Expand All @@ -17,46 +17,47 @@ describe('UserStatePresentational Component', () => {
errorMessage: '',
elapsedTime: 3661, // 1 hour, 1 minute, 1 second
currentState: { id: '1' },
setCurrentState: mockSetCurrentState
setCurrentState: mockSetCurrentState,
currentTheme: 'LIGHT'
};

it('should render the component with correct elements', () => {
render(<UserStatePresentational {...defaultProps} />);
render(<UserStateComponent {...defaultProps} />);
expect(screen.getByTestId('user-state-title')).toHaveTextContent('Agent State');
expect(screen.getByRole('combobox')).toBeInTheDocument();
expect(screen.getByText('01:01:01')).toBeInTheDocument();
});

it('should render only non-system idle codes in the dropdown', () => {
render(<UserStatePresentational {...defaultProps} />);
render(<UserStateComponent {...defaultProps} />);
const options = screen.getAllByRole('option');
expect(options).toHaveLength(2);
expect(options[0]).toHaveTextContent('Idle Code 1');
expect(options[1]).toHaveTextContent('Idle Code 3');
});

it('should call setAgentStatus with correct code when an idle code is selected', () => {
render(<UserStatePresentational {...defaultProps} />);
render(<UserStateComponent {...defaultProps} />);
fireEvent.change(screen.getByRole('combobox'), { target: { value: '3' } });
expect(mockSetAgentStatus).toHaveBeenCalledWith({ id: '3', name: 'Idle Code 3', isSystem: false });
});

it('should display an error message if provided', () => {
render(<UserStatePresentational {...defaultProps} errorMessage="Error message" />);
render(<UserStateComponent {...defaultProps} errorMessage="Error message" />);
expect(screen.getByText('Error message')).toBeInTheDocument();
expect(screen.getByText('Error message')).toHaveStyle('color: red');
});

it('should disable the select box when isSettingAgentStatus is true', () => {
render(<UserStatePresentational {...{ ...defaultProps, isSettingAgentStatus: true }} />);
render(<UserStateComponent {...{ ...defaultProps, isSettingAgentStatus: true }} />);
expect(screen.getByRole('combobox')).toBeDisabled();
});

it('should render elapsed time in correct color based on isSettingAgentStatus', () => {
const { rerender } = render(<UserStatePresentational {...defaultProps} />);
expect(screen.getByText('01:01:01')).toHaveStyle('color: black');
const { rerender } = render(<UserStateComponent {...defaultProps} />);
expect(screen.getByText('01:01:01')).toHaveClass('elapsedTime');

rerender(<UserStatePresentational {...{ ...defaultProps, isSettingAgentStatus: true }} />);
expect(screen.getByText('01:01:01')).toHaveStyle('color: grey');
rerender(<UserStateComponent {...{ ...defaultProps, isSettingAgentStatus: true }} />);
expect(screen.getByText('01:01:01')).toHaveClass('elapsedTime elapsedTime-disabled');
});
});
Loading