Skip to content
Open
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
Expand Up @@ -43,6 +43,7 @@

.cad-global-variables {
margin-top: 0.5rem;
max-height: none;
}

/* On-hold chip styling */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,26 @@ const CallControlCADComponent: React.FC<CallControlComponentProps> = (props) =>
const callAssociatedData = currentTask?.data?.interaction?.callAssociatedData as CallAssociatedDataMap | undefined;
const latestGlobalVariables = getAgentViewableGlobalVariables(callAssociatedData);

// Persist global variables across task updates — some store refreshes
// replace currentTask with a snapshot that omits callAssociatedData,
// which causes getAgentViewableGlobalVariables to return [].
// We intentionally keep the previous values when length === 0 because
// an empty array indicates missing data, not a legitimate clearing of
// variables. Variables are never cleared mid-call by the backend.
// Reset when the interaction changes so stale CAD from a previous task
// is never shown on a new call.
// Persist and accumulate global variables across task updates.
// Each websocket event may only carry a subset of the full
// callAssociatedData, so we merge new variables into the ref by name
// instead of replacing the whole array. This ensures all global
// variables seen during the interaction are displayed — matching the
// regular desktop behaviour.
// When length === 0 we keep previous values (missing data, not a
// legitimate clearing — variables are never cleared mid-call).
// Reset when the interaction changes so stale CAD from a previous
// task is never shown on a new call.
const interactionId = currentTask.data.interaction.interactionId;
const globalVariablesRef = useRef(latestGlobalVariables);
const prevInteractionIdRef = useRef(interactionId);
if (prevInteractionIdRef.current !== interactionId) {
prevInteractionIdRef.current = interactionId;
globalVariablesRef.current = latestGlobalVariables;
} else if (latestGlobalVariables.length > 0) {
globalVariablesRef.current = latestGlobalVariables;
const existingMap = new Map(globalVariablesRef.current.map((v) => [v.name, v]));
latestGlobalVariables.forEach((v) => existingMap.set(v.name, v));
globalVariablesRef.current = Array.from(existingMap.values());
}
const globalVariables = globalVariablesRef.current;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ const CampaignTaskPopover: React.FC<CampaignTaskPopoverProps> = ({
.callAssociatedData;
const latestGlobalVariables = getAgentViewableGlobalVariables(callAssociatedData);

// Persist global variables across task updates — some store refreshes
// replace the task with a snapshot that omits callAssociatedData,
// which causes getAgentViewableGlobalVariables to return [].
// Persist and accumulate global variables across task updates.
// Each websocket event may only carry a subset of the full
// callAssociatedData, so we merge new variables into the ref by name
// instead of replacing the whole array.
// Reset when the interaction changes so stale CAD from a previous
// contact is never shown on the next preview.
const interactionId = task.data.interactionId;
Expand All @@ -54,7 +55,9 @@ const CampaignTaskPopover: React.FC<CampaignTaskPopoverProps> = ({
prevInteractionIdRef.current = interactionId;
globalVariablesRef.current = latestGlobalVariables;
} else if (latestGlobalVariables.length > 0) {
globalVariablesRef.current = latestGlobalVariables;
const existingMap = new Map(globalVariablesRef.current.map((v) => [v.name, v]));
latestGlobalVariables.forEach((v) => existingMap.set(v.name, v));
globalVariablesRef.current = Array.from(existingMap.values());
}
const globalVariables = globalVariablesRef.current;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,25 @@ const CampaignTask: React.FC<CampaignTaskProps> = ({
.callAssociatedData;
const latestGlobalVariables = getAgentViewableGlobalVariables(callAssociatedData);

// Persist global variables across task updates — some store refreshes
// replace the task with a snapshot that omits callAssociatedData.
// Reset when the interaction changes so stale CAD from a previous task
// is never shown on a new call.
// Persist and accumulate global variables across task updates.
// Each websocket event may only carry a subset of the full
// callAssociatedData, so we merge new variables into the ref by name
// instead of replacing the whole array. This ensures all global
// variables seen during the interaction are displayed — matching the
// regular desktop behaviour.
// When length === 0 we keep previous values (missing data, not a
// legitimate clearing — variables are never cleared mid-call).
// Reset when the interaction changes so stale CAD from a previous
// task is never shown on a new call.
const globalVariablesRef = useRef(latestGlobalVariables);
const prevInteractionIdRef = useRef(interactionId);
if (prevInteractionIdRef.current !== interactionId) {
prevInteractionIdRef.current = interactionId;
globalVariablesRef.current = latestGlobalVariables;
} else if (latestGlobalVariables.length > 0) {
globalVariablesRef.current = latestGlobalVariables;
const existingMap = new Map(globalVariablesRef.current.map((v) => [v.name, v]));
latestGlobalVariables.forEach((v) => existingMap.set(v.name, v));
globalVariablesRef.current = Array.from(existingMap.values());
Comment on lines 69 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset cached CAD when campaign contact changes

When a preview campaign offer is skipped or removed, this component can receive a new contact on the same task/interaction; the component already detects that scenario below via campaignPreviewOfferTimeout. Because this accumulator only resets on interactionId changes and otherwise merges by variable name, any variable that was present only on the previous contact remains visible in the inline panel (and the mirrored popover accumulator has the same behavior) until unmount. For example, contact A's Global_Language will still display for contact B if B's payload contains only Global_Account, leaking stale customer data.

Useful? React with 👍 / 👎.

Comment on lines +70 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Update the cc-components spec for CAD accumulation

This change alters how cc-components persists and accumulates agent-viewable CAD globals, but the commit only touches source/tests/styles and leaves packages/contact-center/cc-components/ai-docs/cc-components-spec.md unchanged. Root AGENTS.md Critical Rule 9 and ai-docs/REVIEW_CHECKLIST.md C1 require the module spec/standing doc to be updated in the same change, so the canonical component behavior will drift unless this accumulation/reset behavior is documented with the code change.

Useful? React with 👍 / 👎.

}
const globalVariables = globalVariablesRef.current;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,5 +512,130 @@ describe('CallControlCADComponent', () => {

expect(screen.queryByTestId('call-control:participants-trigger')).not.toBeInTheDocument();
});

it('should accumulate global variables across re-renders (CAI-8143)', () => {
const firstPayload: CallAssociatedDataMap = {
Global_Language: {
name: 'Global_Language',
displayName: 'Customer Language',
value: 'English',
type: 'STRING',
agentEditable: false,
agentViewable: true,
global: true,
isSecure: false,
secureKeyId: '',
secureKeyVersion: 0,
},
};

const secondPayload: CallAssociatedDataMap = {
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,
},
};

// First render with only Global_Language
const screen = render(<CallControlCADComponent {...makePropsWithCallAssociatedData(firstPayload)} />);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Define the CAD test helper before using it

The @webex/cc-components test:unit script starts with tsc --project tsconfig.test.json, but these added tests reference makePropsWithCallAssociatedData even though no local helper or import exists (repo-wide search only finds these new call sites). This same block also uses CallAssociatedDataMap without importing it, so the type-check step fails before Jest can run and the new regression coverage breaks the workspace test command.

Useful? React with 👍 / 👎.

expect(screen.getByText('Customer Language:')).toBeInTheDocument();
expect(screen.getByText('English')).toBeInTheDocument();

// Re-render with only Global_FeedbackSurveyOptIn (simulating a new websocket payload)
screen.rerender(<CallControlCADComponent {...makePropsWithCallAssociatedData(secondPayload)} />);

// Both variables should now be visible (merged, not replaced)
expect(screen.getByText('Customer Language:')).toBeInTheDocument();
expect(screen.getByText('English')).toBeInTheDocument();
expect(screen.getByText('Post Call Survey Opt-in:')).toBeInTheDocument();
expect(screen.getByText('true')).toBeInTheDocument();
});

it('should update existing variable values when re-rendered with new data (CAI-8143)', () => {
const initialPayload: CallAssociatedDataMap = {
Global_Language: {
name: 'Global_Language',
displayName: 'Customer Language',
value: 'English',
type: 'STRING',
agentEditable: false,
agentViewable: true,
global: true,
isSecure: false,
secureKeyId: '',
secureKeyVersion: 0,
},
};

const updatedPayload: CallAssociatedDataMap = {
Global_Language: {
name: 'Global_Language',
displayName: 'Customer Language',
value: 'Spanish',
type: 'STRING',
agentEditable: false,
agentViewable: true,
global: true,
isSecure: false,
secureKeyId: '',
secureKeyVersion: 0,
},
};

const screen = render(<CallControlCADComponent {...makePropsWithCallAssociatedData(initialPayload)} />);
expect(screen.getByText('English')).toBeInTheDocument();

// Re-render with updated value for the same variable
screen.rerender(<CallControlCADComponent {...makePropsWithCallAssociatedData(updatedPayload)} />);
expect(screen.getByText('Spanish')).toBeInTheDocument();
expect(screen.queryByText('English')).not.toBeInTheDocument();
});

it('should reset global variables when interaction changes (CAI-8143)', () => {
const payload: CallAssociatedDataMap = {
Global_Language: {
name: 'Global_Language',
displayName: 'Customer Language',
value: 'English',
type: 'STRING',
agentEditable: false,
agentViewable: true,
global: true,
isSecure: false,
secureKeyId: '',
secureKeyVersion: 0,
},
};

const screen = render(<CallControlCADComponent {...makePropsWithCallAssociatedData(payload)} />);
expect(screen.getByText('Customer Language:')).toBeInTheDocument();

// Re-render with a different interaction ID and no global variables
const newInteractionProps = {
...defaultProps,
currentTask: {
...defaultProps.currentTask,
data: {
...defaultProps.currentTask.data,
interaction: {
...defaultProps.currentTask.data.interaction,
interactionId: 'new-interaction-456',
},
},
},
};
screen.rerender(<CallControlCADComponent {...newInteractionProps} />);

// Old variables should be cleared for the new interaction
expect(screen.queryByText('Customer Language:')).not.toBeInTheDocument();
});
});
});