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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Outbound Dial Labels and/or Strings
export const OutdialStrings = {
ANI_SELECT_LABEL: 'Outdial ANI',
ANI_SELECT_PLACEHOLDER: 'Enter Outdial ANI',
CALL_BUTTON_ARIA_LABEL: 'Start Outdial Call',
DN_PLACEHOLDER: 'Enter number to dial',
INCORRECT_DN_FORMAT: 'Incorrect format.',
OUTDIAL_CALL: 'Outdial Call',
};

// Utility Constants
export const KEY_LIST = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'];
Original file line number Diff line number Diff line change
@@ -1,67 +1,34 @@
.out-dial-call-box {
position: relative;
width: 100%;
background-color: var(--mds-color-theme-background-primary-normal);
border-radius: 0.5rem;
box-shadow: var(--mds-shadow-2);
padding: 1.25rem;
max-width: 50rem;
}

.out-dial-call-section-box {
padding: 0.625rem;
}

.out-dial-call-fieldset {
border: 1px solid var(--mds-color-theme-outline-secondary-normal);
border-radius: 0.325rem;
padding: 0.625rem;
margin-bottom: 1.25rem;
}

.out-dial-call-legend-box {
font-weight: bold;
color: var(--mds-color-theme-text-primary-normal);
}

.keypad {
display: flex;
flex-direction: column;
align-items: center;
background: var(--mds-color-theme-background-secondary-normal);
padding: 1.25rem;
border-radius: 0.625rem;
width: 15.625rem;

input {
width: 100%;
padding: 0.625rem;
text-align: center;
font-size: 1.125rem;
margin-bottom: 0.625rem;
background: var(--mds-color-theme-background-primary-normal);
border: 1px solid var(--mds-color-theme-outline-secondary-normal);
border-radius: 0.25rem;
color: var(--mds-color-theme-text-primary-normal);

mdc-input {
padding-bottom: 0; // default is 1 rem, 1.5 rem needed but provided by .keys
}

.outdial-input {
width: 100%;
}

.keys {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.625rem;
row-gap: 1rem;
column-gap: 1.5rem;
padding: 1.5rem 0;
list-style-type: none;
margin: 0;
}

.key {
width: 3.75rem;
height: 3.75rem;
display: flex;
align-items: center;
justify-content: center;
background: var(--mds-color-theme-background-primary-normal);
background: var(--mds-color-theme-background-alert-default-normal);
color: var(--mds-color-theme-text-primary-normal);
font-size: 1.25rem;
border-radius: 50%;
cursor: pointer;

transition: background-color 0.2s ease;

&:hover {
Expand All @@ -72,22 +39,4 @@
background: var(--mds-color-theme-background-primary-active);
}
}
}

.out-dial-call-btn {
margin-top: 0.625rem;
background: var(--mds-color-theme-background-alert-success-normal);
color: var(--mds-color-theme-text-primary-normal);
padding: 0.625rem 1.25rem;
border-radius: 50%;
cursor: pointer;
transition: background-color 0.2s ease;

&:hover {
background: var(--mds-color-theme-background-alert-success-hover);
}

&:active {
background: var(--mds-color-theme-background-alert-success-active);
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,135 @@
import React, {useState} from 'react';
import {OutdialCallComponentProps} from '../task.types';
import React, {useEffect, useMemo, useState} from 'react';
import {OutdialAniEntry, OutdialCallComponentProps} from '../task.types';
import './outdial-call.style.scss';
import {withMetrics} from '@webex/cc-ui-logging';
import {Input, Button, Option, Select} from '@momentum-design/components/dist/react';
import {OutdialStrings, KEY_LIST} from './constants';

/**
* OutdialCallComponent renders a dialpad UI for agents to initiate outbound calls.
* It allows input of a destination number, selection of an ANI, and validates input.
*
* This component provides a keypad interface for entering a destination number, validates the input,
* allows selection of an ANI (Automatic Number Identification), and triggers an outbound call action.
*
* @param props - Properties for the OutdialCallComponent.
* @property startOutdial - Function to initiate the outdial call with the entered destination number.
*/
const OutdialCallComponent: React.FunctionComponent<OutdialCallComponentProps> = (props) => {
const {startOutdial} = props;
const {logger, startOutdial, getOutdialANIEntries} = props;

// State Hooks
const [destination, setDestination] = useState('');
const [isValidNumber, setIsValidNumber] = useState('');
const [selectedANI, setSelectedANI] = useState(undefined);
const [outdialANIList, setOutdialANIList] = useState<OutdialAniEntry[]>([]);

// Validate the input format using regex from agent desktop
const regExForDnSpecialChars = useMemo(
() => new RegExp('^[+1][0-9]{3,18}$|^[*#][+1][0-9*#:]{3,18}$|^[0-9*#]{3,18}$'),
[]
);

// useEffect and useState to allow for async fetching of outdial ANI entries
useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about pulling entries for address book ? When the outdial component loads, we have 3 options in Agent desktop currently: Dialpad, searching from address book and organization search so since address book is available now would be good to add that as well here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Address book is not part of the scope of my current story, that can be added at a later date. This PR is to update the Outdial Dialpad

// Give Select an empty list if outdial ANI entries are not provided
const updateOutdialANIList = async () => {
try {
const result = await getOutdialANIEntries();
setOutdialANIList(result);
} catch (error) {
logger?.error(`CC-Widgets: Task: Error fetching outdial ANI entries: ${error}`, {
module: 'OutdialCallComponent',
method: 'updateOutdialANIList',
});
setOutdialANIList([]);
}
};
updateOutdialANIList();
}, []);

const updateOutboundNumber = (e: React.ChangeEvent<HTMLInputElement>) => {
// Allow only valid input that is digits, #, *, and +
const VALID_KEYPAD_CHARS = /[\d#*+]/g;
const filteredValue = e.target.value.match(VALID_KEYPAD_CHARS)?.join('') || '';
setDestination(filteredValue);
/**
* validateOutboundNumber
* @param value the dial number to validate
* If the input is invalid, sets an error message on dial number input
*/
const validateOutboundNumber = (value: string) => {
if (value && !regExForDnSpecialChars.test(value)) {
setIsValidNumber(OutdialStrings.INCORRECT_DN_FORMAT);
} else {
setIsValidNumber('');
}
};

// Function to press a key on the outdial keypad.
const handelKeyPress = (value: string) => {
setDestination((prev) => prev + value);
/**
* handleOnClick
* @param value The key value pressed
* Appends the pressed key to the destination input field
*/
const handleOnClick = (value: string) => {
setDestination(destination + value);
validateOutboundNumber(destination + value);
};

return (
<div className="out-dial-call-box">
<section className="out-dial-call-section-box">
<fieldset className="out-dial-call-fieldset">
<legend className="out-dial-call-legend-box">Outdial Call</legend>
<div className="keypad">
<input
onChange={updateOutboundNumber}
id="outBoundDialNumber"
placeholder="Enter number to dial"
value={destination}
/>
<div className="keys">
{['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'].map((key) => (
<div key={key} className="key" onClick={() => handelKeyPress(key)}>
{key}
</div>
))}
</div>
<button className="out-dial-call-btn" onClick={() => startOutdial(destination)}>
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M6.62 10.79a15.053 15.053 0 006.59 6.59l2.2-2.2a1 1 0 011.11-.27c1.12.45 2.33.69 3.58.69a1 1 0 011 1v3.5a1 1 0 01-1 1C10.29 21 3 13.71 3 4.5a1 1 0 011-1h3.5a1 1 0 011 1c0 1.25.24 2.46.69 3.58a1 1 0 01-.27 1.11l-2.2 2.2z" />
</svg>
</button>
</div>
</fieldset>
</section>
</div>
<article className="keypad" data-testid="outdial-call-container">
<Input
Comment thread
brain-frog marked this conversation as resolved.
className="outdial-input"
id="outdial-number-input"
name="outdial-number-input"
data-testid="outdial-number-input"
helpText={isValidNumber}
helpTextType={isValidNumber ? 'error' : 'default'}
placeholder={OutdialStrings.DN_PLACEHOLDER}
value={destination}
onChange={(e: unknown) => {
const inputValue = (e as React.ChangeEvent<HTMLInputElement>).target.value;
setDestination(inputValue);
validateOutboundNumber(inputValue);
}}
/>
<ul className="keys" data-testid="outdial-keypad-keys">
{KEY_LIST.map((key) => (
<li key={key}>
<Button className="key button" onClick={() => handleOnClick(key)}>
{key}
</Button>
</li>
))}
</ul>
<Select
className="outdial-input"
label={OutdialStrings.ANI_SELECT_LABEL}
id="outdial-ani-option-select"
name="outdial-ani-option-select"
data-testid="outdial-ani-option-select"
placeholder={OutdialStrings.ANI_SELECT_PLACEHOLDER}
onChange={(event: CustomEvent) => {
setSelectedANI(event.detail.value);
}}
>
{outdialANIList.map((option: OutdialAniEntry, index: number) => {
return (
<Option
selected={option.number === selectedANI}
key={index}
value={option.number}
name={`outdial-ani-option-${index}`}
data-testid={`outdial-ani-option-${index}`}
>
{option.name}
</Option>
);
})}
</Select>
<Button
data-testid="outdial-call-button"
prefixIcon={'handset-regular'}
onClick={() => startOutdial(destination, selectedANI)}
disabled={!!isValidNumber || !destination}
size={40}
/>
</article>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,14 +477,34 @@ export type CallControlComponentProps = Pick<
| 'consultTransferOptions'
>;

export type OutdialAniEntry = {
Comment thread
brain-frog marked this conversation as resolved.
/** Unique identifier for the ANI entry */
id: string;
/** Display name for the ANI entry */
name: string;
/** Phone number associated with this ANI entry */
number: string;
/** Related links for this ANI entry */
links: string[];
/** Timestamp when this entry was created (Unix timestamp in milliseconds) */
createdTime: number;
/** Timestamp when this entry was last updated (Unix timestamp in milliseconds) */
lastUpdatedTime: number;
};

/**
* Interface representing the properties for OutdialCall component.
*/
export interface OutdialCallProps {
/**
* Function to start outdial call.
*/
startOutdial: (destination: string) => void;
startOutdial: (destination: string, origin?: string) => void;

/**
* Function to get a list of Outdial ANI entries.
*/
getOutdialANIEntries: () => Promise<OutdialAniEntry[]>;

/**
* CC SDK Instance.
Expand All @@ -497,7 +517,7 @@ export interface OutdialCallProps {
logger: ILogger;
}

export type OutdialCallComponentProps = Pick<OutdialCallProps, 'startOutdial'>;
export type OutdialCallComponentProps = Pick<OutdialCallProps, 'logger' | 'startOutdial' | 'getOutdialANIEntries'>;

/**
* Interface representing the properties for CallControlListItem component.
Expand Down
Loading
Loading