diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md new file mode 100644 index 000000000..5083a2313 --- /dev/null +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -0,0 +1,605 @@ +# Component Layer (`cc-components`) Migration + +## Summary + +The `cc-components` package contains the presentational React components for task widgets. These components receive control visibility as props. The prop interface must be updated to match the new `TaskUIControls` shape from SDK (renamed controls, merged controls, removed state flags). + +### Source of truth — the task object (`ITask`) + +After the task-refactor, **everything comes from the SDK task object** (`ITask`). Widgets should not derive state with helper functions like `getControlsVisibility()` or `findHoldStatus()`, and should have zero awareness of the SDK's internal state machine. + +- **Control visibility and enablement:** `task.uiControls` — a property on the `ITask` object. Each of the 17 controls has `{ isVisible: boolean, isEnabled: boolean }`. The widget hook reads `store.currentTask.uiControls` and passes it to the component. Do not derive from `deviceType`, `featureFlags`, `conferenceEnabled`, or the legacy `getControlsVisibility()`. +- **Hold state (`isHeld`, `consultCallHeld`):** Provided by the task object. Do not use the legacy `findHoldStatus()` helper or `controls.hold.isEnabled` (those are action flags, not hold state). +- **Conference state (`isConferenceInProgress`):** Provided by the task object (e.g. `task.data.isConferenceInProgress`). Do not use `controls.exitConference.isVisible` as sole source — it can be false when consult is active even if conference is in progress. + +--- + +## ControlVisibility Interface — Delete and Replace + +**File:** `cc-components/src/components/task/task.types.ts` + +The old `ControlVisibility` interface (22 controls + 7 state flags) must be replaced with `TaskUIControls` from the SDK. All new control values come from `task.uiControls` — a property on the `ITask` object provided by the SDK task-refactor branch. + +```typescript +// OLD — DELETE this interface +export interface ControlVisibility { + accept: Visibility; // → task.uiControls.accept (same name) + decline: Visibility; // → task.uiControls.decline (same name) + end: Visibility; // → task.uiControls.end (same name) + muteUnmute: Visibility; // → task.uiControls.mute (renamed) + muteUnmuteConsult: Visibility; // → REMOVE — use task.uiControls.mute (single mute control covers both main and consult) + holdResume: Visibility; // → task.uiControls.hold (renamed) + consult: Visibility; // → task.uiControls.consult (same name) + transfer: Visibility; // → task.uiControls.transfer (same name) + conference: Visibility; // → task.uiControls.conference; SDK also has task.uiControls.mergeToConference — use mergeToConference for Merge action + wrapup: Visibility; // → task.uiControls.wrapup (same name) + pauseResumeRecording: Visibility; // → task.uiControls.recording (renamed) + endConsult: Visibility; // → task.uiControls.endConsult (same name) + recordingIndicator: Visibility; // → REMOVE — merged into task.uiControls.recording (use recording.isVisible for badge, recording.isEnabled for toggle) + exitConference: Visibility; // → task.uiControls.exitConference (same name) + mergeConference: Visibility; // → task.uiControls.mergeToConference (renamed) + consultTransfer: Visibility; // → task.uiControls.consultTransfer — NOTE: always hidden in new SDK; use task.uiControls.transfer or task.uiControls.transferConference instead + mergeConferenceConsult: Visibility; // → REMOVE — use task.uiControls.mergeToConference (single control covers both main and consult merge) + consultTransferConsult: Visibility; // → REMOVE — use task.uiControls.transfer for consult transfer, task.uiControls.transferConference for conference transfer + switchToMainCall: Visibility; // → task.uiControls.switchToMainCall (same name) + switchToConsult: Visibility; // → task.uiControls.switchToConsult (same name) + isConferenceInProgress: boolean; // → use `task.data.isConferenceInProgress` (SDK provides this directly); do NOT use controls.exitConference.isVisible as sole source — it can be false when consult is active even if conference is in progress + isConsultInitiated: boolean; // → Do NOT use endConsult.isVisible as "initiated only"; it covers both initiated and accepted. Use `task.data.consultStatus` if you need that distinction (e.g. `consultInitiated` vs `consultAccepted`). + isConsultInitiatedAndAccepted: boolean; // → REMOVE + isConsultReceived: boolean; // → REMOVE + isConsultInitiatedOrAccepted: boolean; // → REMOVE + isHeld: boolean; // → get from task object (SDK provides hold state). Do NOT use controls.hold.isEnabled (that is an action flag, not hold state). + consultCallHeld: boolean; // → get from task object. Do NOT use controls.switchToConsult.isVisible (that is button visibility, not hold state). +} + +// NEW — import via store to preserve layering (cc-components → store → SDK). Store re-exports TaskUIControls from SDK. +import type { TaskUIControls } from '@webex/cc-store'; +``` + +--- + +## Components to Update + +### CallControlComponent +**File:** `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` + +#### Old Prop Names → New Prop Names + +| Old Prop | New Prop | Change | +|----------|----------|--------| +| `holdResume` | `hold` | **Rename** | +| `muteUnmute` | `mute` | **Rename** | +| `pauseResumeRecording` | `recording` | **Rename** — toggle button (pause/resume) | +| `recordingIndicator` | `recording` | **Same SDK control** — widget must preserve separate recording status badge UI. Use `recording.isVisible` for badge, `recording.isEnabled` for toggle | +| `mergeConference` | `mergeToConference` | **Rename**. SDK also has a separate `conference` control; both are visible during consulting when initiator, agent joined, and not at max participants. Use `mergeToConference` for the Merge action; `conference` is a semantic alias for the same merge-from-consult flow. | +| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — use `transfer` for consult transfer, `transferConference` for conference transfer | +| `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | +| `muteUnmuteConsult` | — | **Remove** (use `mute`) | +| `isConferenceInProgress` | — | **Remove** (use `task.data.isConferenceInProgress` directly; do not use `controls.exitConference.isVisible` as sole source) | +| `isConsultInitiated` | — | **Remove** (if needed, use `task.data.consultStatus` for consult phase distinction) | +| `isConsultInitiatedAndAccepted` | — | **Remove** | +| `isConsultReceived` | — | **Remove** | +| `isConsultInitiatedOrAccepted` | — | **Remove** | +| `isHeld` | `isHeld` | **Retain** — get from the task object (SDK provides hold state). Do NOT derive from `controls.hold.isEnabled`. | +| `consultCallHeld` | — | **Remove** (get from the task object if needed for display) | + +#### Proposed New Interface + +```typescript +interface CallControlComponentProps { + controls: TaskUIControls; // All 17 controls from task.uiControls + // Hold state from the task object (SDK provides this). Do NOT use findHoldStatus() or controls.hold.isEnabled. + isHeld: boolean; + isMuted: boolean; + isRecording: boolean; + holdTime: number; + secondsUntilAutoWrapup: number; + buddyAgents: BuddyDetails[]; // Use exported type from @webex/cc-store or task.types (not a generic Agent type) + consultAgentName: string; + // Actions. onToggleHold(hold) — pass intended hold state (true = hold, false = resume); matches toggleHold(hold: boolean) in task.types. + onToggleHold: (hold: boolean) => void; + onToggleMute: () => void; + onToggleRecording: () => void; + onEndCall: () => void; + onWrapupCall: (reason: string, auxCodeId: string) => void; // Invoked from wrap-up UI on submit + onTransferCall: (payload: TransferPayLoad) => void; // Invoked from transfer popover on submit + onConsultCall: (payload: ConsultPayload) => void; // Invoked from consult popover on submit + onEndConsultCall: () => void; + onConsultTransfer: () => void; + onConsultConference: () => void; + onExitConference: () => void; + onSwitchToConsult: () => void; + onSwitchToMainCall: () => void; + onCancelAutoWrapup: () => void; +} +``` + +### CallControlConsult +**File:** `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/call-control-consult.tsx` + +- Update to use `controls.endConsult`, `controls.mergeToConference`, `controls.switchToMainCall`, `controls.switchToConsult` +- Remove separate `consultTransferConsult`, `mergeConferenceConsult`, `muteUnmuteConsult` props + +### IncomingTaskComponent +**File:** `packages/contact-center/cc-components/src/components/task/IncomingTask/incoming-task.tsx` + +- Accept: `controls.accept.isVisible` / `controls.accept.isEnabled` +- Decline: `controls.decline.isVisible` / `controls.decline.isEnabled` +- Minimal changes — shape is compatible + +### TaskListComponent +**File:** `packages/contact-center/cc-components/src/components/task/TaskList/task-list.tsx` + +- Per-task accept/decline: use `task.uiControls.accept` / `task.uiControls.decline` +- Task status display: if consult status is needed for labels, use `task.data.consultStatus` (SDK provides directly) + +### OutdialCallComponent +**File:** `packages/contact-center/cc-components/src/components/task/OutdialCall/outdial-call.tsx` + +- **No changes needed** — OutdialCall does not use task controls + +--- + +## Full Before/After: CallControlComponent + +### Before +```tsx +// call-control.tsx — old approach +const CallControlComponent = ({ + // 22 individual control props + accept, decline, end, muteUnmute, holdResume, + pauseResumeRecording, recordingIndicator, + transfer, conference, exitConference, mergeConference, + consult, endConsult, consultTransfer, consultTransferConsult, + mergeConferenceConsult, muteUnmuteConsult, + switchToMainCall, switchToConsult, wrapup, + // 7 state flags + isConferenceInProgress, isConsultInitiated, + isConsultInitiatedAndAccepted, isConsultReceived, + isConsultInitiatedOrAccepted, isHeld, consultCallHeld, + // Actions and hook state + isMuted, isRecording, holdTime, onToggleHold, onToggleMute, ... +}) => { + return ( +
+ {holdResume.isVisible && ( + + )} + {muteUnmute.isVisible && ( + + )} + {end.isVisible && ( + + )} + {/* Consult sub-controls */} + {isConsultInitiatedOrAccepted && ( +
+ {endConsult.isVisible && } + {consultTransferConsult.isVisible && } + {mergeConferenceConsult.isVisible && } + {muteUnmuteConsult.isVisible && } +
+ )} + {/* Conference sub-controls */} + {isConferenceInProgress && ( +
+ {exitConference.isVisible && } + {mergeConference.isVisible && } +
+ )} +
+ ); +}; +``` + +### After +```tsx +// call-control.tsx — new approach +const CallControlComponent = ({ + controls, // TaskUIControls — all 17 controls from task.uiControls + isHeld, // From task object (SDK provides hold state) + isMuted, isRecording, holdTime, + onToggleHold, onToggleMute, onEndCall, onEndConsultCall, + onConsultTransfer, onConsultConference, onExitConference, + onSwitchToMainCall, onSwitchToConsult, ... +}: CallControlComponentProps) => { + // Implement openTransferPopover / openConsultPopover / openWrapupPopover (e.g. set state to show popover); popover on submit calls onTransferCall(payload) / onConsultCall(payload) / onWrapupCall(reason, auxCodeId). + // Derive display-only flags from controls (replaces old state flag props) + const isConsulting = controls.endConsult.isVisible; + // Get from task object; do not use controls.exitConference.isVisible as sole source + const isConferencing = currentTask.data.isConferenceInProgress; + + // isHeld comes from the task object (SDK provides hold state). Do NOT use controls.hold.isEnabled for toggle — that is an action flag, not hold state. + return ( +
+ {controls.hold.isVisible && ( + + )} + {controls.mute.isVisible && ( + + )} + {controls.end.isVisible && ( + + )} + {/* Transfer and Consult: buttons open popover/menu; popover invokes onTransferCall(payload) / onConsultCall(payload) on confirm */} + {controls.transfer.isVisible && ( + + )} + {controls.consult.isVisible && ( + + )} + {/* Active consult controls */} + {controls.endConsult.isVisible && ( + + )} + {controls.mergeToConference.isVisible && ( + + )} + {controls.switchToMainCall.isVisible && ( + + )} + {controls.switchToConsult.isVisible && ( + + )} + {/* Conference controls */} + {controls.exitConference.isVisible && ( + + )} + {controls.transferConference.isVisible && ( + + )} + {/* Recording */} + {controls.recording.isVisible && ( + + )} + {/* Wrap Up: button opens wrap-up UI; UI on submit calls onWrapupCall(reason, auxCodeId) */} + {controls.wrapup.isVisible && ( + + )} +
+ ); +}; +``` + +--- + +## Deriving State Flags from Controls + +Components that previously relied on state flags can derive them: + +```typescript +// Old: isConferenceInProgress (boolean prop) +// New: get from the task object (e.g. task.data.isConferenceInProgress). +// Do NOT use controls.exitConference.isVisible — it can be false when consult is active +// even if conference is in progress. +const isConferenceInProgress = currentTask.data.isConferenceInProgress; + +// Old: isConsultInitiatedOrAccepted (boolean prop) +// New: derive from controls +const isConsulting = controls.endConsult.isVisible; + +// Old: isHeld (boolean state flag from getControlsVisibility, derived via findHoldStatus) +// New: get from the task object — SDK provides hold state directly. +// Do NOT use findHoldStatus() (legacy widget-side derivation) or controls.hold.isEnabled (action flag). +const isHeld = /* from task object */; +``` + +--- + +## Critical Utility Files + +### 1. `buildCallControlButtons()` — call-control.utils.ts + +This function builds the main call control button array. It references 12 old control names and 2 state flags: + +| Old Reference | New Equivalent | +|--------------|---------------| +| `controlVisibility.muteUnmute.isVisible` | `controls.mute.isVisible` | +| `controlVisibility.isHeld` | Get from the task object (SDK provides hold state). Do NOT use `findHoldStatus()`. | +| `controlVisibility.holdResume.isEnabled` | `controls.hold.isEnabled` | +| `controlVisibility.holdResume.isVisible` | `controls.hold.isVisible` | +| `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | +| `controlVisibility.consult.isVisible` | `controls.consult.isVisible` | +| `controlVisibility.isConferenceInProgress` | Use `task.data.isConferenceInProgress` (SDK provides this directly). Do not use `controls.exitConference.isVisible` as sole source — it can be false when consult is active | +| `controlVisibility.consultTransfer.isEnabled` / `.isVisible` | Use **`controls.transfer`** or **`controls.transferConference`** (consult vs conference). Do NOT use `controls.consultTransfer` — always hidden in new SDK. | +| `controlVisibility.mergeConference.isEnabled` | `controls.mergeToConference.isEnabled` | +| `controlVisibility.transfer.isEnabled` | `controls.transfer.isEnabled` | +| `controlVisibility.pauseResumeRecording.isEnabled` | `controls.recording.isEnabled` | +| `controlVisibility.exitConference.isEnabled` | `controls.exitConference.isEnabled` | +| `controlVisibility.end.isEnabled` | `controls.end.isEnabled` | + +### 2. `createConsultButtons()` — call-control-custom.utils.ts + +| Old Reference | New Equivalent | +|--------------|---------------| +| `controlVisibility.muteUnmuteConsult` | `controls.mute` | +| `controlVisibility.switchToMainCall` | `controls.switchToMainCall` | +| `controlVisibility.isConferenceInProgress` | Use `task.data.isConferenceInProgress` (SDK provides this directly). Do not use `controls.exitConference.isVisible` as sole source | +| `controlVisibility.consultTransferConsult` | `controls.transfer` / `controls.transferConference` | +| `controlVisibility.mergeConferenceConsult` | `controls.mergeToConference` | +| `controlVisibility.endConsult` | `controls.endConsult` | + +### 3. `filterButtonsForConsultation()` — call-control.utils.ts + +```typescript +// OLD: uses consultInitiated flag (from getControlsVisibility state flags) +// NEW: use task.data.consultStatus from SDK for accurate consult phase. +// Do NOT derive consult-init state from controls.endConsult.isVisible (it spans both initiated and accepted). +// e.g. task.data.consultStatus === 'consultInitiated' for "initiated only" distinction. +``` + +### 4. `getConsultStatusText()` — call-control-custom.utils.ts + +```typescript +// OLD: uses consultInitiated boolean (derived from getControlsVisibility state flags) +// NEW: use task.data.consultStatus from SDK for accurate consult phase. +// e.g. task.data.consultStatus === 'consultInitiated' → 'Consult requested' +// task.data.consultStatus === 'consultAccepted' → 'Consulting' +// Do NOT derive from control visibility (endConsult.isVisible, mergeToConference.isEnabled); +// visibility can change for feature gating and misclassify phase. +``` + +--- + +## Other Impacted Types and Props + +### `CallControlConsultComponentsProps` — task.types.ts +```typescript +// OLD: controlVisibility: ControlVisibility +// NEW: controls: TaskUIControls +``` + +### `ConsultTransferPopoverComponentProps` — task.types.ts +```typescript +// OLD: isConferenceInProgress?: boolean +// NEW: use task.data.isConferenceInProgress (SDK provides directly); not controls.exitConference.isVisible +``` + +### `ControlProps` — task.types.ts (Master Interface) +- `controlVisibility: ControlVisibility` → `controls: TaskUIControls` +- `isHeld: boolean` → get from the task object (SDK provides hold state); remove `findHoldStatus` derivation +- `deviceType: string` → REMOVE (SDK handles) +- `featureFlags: {[key: string]: boolean}` → REMOVE (SDK handles) +- `conferenceEnabled: boolean` → REMOVE (SDK handles) +- `agentId: string` → RETAIN (needed for timer participant lookup) + +### `CallControlCAD` — task package and cc-components view +- **task/src/CallControlCAD/index.tsx:** `deviceType`, `featureFlags`, `conferenceEnabled` are used today in `getControlsVisibility` (task-util.ts lines 421–525). The **SDK** handles feature-flag-like gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled` from agent profile and `callProcessingDetails`. Since widgets will read `task.uiControls` instead of calling `getControlsVisibility`, these props can be **removed** — the SDK has already computed them. **Retain `agentId`** for timer participant lookup. +- **cc-components/.../CallControlCAD/call-control-cad.tsx:** This view consumes `controlVisibility` (and related state flags such as `isConferenceInProgress`, `isHeld`, `isConsultReceived`, `recordingIndicator`, `isConsultInitiatedOrAccepted`). It must be updated to use `TaskUIControls` and the new prop shape when replacing `ControlVisibility`; otherwise migration will leave stale references and break at compile or runtime. + +### Files NOT Impacted (Confirmed) + +| File | Reason | +|------|--------| +| `AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | +| `consult-transfer-popover-hooks.ts` | Pagination/search logic | +| `consult-transfer-list-item.tsx` | Display only | +| `consult-transfer-dial-number.tsx` | Input handling | +| `consult-transfer-empty-state.tsx` | Display only | +| `TaskTimer/index.tsx` | Timer display | +| `Task/index.tsx` | Task card display | +| `OutdialCall/outdial-call.tsx` | No task controls used | + +--- + +## Files to Modify + +**Utils and Web Component layer:** Accept/decline and task-display logic live in **task-list.utils.ts** and **incoming-task.utils.tsx** (they today take `isBrowser` and/or `isDeclineButtonEnabled`). These must be updated when moving to per-task `task.uiControls`. The **wc.ts** file defines r2wc props for the Web Component build; when React props drop `isBrowser`, the WC layer must drop the attribute so consumers stay in sync. + +### Before/After: Utils (accept/decline and task list data) + +#### `extractIncomingTaskData` (incoming-task.utils.tsx) + +**Before:** Signature and logic use `isBrowser` and `isDeclineButtonEnabled`; accept/decline text and disable state are gated by device type and store flag. + +```typescript +export const extractIncomingTaskData = ( + incomingTask: ITask, + isBrowser: boolean, + logger?, + isDeclineButtonEnabled?: boolean +): IncomingTaskData => { + // ... + const acceptText = !incomingTask.data.wrapUpRequired + ? isTelephony && !isBrowser ? 'Ringing...' : 'Accept' + : undefined; + const declineText = !incomingTask.data.wrapUpRequired && isTelephony && isBrowser ? 'Decline' : undefined; + const disableAccept = (isTelephony && !isBrowser) || isAutoAnswering; + const disableDecline = (isTelephony && !isBrowser) || (isAutoAnswering && !isDeclineButtonEnabled); + // ... +}; +``` + +**After:** Remove `isBrowser` and `isDeclineButtonEnabled` from the signature. Derive accept/decline text and disable state from `task.uiControls?.accept` / `task.uiControls?.decline` (or caller-passed visibility) so the util no longer depends on device type or store flag. + +```typescript +export const extractIncomingTaskData = ( + incomingTask: ITask, + logger? +): IncomingTaskData => { + // Use task.uiControls for button visibility and enablement when available + const accept = incomingTask.uiControls?.accept ?? { isVisible: false, isEnabled: false }; + const decline = incomingTask.uiControls?.decline ?? { isVisible: false, isEnabled: false }; + // acceptText: 'Accept' when accept.isVisible, 'Ringing...' for extension telephony if needed from task state + // declineText: 'Decline' when decline.isVisible + // disableAccept: !accept.isEnabled or isAutoAnswering + // disableDecline: !decline.isEnabled or (isAutoAnswering && !decline.isEnabled) + // ... +}; +``` + +#### `extractTaskListItemData` (task-list.utils.ts) + +**Before:** Signature takes `isBrowser`; uses `store.isDeclineButtonEnabled` for disable state; accept/decline text gated by `isBrowser`. + +```typescript +export const extractTaskListItemData = ( + task: ITask, + isBrowser: boolean, + agentId: string, + logger?: ILogger +): TaskListItemData => { + // ... + const acceptText = isTaskIncoming ? (isTelephony && !isBrowser ? 'Ringing...' : 'Accept') : undefined; + const declineText = isTaskIncoming && isTelephony && isBrowser ? 'Decline' : undefined; + const disableDecline = + (isTaskIncoming && isTelephony && !isBrowser) || (isAutoAnswering && !store.isDeclineButtonEnabled); + // ... +}; +``` + +**After:** Remove `isBrowser` param and `store.isDeclineButtonEnabled` usage. Use `task.uiControls?.accept` and `task.uiControls?.decline` for button text and disable state. + +```typescript +export const extractTaskListItemData = ( + task: ITask, + agentId: string, + logger?: ILogger +): TaskListItemData => { + const accept = task.uiControls?.accept ?? { isVisible: false, isEnabled: false }; + const decline = task.uiControls?.decline ?? { isVisible: false, isEnabled: false }; + // acceptText from accept.isVisible / task state; declineText from decline.isVisible + // disableAccept: !accept.isEnabled or isAutoAnswering + // disableDecline: !decline.isEnabled or (isAutoAnswering && !decline.isEnabled) + // ... +}; +``` + +### Before/After: CallControlCAD view (call-control-cad.tsx) + +**Before:** Component receives `controlVisibility: ControlVisibility` and reads legacy state flags and control shapes. + +```tsx +// call-control-cad.tsx +{controlVisibility.isConferenceInProgress && !controlVisibility.wrapup.isVisible && ( + // ... +)} +{controlVisibility.isHeld && !controlVisibility.isConsultReceived && !controlVisibility.consultCallHeld && ( + // ... +)} +{controlVisibility.recordingIndicator.isVisible && ( + // ... +)} +{controlVisibility.isConsultInitiatedOrAccepted && ( + // ... +)} + +``` + +**After:** Component receives `controls: TaskUIControls` (from `task.uiControls`), `isHeld` from the task object, and **consult-state flags** (`isConsultReceived`, `consultCallHeld`) from the task object — do NOT use `controls.consult.isVisible` for hold-chip gating, as that is an action-availability flag, not consult state. Derive conference display from the task object (not solely `controls.exitConference.isVisible`). Pass `controls` to CallControlComponent. + +```tsx +// call-control-cad.tsx +// Conference: get from task object (e.g. task.data.isConferenceInProgress), not controls.exitConference.isVisible +{isConferenceInProgress && !controls.wrapup.isVisible && ( + // ... +)} +// Hold chip: keep consult-state gating; parent passes isConsultReceived and consultCallHeld from the task object +{isHeld && !isConsultReceived && !consultCallHeld && ( + // ... +)} +{controls.recording?.isVisible && ( + // ... +)} +{controls.endConsult?.isVisible && ( + // isConsultInitiatedOrAccepted replaced by controls.endConsult.isVisible (covers both initiated and accepted) + // For phase distinction, use task.data.consultStatus + // ... +)} + +``` + +### Before/After: Web Component layer (wc.ts) + +**Before:** IncomingTask and TaskList Web Components expose `isBrowser` as a boolean prop. + +```typescript +const WebIncomingTask = r2wc(IncomingTaskComponent, { + props: { + incomingTask: 'json', + isBrowser: 'boolean', + accept: 'function', + reject: 'function', + }, +}); +const WebTaskList = r2wc(TaskListComponent, { + props: { + currentTask: 'json', + taskList: 'json', + isBrowser: 'boolean', + acceptTask: 'function', + declineTask: 'function', + logger: 'function', + }, +}); +``` + +**After:** Remove `isBrowser` from both r2wc prop definitions so WC consumers do not pass it; accept/decline visibility comes from per-task `task.uiControls` supplied by the widget layer. + +```typescript +const WebIncomingTask = r2wc(IncomingTaskComponent, { + props: { + incomingTask: 'json', + accept: 'function', + reject: 'function', + }, +}); +const WebTaskList = r2wc(TaskListComponent, { + props: { + currentTask: 'json', + taskList: 'json', + acceptTask: 'function', + declineTask: 'function', + logger: 'function', + }, +}); +``` + +--- + +| File | Action | Impact | +|------|--------|--------| +| `cc-components/.../task/task.types.ts` | Replace `ControlVisibility` with `TaskUIControls`; update `ControlProps`, `CallControlComponentProps`; remove `isBrowser` / `isDeclineButtonEnabled` from `IncomingTaskComponentProps` and TaskList-related prop types when using per-task uiControls | **HIGH** | +| `cc-components/.../CallControl/call-control.tsx` | Update to use `controls` prop | **HIGH** | +| `cc-components/.../CallControl/call-control.utils.ts` | Update `buildCallControlButtons()` and `filterButtonsForConsultation()` | **HIGH** | +| `cc-components/.../CallControlCustom/call-control-custom.utils.ts` | Update `createConsultButtons()` and `getConsultStatusText()` | **HIGH** | +| `cc-components/.../CallControlCustom/call-control-consult.tsx` | Update consult control props | **MEDIUM** | +| `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Update `isConferenceInProgress` prop | **LOW** | +| `cc-components/.../IncomingTask/incoming-task.tsx` | Minor prop updates (remove `isBrowser`, `isDeclineButtonEnabled` when using uiControls) | **LOW** | +| `cc-components/.../IncomingTask/incoming-task.utils.tsx` | Update `extractIncomingTaskData()`: remove `isBrowser` and `isDeclineButtonEnabled` params; derive accept/decline text and disable state from task or passed visibility (e.g. `task.uiControls` or caller-provided flags) | **MEDIUM** | +| `cc-components/.../TaskList/task-list.tsx` | Minor prop updates (remove `isBrowser`; pass task or controls for accept/decline) | **LOW** | +| `cc-components/.../TaskList/task-list.utils.ts` | Update `extractTaskListItemData()`: remove `isBrowser` param and `store.isDeclineButtonEnabled` usage; use `task.uiControls?.accept` / `task.uiControls?.decline` for button text and disable state | **MEDIUM** | +| `cc-components/.../CallControlCAD/call-control-cad.tsx` | Replace `ControlVisibility` / legacy control-shape usage with `TaskUIControls`; update props (`controlVisibility.isConferenceInProgress`, `isHeld`, `isConsultReceived`, `recordingIndicator`, `isConsultInitiatedOrAccepted`, etc.) | **MEDIUM** | +| `cc-components/src/wc.ts` | Update Web Component prop definitions: remove `isBrowser` from `WebIncomingTask` and `WebTaskList` r2wc props when migrating to per-task uiControls; align with React prop changes so WC consumers do not pass obsolete attributes | **LOW** | +| `task/src/CallControlCAD/index.tsx` | **Remove** `deviceType`, `featureFlags`, `conferenceEnabled` (SDK handles via `task.uiControls`); retain `agentId` for timer participant lookup | **MEDIUM** | +| All test files for above | Update mocks and assertions | **HIGH** | + +--- + +## Validation Criteria + +- [ ] CallControl renders all 17 controls correctly +- [ ] Consult sub-controls (endConsult, merge, switch) render correctly +- [ ] Conference sub-controls (exit, transfer conference) render correctly +- [ ] State flag derivation works for conditional rendering +- [ ] IncomingTask accept/decline render correctly +- [ ] TaskList per-task controls render correctly +- [ ] CallControlCAD works with simplified props +- [ ] `buildCallControlButtons()` returns correct buttons for all states +- [ ] `createConsultButtons()` returns correct buttons for consult state +- [ ] No TypeScript compilation errors +- [ ] All component tests pass + +--- + +_Part of the task refactor migration doc set (overview in PR 1/4)._ diff --git a/packages/contact-center/ai-docs/migration/task-list-migration.md b/packages/contact-center/ai-docs/migration/task-list-migration.md new file mode 100644 index 000000000..8ff230831 --- /dev/null +++ b/packages/contact-center/ai-docs/migration/task-list-migration.md @@ -0,0 +1,159 @@ +# TaskList Widget Migration + +## Summary + +The TaskList widget displays all active tasks and allows accept/decline/select. Changes are minimal since task list management (add/remove tasks) stays in the store, and SDK methods are unchanged. The main change is using `task.uiControls` (from the task object) for per-task accept/decline visibility instead of `deviceType`/`isBrowser`. + +--- + +## Old Approach + +### Entry Point +**File:** `packages/contact-center/task/src/helper.ts` +**Hook:** `useTaskList(props: UseTaskListProps)` + +### How It Works (Old) +1. Store maintains `taskList: Record` observable +2. Store maintains `currentTask: ITask | null` observable +3. Hook provides `acceptTask(task)`, `declineTask(task)`, `onTaskSelect(task)` actions +4. Accept/decline visibility gated by `deviceType` / `isBrowser` +5. Task display data extracted by `cc-components/task/Task/task.utils.ts` + +--- + +## New Approach + +### What Changes +1. **Accept/decline visibility** — replace `deviceType`/`isBrowser` gating with per-task `task.uiControls.accept` / `task.uiControls.decline` +2. **Task list management** (add/remove) stays the same — store-managed +3. **SDK methods unchanged**: `task.accept()`, `task.decline()` +4. **Store callbacks unchanged**: `setTaskAssigned`, `setTaskRejected`, `setTaskSelected` + +> **Note:** Widgets do not know about the SDK's internal state machine. All state information comes from the task object properties. + +### Minimal Changes Required +- Accept/decline button visibility per task: use `task.uiControls?.accept` and `task.uiControls?.decline` (each has `isVisible`, `isEnabled`) +- Remove `deviceType` / `isBrowser` from hook props and component +- Task selection logic unchanged +- Optional: if the list must react to control updates without task replacement, subscribe to `'task:ui-controls-updated'` per task (event name; enum `TASK_UI_CONTROLS_UPDATED` may not exist in store yet — use literal) + +### Dead Code Removal + +`getTaskStatus()` and `getConsultStatus()` (in `store/src/task-utils.ts`) are **only** used inside `getControlsVisibility()` (call chain: `getControlsVisibility() → getConsultStatus() → getTaskStatus() → getConsultMPCState()`). Since `getControlsVisibility()` is being removed entirely (replaced by `task.uiControls`), the entire chain becomes dead code and should be deleted. + +If widgets ever need consult status for **display purposes** (e.g., a status label like "Consulting" or "Consult requested"), the SDK provides `task.data.consultStatus` with the same values (`CONSULT_INITIATED`, `CONSULT_ACCEPTED`, `BEING_CONSULTED`, `BEING_CONSULTED_ACCEPTED`, `CONNECTED`, `CONFERENCE`, `CONSULT_COMPLETED`). + +--- + +## Old → New Mapping + +| Aspect | Old | New | +|--------|-----|-----| +| Task list source | `store.taskList` observable | `store.taskList` observable (unchanged) | +| Current task | `store.currentTask` observable | `store.currentTask` observable (unchanged) | +| Accept/decline visibility | `isBrowser` (from `deviceType`) | `task.uiControls.accept` / `task.uiControls.decline` (per-task, from SDK) | +| Accept action | `task.accept()` | `task.accept()` (unchanged) | +| Decline action | `task.decline()` | `task.decline()` (unchanged) | +| Select action | `store.setCurrentTask(task, isClicked)` | Unchanged | +| Consult status (display) | `getConsultStatus(task, agentId)` via `getControlsVisibility()` | Dead code — remove. If needed for display, use `task.data.consultStatus` (SDK provides) | + +--- + +## Before/After: Per-Task Accept/Decline in TaskList + +### Before (TaskList component renders accept/decline per task) +```tsx +// task-list.tsx — old approach +const TaskListComponent = ({ taskList, isBrowser, onAccept, onDecline, onSelect }) => { + return taskList.map((task) => { + // Accept/decline visibility computed per-task from device type + const showAccept = isBrowser; // simplified + return ( + onSelect(task)}> + {showAccept && } + {showAccept && } + + ); + }); +}; +``` + +### After (use per-task `uiControls`) +```tsx +// task-list.tsx — new approach +const TaskListComponent = ({ taskList, onAccept, onDecline, onSelect }) => { + return taskList.map((task) => { + // SDK provides per-task control visibility + const acceptControl = task.uiControls?.accept ?? {isVisible: false, isEnabled: false}; + const declineControl = task.uiControls?.decline ?? {isVisible: false, isEnabled: false}; + return ( + onSelect(task)}> + {acceptControl.isVisible && ( + + )} + {declineControl.isVisible && ( + + )} + + ); + }); +}; +``` + +### Before/After: `useTaskList` Hook + +#### Before +```typescript +// helper.ts — useTaskList (abbreviated) +export const useTaskList = (props: UseTaskListProps) => { + const {deviceType, onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; + const isBrowser = deviceType === 'BROWSER'; // Used for accept/decline visibility + + // ... store callbacks and actions unchanged ... + + return {taskList, acceptTask, declineTask, onTaskSelect, isBrowser}; + // ^^^^^^^^^ passed to component +}; +``` + +#### After +```typescript +// helper.ts — useTaskList (migrated) +export const useTaskList = (props: UseTaskListProps) => { + const {onTaskAccepted, onTaskDeclined, onTaskSelected, logger, taskList} = props; + // REMOVED: deviceType, isBrowser — no longer needed, SDK handles per-task visibility + + // ... store callbacks and actions unchanged ... + + return {taskList, acceptTask, declineTask, onTaskSelect}; + // REMOVED: isBrowser — each task.uiControls.accept/decline provides visibility +}; +``` + +--- + +## Files to Modify + +| File | Action | +|------|--------| +| `task/src/helper.ts` (`useTaskList`) | Remove `isBrowser`, use per-task `uiControls` for accept/decline | +| `task/src/TaskList/index.tsx` | Remove `isBrowser` prop pass-through | +| `cc-components/.../TaskList/task-list.tsx` | Use `task.uiControls.accept/decline` per task | +| `cc-components/.../TaskList/task-list.utils.ts` | Update `extractTaskListItemData()` to use `task.uiControls.accept/decline`; remove `isBrowser`-based gating | +| `cc-components/.../Task/task.utils.ts` | Update task data extraction if needed | +| `store/src/task-utils.ts` | **Remove** `getTaskStatus`, `getConsultStatus`, `getConsultMPCState` — dead code once `getControlsVisibility()` is removed. Retain `findHoldTimestamp`, `isIncomingTask`, and other utils still used elsewhere | + +--- + +## Validation Criteria + +- [ ] Task list displays all active tasks +- [ ] Task selection works (sets `currentTask`) +- [ ] Accept/decline per task works +- [ ] Task status displays correctly (connected, held, wrapup, etc.) +- [ ] Tasks removed from list on end/reject +- [ ] New incoming tasks appear in list + +--- + +_Part of the task refactor migration doc set (overview in PR 1/4)._