From 47ee4a56c82a822423a489196392b7706da5242e Mon Sep 17 00:00:00 2001 From: molker Date: Fri, 3 Oct 2025 12:04:10 -0400 Subject: [PATCH 01/11] [CAI-7173] Task: Outdial Styling Updates --- .../components/task/OutdialCall/constants.ts | 13 ++ .../task/OutdialCall/outdial-call.style.scss | 84 ++------- .../task/OutdialCall/outdial-call.tsx | 174 ++++++++++++++---- .../src/components/task/task.types.ts | 7 +- .../task/src/OutdialCall/index.tsx | 2 + .../cc/samples-cc-react-app/src/App.tsx | 11 +- 6 files changed, 193 insertions(+), 98 deletions(-) create mode 100644 packages/contact-center/cc-components/src/components/task/OutdialCall/constants.ts diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/constants.ts b/packages/contact-center/cc-components/src/components/task/OutdialCall/constants.ts new file mode 100644 index 000000000..f1316442b --- /dev/null +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/constants.ts @@ -0,0 +1,13 @@ +// 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', + DIALPAD_LABEL: 'Dialpad', + 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', '#']; diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss index e1f566719..e6e0e98c3 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss @@ -1,67 +1,41 @@ -.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; + padding: 1rem; + + mdc-input { + padding-bottom: 0; // default is 1 rem, 1.5 rem needed but provided by .keys + } + + #outdial-tablist { + margin-bottom: 1rem; + } - input { + .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); } + .button { + width: 2.5rem; + height: 2.5rem; + } + .keys { display: grid; grid-template-columns: repeat(3, 1fr); - gap: 0.625rem; + row-gap: 1rem; + column-gap: 1.5rem; + padding: 1.5rem 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 { @@ -72,22 +46,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); - } } \ No newline at end of file diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx index 284520907..17a5ee073 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx @@ -2,50 +2,160 @@ import React, {useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; import {withMetrics} from '@webex/cc-ui-logging'; +import {Input, Button, Option, Select, Tab} from '@momentum-design/components/dist/react'; +import {OutdialStrings, KEY_LIST} from './constants'; +/** + * @interface OutdialANIEntry + * Interface representing an ANI (Automatic Number Identification) entry returned by the + * List Outdial ANI Entries API call. + * + * @property {string} organizationId - The organization ID the ANI is associated with. + * @property {string} id - ID of this contact center ANI entry. + * @property {number} version - The version number of the ANI entry. + * @property {string} name - The name assigned to the ANI entry. + * @property {string} number - The phone number associated with the ANI entry. + * @property {number} createdTime - The timestamp(in epoch millis) when the ANI entry was created. + * @property {number} lastUpdatedTime - The timestamp(in epoch millis) when the ANI entry was last updated. + */ +interface OutdialANIEntry { + organizationId?: string; + id?: string; + version?: number; + name: string; + number: string; + createdTime?: number; + lastUpdatedTime?: number; +} +// Note: Only 'name' and 'number' are needed in this component. + +/** + * 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. + * @property dialNumberRegex - Optional regular expression provided by the CC Store for validating destination number format. + */ const OutdialCallComponent: React.FunctionComponent = (props) => { - const {startOutdial} = props; + const {startOutdial, dialNumberRegex} = props; + + // State Hooks const [destination, setDestination] = useState(''); + const [error, setError] = useState(''); + const [selectedANI, setSelectedANI] = useState(''); + + const outdialANIEntries: OutdialANIEntry[] = [ + {number: '+1(234)567-8910', name: 'name 1'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 3'}, + {number: '+1(019)876-5432', name: 'name 4'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + {number: '+1(019)876-5432', name: 'name 2'}, + ]; + /** + * updateOutboundNumber + * @param e The input change event + * Updates the destination state with validated input + * If the input is invalid, sets an error message + */ const updateOutboundNumber = (e: React.ChangeEvent) => { // 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); + const inputValue = e.target.value; + const filteredValue = inputValue.match(VALID_KEYPAD_CHARS)?.join('') || ''; + setDestination(inputValue); + + // Validate the input format + const regexForDn = new RegExp(dialNumberRegex ?? '1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}'); + + if (inputValue !== filteredValue || (inputValue && !regexForDn.test(inputValue))) { + setError(OutdialStrings.INCORRECT_DN_FORMAT); + } else { + setError(''); + } }; - // Function to press a key on the outdial keypad. - const handelKeyPress = (value: string) => { - setDestination((prev) => prev + value); + /** + * handleKeyPress + * @param value The key value pressed + * Appends the pressed key to the destination input field + */ + const handleKeyPress = (value: string) => { + updateOutboundNumber({target: {value: destination + value}} as React.ChangeEvent); }; return ( -
-
-
- Outdial Call -
- -
- {['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#'].map((key) => ( -
handelKeyPress(key)}> - {key} -
- ))} -
- -
-
-
+
+
+ +
+ { + updateOutboundNumber(e as React.ChangeEvent); + }} + /> +
+ {KEY_LIST.map((key) => ( + + ))} +
+ +
); }; diff --git a/packages/contact-center/cc-components/src/components/task/task.types.ts b/packages/contact-center/cc-components/src/components/task/task.types.ts index a96c41b06..6021a7b9f 100644 --- a/packages/contact-center/cc-components/src/components/task/task.types.ts +++ b/packages/contact-center/cc-components/src/components/task/task.types.ts @@ -474,9 +474,14 @@ export interface OutdialCallProps { * Logger instance for logging purpose. */ logger: ILogger; + + /** + * Regex pattern for validating the a phone number. + */ + dialNumberRegex?: string; } -export type OutdialCallComponentProps = Pick; +export type OutdialCallComponentProps = Pick; /** * Interface representing the properties for CallControlListItem component. diff --git a/packages/contact-center/task/src/OutdialCall/index.tsx b/packages/contact-center/task/src/OutdialCall/index.tsx index 9f554c293..32132aba5 100644 --- a/packages/contact-center/task/src/OutdialCall/index.tsx +++ b/packages/contact-center/task/src/OutdialCall/index.tsx @@ -7,10 +7,12 @@ import {useOutdialCall} from '../helper'; const OutdialCallInternal: React.FunctionComponent = observer(() => { const {cc, logger} = store; + const dialNumberRegex = cc?.agentConfig?.regexUS; const result = useOutdialCall({cc, logger}); const props = { ...result, + dialNumberRegex, }; return ; diff --git a/widgets-samples/cc/samples-cc-react-app/src/App.tsx b/widgets-samples/cc/samples-cc-react-app/src/App.tsx index af134eff5..8ffda809e 100644 --- a/widgets-samples/cc/samples-cc-react-app/src/App.tsx +++ b/widgets-samples/cc/samples-cc-react-app/src/App.tsx @@ -827,7 +827,16 @@ function App() {
)} - {selectedWidgets.outdialCall && } + {selectedWidgets.outdialCall && ( +
+
+
+ Outdial Call + +
+
+
+ )} )} From 9fc04b26d50dbb6d94c12fb784d18e8d40fa438c Mon Sep 17 00:00:00 2001 From: molker Date: Tue, 7 Oct 2025 11:01:22 -0400 Subject: [PATCH 02/11] PR Comments --- .../task/OutdialCall/outdial-call.style.scss | 2 + .../task/OutdialCall/outdial-call.tsx | 72 +++++++++---------- .../src/components/task/task.types.ts | 7 +- .../task/src/OutdialCall/index.tsx | 2 - 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss index e6e0e98c3..27abb76bc 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.style.scss @@ -29,6 +29,8 @@ row-gap: 1rem; column-gap: 1.5rem; padding: 1.5rem 0; + list-style-type: none; + margin: 0; } .key { diff --git a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx index 17a5ee073..2d8cf017c 100644 --- a/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx +++ b/packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {OutdialCallComponentProps} from '../task.types'; import './outdial-call.style.scss'; import {withMetrics} from '@webex/cc-ui-logging'; @@ -15,8 +15,8 @@ import {OutdialStrings, KEY_LIST} from './constants'; * @property {number} version - The version number of the ANI entry. * @property {string} name - The name assigned to the ANI entry. * @property {string} number - The phone number associated with the ANI entry. - * @property {number} createdTime - The timestamp(in epoch millis) when the ANI entry was created. - * @property {number} lastUpdatedTime - The timestamp(in epoch millis) when the ANI entry was last updated. + * @property {number} createdTime - The timestamp(in epoch milliseconds) when the ANI entry was created. + * @property {number} lastUpdatedTime - The timestamp(in epoch milliseconds) when the ANI entry was last updated. */ interface OutdialANIEntry { organizationId?: string; @@ -38,16 +38,21 @@ interface OutdialANIEntry { * * @param props - Properties for the OutdialCallComponent. * @property startOutdial - Function to initiate the outdial call with the entered destination number. - * @property dialNumberRegex - Optional regular expression provided by the CC Store for validating destination number format. */ const OutdialCallComponent: React.FunctionComponent = (props) => { - const {startOutdial, dialNumberRegex} = props; + const {startOutdial} = props; // State Hooks const [destination, setDestination] = useState(''); - const [error, setError] = useState(''); + const [isValidNumber, setIsValidNumber] = useState(''); const [selectedANI, setSelectedANI] = useState(''); + // 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}$'), + [] + ); + const outdialANIEntries: OutdialANIEntry[] = [ {number: '+1(234)567-8910', name: 'name 1'}, {number: '+1(019)876-5432', name: 'name 2'}, @@ -65,25 +70,15 @@ const OutdialCallComponent: React.FunctionComponent = ]; /** - * updateOutboundNumber + * validateOutboundNumber * @param e The input change event - * Updates the destination state with validated input - * If the input is invalid, sets an error message + * If the input is invalid, sets an error message on dialnumber input */ - const updateOutboundNumber = (e: React.ChangeEvent) => { - // Allow only valid input that is digits, #, *, and + - const VALID_KEYPAD_CHARS = /[\d#*+]/g; - const inputValue = e.target.value; - const filteredValue = inputValue.match(VALID_KEYPAD_CHARS)?.join('') || ''; - setDestination(inputValue); - - // Validate the input format - const regexForDn = new RegExp(dialNumberRegex ?? '1[0-9]{3}[2-9][0-9]{6}([,]{1,10}[0-9]+){0,1}'); - - if (inputValue !== filteredValue || (inputValue && !regexForDn.test(inputValue))) { - setError(OutdialStrings.INCORRECT_DN_FORMAT); + const validateOutboundNumber = (value: string) => { + if (value && !regExForDnSpecialChars.test(value)) { + setIsValidNumber(OutdialStrings.INCORRECT_DN_FORMAT); } else { - setError(''); + setIsValidNumber(''); } }; @@ -93,12 +88,13 @@ const OutdialCallComponent: React.FunctionComponent = * Appends the pressed key to the destination input field */ const handleKeyPress = (value: string) => { - updateOutboundNumber({target: {value: destination + value}} as React.ChangeEvent); + setDestination(destination + value); + validateOutboundNumber(destination + value); }; return ( -
-
+
+
= variant="pill" aria-controls="dialpad-panel" > -
+ { - updateOutboundNumber(e as React.ChangeEvent); + const inputValue = (e as React.ChangeEvent).target.value; + setDestination(inputValue); + validateOutboundNumber(inputValue); }} /> -
+
    {KEY_LIST.map((key) => ( - +
  • + +
  • ))} -
+ = className="input" label={OutdialStrings.ANI_SELECT_LABEL} id="outdial-ani-option" - name="outdial-ani-option" + name="outdial-ani-option-select" data-testid="outdial-ani-option-select" placeholder={OutdialStrings.ANI_SELECT_PLACEHOLDER} onChange={(event: CustomEvent) => { setSelectedANI(event.detail.value); }} > - {outdialANIEntries.map((option: OutdialANIEntry, index: number) => { + {outdialANIList.map((option: OutdialANIEntry, index: number) => { return (