diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss index 068a103dc..e93cfb8a9 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.styles.scss @@ -22,6 +22,24 @@ } } +.global-variables { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + width: 100%; + max-height: 11.25rem; + overflow-y: auto; + margin-top: 0.75rem; + + .global-variable-item { + flex-basis: 50%; + min-width: 0; + box-sizing: border-box; + padding: 0.125rem 0; + line-height: 1.75rem; + } +} + /* On-hold chip styling */ .on-hold { display: flex; @@ -44,7 +62,6 @@ } } - .recording-indicator { position: absolute; top: 0.5rem; @@ -143,8 +160,6 @@ transform: rotate(180deg); } - - .call-status { display: flex; align-items: center; @@ -200,11 +215,11 @@ } .digital-customer-name { - width: auto; + width: auto; } .digital-phone-number { - width: auto; + width: auto; } @media (min-width: 64.0625rem) and (max-width: 75rem) { @@ -230,8 +245,8 @@ max-width: 15rem; } .call-control-task-tooltip { - width: 50%; - text-align: center; + width: 50%; + text-align: center; } } @@ -246,8 +261,8 @@ max-width: 13rem; } .call-control-task-tooltip { - width: 50%; - text-align: center; + width: 50%; + text-align: center; } } @@ -262,12 +277,12 @@ max-width: 12rem; } .call-control-task-tooltip { - width: 50%; - text-align: center; + width: 50%; + text-align: center; } } -@media (min-width: 15.625rem) and (max-width: 20rem){ +@media (min-width: 15.625rem) and (max-width: 20rem) { .call-control-task-tooltip { width: 50%; text-align: center; @@ -275,14 +290,14 @@ } .participants-popover { - background: var(--mds-color-theme-background-solid-primary-normal, #FFFFFF) !important; + background: var(--mds-color-theme-background-solid-primary-normal, #ffffff) !important; align-items: normal; width: 100%; padding: 0 !important; border: var(--mds-color-theme-outline-secondary-normal); &::part(popover-content) { - background: var(--mds-color-theme-background-solid-primary-normal, #FFFFFF); + background: var(--mds-color-theme-background-solid-primary-normal, #ffffff); border-radius: 0.75rem; border: 1px solid var(--mds-color-theme-outline-secondary-normal); box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.08); @@ -307,7 +322,7 @@ padding: 0.4rem 0.6rem; font-size: 0.875rem; cursor: pointer; - color: var(--mds-color-theme-text-primary-normal, #000000F2); + color: var(--mds-color-theme-text-primary-normal, #000000f2); border-radius: 0.5rem; min-width: 0; max-width: 100%; diff --git a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx index 7fc150cc2..34eea5920 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControlCAD/call-control-cad.tsx @@ -5,7 +5,13 @@ import {Brandvisual, Icon, Tooltip, Button} from '@momentum-design/components/di import './call-control-cad.styles.scss'; import TaskTimer from '../TaskTimer/index'; import CallControlConsultComponent from '../CallControl/CallControlCustom/call-control-consult'; -import {MEDIA_CHANNEL as MediaChannelType, CallControlComponentProps, getCallerIdentifier} from '../task.types'; +import { + MEDIA_CHANNEL as MediaChannelType, + CallControlComponentProps, + getCallerIdentifier, + CallAssociatedDataMap, +} from '../task.types'; +import {getAgentViewableGlobalVariables} from '../Task/task.utils'; import {getMediaTypeInfo} from '../../../utils'; import { @@ -69,6 +75,10 @@ const CallControlCADComponent: React.FC = (props) => //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 const dn = currentTask?.data?.interaction?.callAssociatedDetails?.dn; + //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 + const callAssociatedData = currentTask?.data?.interaction?.callAssociatedData as CallAssociatedDataMap | undefined; + const globalVariables = getAgentViewableGlobalVariables(callAssociatedData); + // Create unique IDs for tooltips const customerNameTriggerId = `customer-name-trigger-${currentTask.data.interaction.interactionId}`; const customerNameTooltipId = `customer-name-tooltip-${currentTask.data.interaction.interactionId}`; @@ -275,6 +285,24 @@ const CallControlCADComponent: React.FC = (props) => {renderPhoneNumber()} + {globalVariables.length > 0 && ( +
+ {globalVariables.map((variable) => ( +
+ + {variable.displayName || variable.name} + + + {variable.value || ''} + +
+ ))} +
+ )} {controlVisibility.isConsultInitiatedOrAccepted && !controlVisibility.wrapup.isVisible && (
diff --git a/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts index 3f4b9b86b..e37e875ab 100644 --- a/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts +++ b/packages/contact-center/cc-components/src/components/task/Task/task.utils.ts @@ -1,6 +1,43 @@ -import type {MEDIA_CHANNEL as MediaChannelType, TaskComponentData} from '../task.types'; +import type { + MEDIA_CHANNEL as MediaChannelType, + TaskComponentData, + CADVariable, + CallAssociatedDataMap, +} from '../task.types'; import {getMediaTypeInfo} from '../../../utils'; +/** System CAD variable keys that are already displayed elsewhere in the UI. */ +export const SYSTEM_CAD_KEYS = new Set([ + 'ani', + 'dn', + 'customerName', + 'virtualTeamName', + 'ronaTimeout', + 'FC-DESKTOP-VIEW', +]); + +/** + * Returns agent-viewable global variables from a callAssociatedData map, + * excluding system variables that are already rendered elsewhere. + */ +export const getAgentViewableGlobalVariables = ( + callAssociatedData: CallAssociatedDataMap | undefined +): CADVariable[] => { + if (!callAssociatedData || typeof callAssociatedData !== 'object') { + return []; + } + + return Object.entries(callAssociatedData) + .filter(([key, cadVar]) => { + if (!cadVar || !cadVar.name) return false; + if (cadVar.agentViewable === false) return false; + if (!cadVar.global) return false; + if (SYSTEM_CAD_KEYS.has(key)) return false; + return true; + }) + .map(([, cadVar]) => cadVar); +}; + /** * Capitalizes the first word of a string * @param str - The string to capitalize 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 3816154d3..3104910a5 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 @@ -16,6 +16,29 @@ import { type Enum> = T[keyof T]; +/** + * Represents a single Call Associated Data (CAD) variable on an interaction. + * Global variables have `global: true` and are set by flow control. + */ +export interface CADVariable { + name: string; + displayName: string; + value: string; + type: string; + agentEditable: boolean; + agentViewable: boolean; + global: boolean; + isSecure: boolean; + secureKeyId: string; + secureKeyVersion: number; +} + +/** + * Record of CAD variables keyed by variable name. + * This is the shape of `callAssociatedData` on the interaction at runtime. + */ +export type CallAssociatedDataMap = Record; + /** * Target types for consult/transfer operations */ diff --git a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx index e7a321ebe..afc35b2f7 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControlCAD/call-control-cad.tsx @@ -1,8 +1,13 @@ import React from 'react'; import {render} from '@testing-library/react'; import CallControlCADComponent from '../../../../src/components/task/CallControlCAD/call-control-cad'; -import {CallControlComponentProps, TARGET_TYPE, OUTBOUND_TYPE} from '../../../../src/components/task/task.types'; -import {mockTask} from '@webex/test-fixtures'; +import { + CallControlComponentProps, + TARGET_TYPE, + OUTBOUND_TYPE, + CallAssociatedDataMap, +} from '../../../../src/components/task/task.types'; +import {mockTask, mockCallAssociatedData} from '@webex/test-fixtures'; import {BuddyDetails} from '@webex/cc-store'; import '@testing-library/jest-dom'; @@ -370,4 +375,92 @@ describe('CallControlCADComponent', () => { const customConsultContainer = customScreen.container.querySelector('.call-control-consult-container'); expect(customConsultContainer).toHaveClass('custom-consult-control'); }); + + describe('Global Variables', () => { + const makePropsWithCallAssociatedData = (callAssociatedData: CallAssociatedDataMap) => ({ + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + callAssociatedData, + }, + }, + }, + }); + + it('should render agent-viewable global variables', () => { + const screen = render(); + + const globalVarsContainer = screen.getByTestId('cc-cad:global-variables'); + expect(globalVarsContainer).toBeInTheDocument(); + + expect(screen.getByTestId('cc-cad:global-var-Global_Language')).toBeInTheDocument(); + expect(screen.getByText('Customer Language')).toBeInTheDocument(); + expect(screen.getByText('English')).toBeInTheDocument(); + + expect(screen.getByTestId('cc-cad:global-var-Global_FeedbackSurveyOptIn')).toBeInTheDocument(); + expect(screen.getByText('Post Call Survey Opt-in')).toBeInTheDocument(); + expect(screen.getByText('true')).toBeInTheDocument(); + }); + + it('should not render non-global variables (e.g. system CAD like ani)', () => { + const screen = render(); + + expect(screen.queryByTestId('cc-cad:global-var-ani')).not.toBeInTheDocument(); + }); + + it('should not render global variables where agentViewable is false', () => { + const screen = render(); + + expect(screen.queryByTestId('cc-cad:global-var-Global_Hidden')).not.toBeInTheDocument(); + }); + + it('should not render global variables section when no global variables exist', () => { + const screen = render(); + + expect(screen.queryByTestId('cc-cad:global-variables')).not.toBeInTheDocument(); + }); + + it('should not render global variables section when callAssociatedData is undefined', () => { + const propsWithNoData = { + ...defaultProps, + currentTask: { + ...defaultProps.currentTask, + data: { + ...defaultProps.currentTask.data, + interaction: { + ...defaultProps.currentTask.data.interaction, + }, + }, + }, + }; + const screen = render(); + + expect(screen.queryByTestId('cc-cad:global-variables')).not.toBeInTheDocument(); + }); + + it('should use variable name as label when displayName is empty', () => { + const dataWithEmptyDisplayName: CallAssociatedDataMap = { + Global_NoDisplay: { + name: 'Global_NoDisplay', + displayName: '', + value: 'some value', + type: 'STRING', + agentEditable: false, + agentViewable: true, + global: true, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + }, + }; + const screen = render(); + + expect(screen.getByText('Global_NoDisplay')).toBeInTheDocument(); + expect(screen.getByText('some value')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/contact-center/cc-components/tests/components/task/task.types.test.ts b/packages/contact-center/cc-components/tests/components/task/task.types.test.ts new file mode 100644 index 000000000..7d84d66b3 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/task.types.test.ts @@ -0,0 +1,92 @@ +import {CallAssociatedDataMap} from '../../../src/components/task/task.types'; +import {getAgentViewableGlobalVariables, SYSTEM_CAD_KEYS} from '../../../src/components/task/Task/task.utils'; + +describe('getAgentViewableGlobalVariables', () => { + const makeGlobalVar = ( + name: string, + overrides: Partial = {} + ): CallAssociatedDataMap[string] => ({ + name, + displayName: name, + value: 'test-value', + type: 'STRING', + agentEditable: false, + agentViewable: true, + global: true, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + ...overrides, + }); + + it('should return agent-viewable global variables', () => { + const data: CallAssociatedDataMap = { + Global_Language: makeGlobalVar('Global_Language', {value: 'English'}), + Global_Region: makeGlobalVar('Global_Region', {value: 'US'}), + }; + + const result = getAgentViewableGlobalVariables(data); + expect(result).toHaveLength(2); + expect(result[0].name).toBe('Global_Language'); + expect(result[1].name).toBe('Global_Region'); + }); + + it('should exclude variables where agentViewable is false', () => { + const data: CallAssociatedDataMap = { + Global_Visible: makeGlobalVar('Global_Visible'), + Global_Hidden: makeGlobalVar('Global_Hidden', {agentViewable: false}), + }; + + const result = getAgentViewableGlobalVariables(data); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Global_Visible'); + }); + + it('should exclude non-global variables', () => { + const data: CallAssociatedDataMap = { + Global_Var: makeGlobalVar('Global_Var'), + LocalCadVar: makeGlobalVar('LocalCadVar', {global: false}), + }; + + const result = getAgentViewableGlobalVariables(data); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Global_Var'); + }); + + it('should exclude system CAD keys', () => { + const data: CallAssociatedDataMap = {}; + SYSTEM_CAD_KEYS.forEach((key) => { + data[key] = makeGlobalVar(key, {global: true, agentViewable: true}); + }); + data['Global_Custom'] = makeGlobalVar('Global_Custom'); + + const result = getAgentViewableGlobalVariables(data); + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Global_Custom'); + }); + + it('should exclude entries with no name', () => { + const data: CallAssociatedDataMap = { + Global_NoName: makeGlobalVar('', {displayName: 'No Name'}), + }; + + const result = getAgentViewableGlobalVariables(data); + expect(result).toHaveLength(0); + }); + + it('should return empty array for undefined input', () => { + expect(getAgentViewableGlobalVariables(undefined)).toEqual([]); + }); + + it('should return empty array for null input', () => { + expect(getAgentViewableGlobalVariables(null as unknown as undefined)).toEqual([]); + }); + + it('should return empty array for non-object input', () => { + expect(getAgentViewableGlobalVariables('string' as unknown as undefined)).toEqual([]); + }); + + it('should return empty array for empty object', () => { + expect(getAgentViewableGlobalVariables({})).toEqual([]); + }); +}); diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index 643eddf9c..13ab6fee5 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -514,6 +514,71 @@ const mockCC: IContactCenter = { getAccessToken: jest.fn().mockResolvedValue('mock-access-token'), }; +const mockCallAssociatedData: Record< + string, + { + name: string; + displayName: string; + value: string; + type: string; + agentEditable: boolean; + agentViewable: boolean; + global: boolean; + isSecure: boolean; + secureKeyId: string; + secureKeyVersion: number; + } +> = { + Global_Language: { + name: 'Global_Language', + displayName: 'Customer Language', + value: 'English', + type: 'STRING', + agentEditable: false, + agentViewable: true, + global: true, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + }, + Global_FeedbackSurveyOptIn: { + name: 'Global_FeedbackSurveyOptIn', + displayName: 'Post Call Survey Opt-in', + value: 'true', + type: 'STRING', + agentEditable: false, + agentViewable: true, + global: true, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + }, + ani: { + name: 'ani', + displayName: 'ani', + value: '555-123-4567', + type: 'STRING', + agentEditable: false, + agentViewable: true, + global: false, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + }, + Global_Hidden: { + name: 'Global_Hidden', + displayName: 'Hidden Variable', + value: 'secret', + type: 'STRING', + agentEditable: false, + agentViewable: false, + global: true, + isSecure: false, + secureKeyId: '', + secureKeyVersion: 0, + }, +}; + export { mockProfile, mockCC, @@ -523,4 +588,5 @@ export { mockEntryPointsResponse, mockAddressBookEntriesResponse, makeMockAddressBook, + mockCallAssociatedData, };