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 @@ -24,6 +24,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 @@ -82,22 +82,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 👍 / 👎.

}
const globalVariables = globalVariablesRef.current;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,5 +462,130 @@ describe('CallControlCADComponent', () => {
expect(screen.getByText('Global_NoDisplay:')).toBeInTheDocument();
expect(screen.getByText('some value')).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)} />);
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();
});
});
});