feat(cc): add campaign preview#684
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3cda98967
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Campaign preview handling: render only the active one, skip stale duplicates | ||
| if (hasCampaignPreviewEnabled && isCampaignPreviewTask(task) && hasAgentJoinedTask(task, agentId)) { | ||
| if (task.data.interactionId !== activeCampaignId) { | ||
| return null; // skip stale campaign preview | ||
| } |
There was a problem hiding this comment.
Filter dismissed campaign previews before rendering
When Skip or Remove succeeds, dismissAndSkip/dismissAndRemove add the interaction to dismissedCampaignIds, but this render branch never checks that set. If the SDK keeps returning the same task until the backend sends the follow-up update/end event, the skipped or removed preview remains visible with disabled controls instead of being hidden immediately, which defeats the dismissal tracking added in the store.
Useful? React with 👍 / 👎.
|
|
||
| if (!participants) return undefined; | ||
|
|
There was a problem hiding this comment.
Use the agent participant for handle time
For accepted campaign previews with multiple joined participants, this returns the first joined participant in object iteration order rather than the logged-in agent. If the customer or another participant appears first, the CampaignTask handle-time timer is initialized from that participant's joinTimestamp, so the displayed handle time can be wrong after acceptance or hydration; pass/use the agent id and read that participant specifically.
Useful? React with 👍 / 👎.
|
This pull request is automatically being deployed by Amplify Hosting (learn more). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a2feebe73
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const globalVariablesRef = useRef(latestGlobalVariables); | ||
| if (latestGlobalVariables.length > 0) { | ||
| globalVariablesRef.current = latestGlobalVariables; | ||
| } |
There was a problem hiding this comment.
Reset CAD variables when task changes
When currentTask changes to a different interaction whose snapshot has no callAssociatedData, this ref is not cleared because it only updates for non-empty arrays. GlobalVariablesPanel will then keep rendering the previous task's global variables, which can show stale or wrong customer CAD on the next call; key/reset the ref by interactionId before preserving missing snapshots for the same task.
Useful? React with 👍 / 👎.
| if (this.onIncomingTask && !this.taskList[task.data.interactionId]) { | ||
| this.onIncomingTask({task}); |
There was a problem hiding this comment.
Do not enqueue campaign previews as incoming tasks
For campaign preview reservations, invoking onIncomingTask sends the preview through the existing IncomingTask UI path; that hook's Accept/Decline buttons call task.accept()/task.decline(), while the preview flow added here requires cc.acceptPreviewContact/skipPreviewContact/removePreviewContact from CampaignTask. In apps that render IncomingTask from this callback (including the sample app), a preview offer can therefore expose duplicate controls and let users take the wrong SDK action.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is intended and is consistent with agent desktop the flow is:
- handleIncomingCampaignPreview → onIncomingTask → IncomingTask UI renders (Accept/Decline)
2)Agent clicks Accept → task.accept() → enters RESERVED - handleCampaignPreviewReservation → refreshTaskList → CampaignTask renders (Accept/Skip/Remove the contact)
The IncomingTask and CampaignTask never render simultaneously — IncomingTask handles the initial task offer, and CampaignTask only renders after the agent has accepted and hasAgentJoinedTask() returns true. The onIncomingTask call is required for the agent to accept the initial task to be in the reserved state so that they can determine what they want to do with the campaign.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 003596fba6
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const globalVariablesRef = useRef(latestGlobalVariables); | ||
| if (latestGlobalVariables.length > 0) { | ||
| globalVariablesRef.current = latestGlobalVariables; | ||
| } |
There was a problem hiding this comment.
Reset campaign CAD when a new preview contact arrives
When Skip/Remove advances the same campaign task to another contact and the SDK update omits or empties callAssociatedData, this ref never clears because it only writes non-empty values. The component already detects new contacts via timeoutTimestamp below, but GlobalVariablesPanel will keep showing the previous contact's global variables on the next preview; reset/key the ref when the offer changes, and mirror the same fix in the popover path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 97c2c9dd1a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // The agent has joined the telephony reservation but hasn't accepted the | ||
| // campaign preview yet (Accept/Skip/Remove buttons still showing). | ||
| // CallControl should only render after the preview is explicitly accepted. | ||
| if (task && this.isCampaignPreview(task) && task.data.interaction.state === 'new') return; |
There was a problem hiding this comment.
Honor accepted campaign IDs when selecting current task
When a campaign preview accept is confirmed via handleCampaignPreviewReservation, this change records the interaction in acceptedCampaignIds and immediately refreshes the task list, but this guard still rejects any campaign snapshot whose interaction.state is still 'new'. In that transient accepted-but-not-yet-state-updated case, the accepted preview is never assigned to currentTask, so CallControl/CallControlCAD stay hidden until another SDK update arrives; use the accepted-campaign set here as well before treating the preview as pending.
Useful? React with 👍 / 👎.
|
|
||
| const CAMPAIGN_PREVIEW_OUTBOUND_TYPES = ['STANDARD_PREVIEW_CAMPAIGN', 'DIRECT_PREVIEW_CAMPAIGN']; | ||
| const CAMPAIGN_PREVIEW_CAMPAIGN_TYPES = ['preview_standard', 'preview_direct']; | ||
|
|
There was a problem hiding this comment.
This can be moved to the constant /types file
| const isCampaignPreviewTask = (task: ITask): boolean => { | ||
| const outboundType = task.data.interaction.outboundType ?? ''; | ||
| const cpd = task.data.interaction.callProcessingDetails as unknown as Record<string, string | undefined>; | ||
| const campaignType = cpd?.campaignType ?? ''; | ||
|
|
||
| return ( | ||
| CAMPAIGN_PREVIEW_OUTBOUND_TYPES.includes(outboundType) || CAMPAIGN_PREVIEW_CAMPAIGN_TYPES.includes(campaignType) | ||
| ); | ||
| }; | ||
|
|
||
| const isUnacceptedCampaignPreview = (task: ITask, acceptedCampaignIds: Set<string>): boolean => { | ||
| if (!isCampaignPreviewTask(task)) return false; | ||
|
|
||
| return !acceptedCampaignIds.has(task.data.interactionId); | ||
| }; | ||
|
|
There was a problem hiding this comment.
These methods can be moved to the utils file if possible.
| const CAMPAIGN_PREVIEW_OUTBOUND_TYPES = ['STANDARD_PREVIEW_CAMPAIGN', 'DIRECT_PREVIEW_CAMPAIGN']; | ||
| const CAMPAIGN_PREVIEW_CAMPAIGN_TYPES = ['preview_standard', 'preview_direct']; | ||
|
|
||
| /** | ||
| * Checks whether the task is a campaign preview that the agent has not | ||
| * explicitly accepted. Uses the store's acceptedCampaignIds as the | ||
| * source of truth — the participants.hasJoined flag is unreliable | ||
| * because CampaignContactUpdated payloads can set it even when the | ||
| * agent only skipped or removed the preview. | ||
| */ | ||
| const isUnacceptedCampaignPreview = (task: ITask, acceptedCampaignIds: Set<string>): boolean => { | ||
| const outboundType = task.data.interaction.outboundType ?? ''; | ||
| const cpd = task.data.interaction.callProcessingDetails as unknown as Record<string, string | undefined>; | ||
| const campaignType = cpd?.campaignType ?? ''; | ||
|
|
||
| const isCampaignPreview = | ||
| CAMPAIGN_PREVIEW_OUTBOUND_TYPES.includes(outboundType) || CAMPAIGN_PREVIEW_CAMPAIGN_TYPES.includes(campaignType); | ||
|
|
||
| if (!isCampaignPreview) return false; | ||
|
|
||
| return !acceptedCampaignIds.has(task.data.interactionId); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Please move the constants to a constants file and the helper methods to utils.
| const regularTask: ITask = { | ||
| data: { | ||
| interactionId: 'regular-1', | ||
| interaction: { | ||
| state: 'connected', | ||
| outboundType: 'OUTDIAL', | ||
| }, | ||
| }, | ||
| on: jest.fn(), | ||
| off: jest.fn(), | ||
| } as unknown as ITask; |
There was a problem hiding this comment.
We are keeping all the mockData in the @webex/test-fixtures package to avoid using unknown. With unknown the issue is that if there are other peices of code that affects the logic then that doesnt get considered if we use unknown as we are forcing a specific type.
|
|
||
| const isCampaignPreview = | ||
| CAMPAIGN_PREVIEW_OUTBOUND_TYPES.includes(outboundType) || CAMPAIGN_PREVIEW_CAMPAIGN_TYPES.includes(campaignType); | ||
|
|
There was a problem hiding this comment.
This same list of outbound types + campaign types + the check function now lives in 4 separate files (here, CallControlCAD, task-list.utils, and storeEventsWrapper). If we ever add a new campaign type we'd need to remember to update all of them.
Could we pull this into a shared util? Maybe export it from the store or cc-components so everyone imports from one place.
| }; | ||
|
|
||
| const CampaignTaskWithMetrics = withMetrics(CampaignTask, 'CampaignTask'); | ||
| export default CampaignTaskWithMetrics; |
There was a problem hiding this comment.
Should we wrap this with ErrorBoundary too? The other task components all have it and if something blows up in here it'd take down the whole TaskList.
| @@ -648,6 +741,36 @@ class StoreWrapper implements IStoreWrapper { | |||
| this.refreshTaskList(); | |||
| }; | |||
|
|
|||
There was a problem hiding this comment.
nit: event is untyped here — should be ITask like the other handlers.
| setIsAcceptClicked(true); | ||
| setHandleTimestamp(Date.now()); | ||
| disableAllButtons(); | ||
| logger?.info('CC-Widgets: CampaignTask: Auto-accept UI state set, awaiting backend', { |
There was a problem hiding this comment.
nit: This useState is way down here after a bunch of useEffects. Easy to miss — mind moving it up with the rest of the state declarations?
| // Persist global variables across task updates — some store refreshes | ||
| // replace currentTask 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. |
There was a problem hiding this comment.
Quick question — if all the CAD variables get legitimately cleared (empty array comes back from a valid task update), this ref will keep showing the old values since we only update when length > 0. Is that intentional? If so maybe a quick comment saying why would be helpful for the next person reading this.
| const POPOVER_WIDTH = '440px'; | ||
| const POPOVER_DELAY = '200,100'; | ||
|
|
||
| const getCampaignCpd = (cpd: Record<string, unknown> | undefined): CampaignCallProcessingDetails => { |
There was a problem hiding this comment.
nit: Same getCampaignCpd helper exists in campaign-task.tsx — worth pulling into a shared spot?
COMPLETES #https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7660 and https://jira-eng-sjc12.cisco.com/jira/browse/CAI-7662
This pull request addresses
Adds the UI for the campaign preview functionality
by making the following changes
creates new components for campaign preview
creates global variable component to share between call control cad and campaign components
makes changes to call control cad to render campaign preview ui
adds campaign preview to task list only when flag is enabled
Figma: https://www.figma.com/design/JvnDPHRIXlvZEiSSCBwtjk/WxCC-6242---Preview-Campaign?node-id=6473-173114&m=dev
Vidcast: https://app.vidcast.io/share/72f03823-417d-4417-8a5a-35032943f3d3
Change Type
The following scenarios were tested
The testing is done with the amplify link
< ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >
Can accept campaign previews
Can skip campaign previews
Can remove campaign previews
Can disable skip/remove buttons based on campaign settings
Campaign auto action fires when campaign times out
The GAI Coding Policy And Copyright Annotation Best Practices
Checklist before merging
Make sure to have followed the contributing guidelines before submitting.