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 @@ -20,7 +20,7 @@ The following functions are deleted — their only consumer (`getControlsVisibil
|----------|-------------|
| `deviceType` | SDK handles via `UIControlConfig` |
| `featureFlags` | SDK handles via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled` |
| `conferenceEnabled` | SDK computes conference/mergeToConference/exitConference visibility based on task state and config |
| ~~`conferenceEnabled`~~ | **RESTORED** — This is an application-level config (not a feature flag). See [Fix Log: Restore conferenceEnabled](#fix-restore-conferenceenabled-prop--application-level-conference-gating) below |

### Props retained

Expand Down Expand Up @@ -415,7 +415,7 @@ export function calculateStateTimerData(

## Migration Gotchas

1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. The SDK handles feature-flag gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled`. Widget props `deviceType`, `featureFlags`, and `conferenceEnabled` can be **removed**. There is no `applyFeatureGates` function. **Retain `agentId`** — timer utils need it for participant lookup.
1. **`UIControlConfig` is built by SDK:** Widgets do NOT provide it. The SDK handles feature-flag gating internally via `config.isEndTaskEnabled`, `config.isEndConsultEnabled`, `config.isRecordingEnabled`. Widget props `deviceType` and `featureFlags` can be **removed**. **`conferenceEnabled` is RETAINED** — it is an application-level config (not a feature flag) that gates conference UI at the consumer level. There is no `applyFeatureGates` function. **Retain `agentId`** — timer utils need it for participant lookup.

2. **`isHeld` derivation:** Hold control can be `VISIBLE_DISABLED` in conference/consulting states without meaning the call is held. Do NOT derive from `controls.hold.isEnabled` — it is an action flag (button clickability), not hold state. Get hold state from the task object (SDK tracks hold state internally). `findHoldStatus()` is dead code and will be removed (see [store-task-utils-migration.md](./store-task-utils-migration.md)).

Expand Down Expand Up @@ -456,3 +456,69 @@ export function calculateStateTimerData(
---

_Parent: [migration-overview.md](./migration-overview.md)_

---

## Migration Fix Log

### Fix: `isHeld` Reactivity — Hold Button State and Multi-Login Sync

- **Issue**: After migration, the hold button icon/tooltip did not toggle on click, and multi-login hold/resume did not sync across systems.
- **Root Cause**: The old `controlVisibility.isHeld` was removed. `controls.hold.isEnabled` is an action flag, not state. `task.data.isOnHold` is not populated by SDK at runtime. The SDK state machine also lacked `HOLD_SUCCESS`/`UNHOLD_SUCCESS` transitions for multi-login scenarios.
- **SDK Source of Truth**: `uiControlsComputer.ts` derives `isHeld` from `serverHold ?? state === TaskState.HELD`. `controls.hold` is `VISIBLE_ENABLED` in both `CONNECTED` and `HELD` states — it's an action flag, not a state indicator.
- **Fix Pattern** (in `useCallControl` hook — `helper.ts`):
```typescript
import { isInteractionOnHold } from '@webex/cc-store';

const [isHeld, setIsHeld] = useState<boolean>(() =>
currentTask ? isInteractionOnHold(currentTask) : false
);

useEffect(() => {
setIsHeld(currentTask ? isInteractionOnHold(currentTask) : false);
}, [currentTask]);

// In holdCallback: setIsHeld(true);
// In resumeCallback: setIsHeld(false);
// Return isHeld from hook
```
- **SDK Fix**: Added `HOLD_SUCCESS` handler to `CONNECTED` state and `UNHOLD_SUCCESS` handler to `HELD` state in `TaskStateMachine.ts` for multi-login sync.

### Fix: Restore `conferenceEnabled` Prop — Application-Level Conference Gating

- **Issue**: During the task-refactor migration, the `conferenceEnabled` prop was removed from the widget APIs. This prop is **not a feature flag** — it is an application-level configuration passed from `App.tsx` that controls whether conference-related UI controls should be available to the agent. Without it, applications cannot disable conference features regardless of SDK `uiControls`.
- **Root Cause**: The migration assumed all UI visibility is driven exclusively by `task.uiControls` from the SDK state machine. However, `conferenceEnabled` is an application-level override that gates conference availability at the consumer level, independent of the SDK's computed state.
- **Design Decision (Option A — Widget-Side Override at Button Level)**: `conferenceEnabled` is applied directly in the button builder functions (`buildCallControlButtons` and `createConsultButtons`) where conference-related buttons are defined. When `false`, the `isVisible` property of conference buttons (`conference`, `exitConference`, `merge`) is forced to `false` regardless of SDK `uiControls`. When `true` (default), SDK controls pass through unchanged.
- **Gating Pattern** (in button builder functions):
```typescript
// call-control.utils.ts — buildCallControlButtons
// conferenceEnabled param defaults to true
{
id: 'conference',
isVisible: conferenceEnabled && (controls?.mergeToConference?.isVisible ?? false) && !!handleConsultConferencePress,
},
{
id: 'exitConference',
isVisible: conferenceEnabled && (controls?.exitConference?.isVisible ?? false),
},

// call-control-custom.utils.ts — createConsultButtons
{
key: 'conference',
isVisible: conferenceEnabled && (controls?.mergeToConference?.isVisible ?? false),
},
```
- **Prop Flow**: `App.tsx` → `CallControl`/`CallControlCAD` → `useCallControl` hook → returned as prop → `CallControlComponent` → `buildCallControlButtons()` / `CallControlConsultComponent` → `createConsultButtons()`
- **Files Changed**:
- `cc-components/…/task.types.ts`: Added `conferenceEnabled: boolean` to `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps`
- `cc-components/…/call-control.utils.ts`: Added `conferenceEnabled` param to `buildCallControlButtons`, gated `conference` and `exitConference` buttons
- `cc-components/…/call-control-custom.utils.ts`: Added `conferenceEnabled` param to `createConsultButtons`, gated `conference` (merge) button
- `cc-components/…/call-control.tsx`: Destructured `conferenceEnabled`, passed to `buildCallControlButtons`
- `cc-components/…/call-control-consult.tsx`: Destructured `conferenceEnabled`, passed to `createConsultButtons`
- `cc-components/…/call-control-cad.tsx`: Destructured `conferenceEnabled`, passed to `CallControlConsultComponent`
- `task/src/task.types.ts`: Added `conferenceEnabled` to `CallControlProps` and `useCallControlProps`
- `task/src/helper.ts`: Destructured `conferenceEnabled` (default `true`), returned from hook
- `task/src/CallControl/index.tsx` and `CallControlCAD/index.tsx`: Pass `conferenceEnabled` to `useCallControl`
- `cc-widgets/src/wc.ts`: Exposed `conferenceEnabled` as r2wc `boolean` prop on `WebCallControl` and `WebCallControlCAD`
- **Consumer Usage**: Apps pass `conferenceEnabled={true|false}` as a prop to `<CallControl>` or `<CallControlCAD>`. Web component consumers set the `conference-enabled` attribute. Defaults to `true` if not provided.
- **Result**: Conference buttons (merge, exit conference) are hidden when `conferenceEnabled` is `false`, while all other SDK-driven controls remain unaffected.
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,11 @@ This function builds the main call control button array. It references 12 old co
- `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)
- ~~`conferenceEnabled: boolean` → REMOVE~~ **RESTORED** — application-level config (not a feature flag), applied at button builder level
- `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.
- **task/src/CallControlCAD/index.tsx:** `deviceType` and `featureFlags` 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`, `deviceType` and `featureFlags` can be **removed** — the SDK has already computed them. **`conferenceEnabled` is RETAINED** — it is an application-level configuration (not a feature flag) passed from the consumer app. **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)
Expand Down Expand Up @@ -581,7 +581,7 @@ const WebTaskList = r2wc(TaskListComponent, {
| `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** |
| `task/src/CallControlCAD/index.tsx` | **Remove** `deviceType`, `featureFlags` (SDK handles via `task.uiControls`); **retain** `conferenceEnabled` (app-level config) and `agentId` (timer participant lookup) | **MEDIUM** |
| All test files for above | Update mocks and assertions | **HIGH** |

---
Expand All @@ -603,3 +603,50 @@ const WebTaskList = r2wc(TaskListComponent, {
---

_Part of the task refactor migration doc set (overview in PR 1/4)._

---

## Migration Fix Log

### Fix: Duplicate Transfer Button — Wrong `uiControls` Field Mapping

- **Issue**: After accepting a call, both "Transfer" and "Transfer Call" buttons appeared simultaneously. The `transferConsult` button and the consult strip `transfer` button were both reading `controls.transfer` instead of `controls.consultTransfer`.
- **Root Cause**: Three button definitions all mapped to `controls.transfer`:
- `call-control.utils.ts` — `transferConsult` button used `controls.transfer` (should be `controls.consultTransfer`)
- `call-control-custom.utils.ts` — consult strip `transfer` button used `controls.transfer` (should be `controls.consultTransfer`)
- `call-control.utils.ts` — main `transfer` button correctly used `controls.transfer`
- **SDK Source of Truth**: `uiControlsComputer.ts` computes `consultTransfer: DISABLED` for `CONNECTED` state and only enables it during active consultation. The main `transfer` control handles the primary transfer action.
- **Fix**:
- `call-control.utils.ts` L252-258: Changed `transferConsult` button's `disabled` and `isVisible` from `controls?.transfer` to `controls?.consultTransfer`
- `call-control-custom.utils.ts` L46-54: Changed consult strip `transfer` button's `disabled` and `isVisible` from `controls?.transfer` to `controls?.consultTransfer`
- **Result**: Only the main "Transfer Call" button shows in `CONNECTED` state. The `transferConsult` button only appears when `consultTransfer` is explicitly enabled by the SDK during active consultation.

### Fix: Hold Button Icon/Tooltip Not Toggling & Multi-Login Hold State Not Syncing

- **Issue**: (1) After clicking Hold, the button icon stayed as pause and tooltip stayed as "Hold the call" instead of changing to play/"Resume the call". (2) In multi-login scenarios, holding/resuming on one system did not reflect on the other system.
- **Root Cause**:
- The old `controlVisibility.isHeld` was removed during migration. The replacement `controls.hold.isEnabled` is an **action flag** (can the user click hold?), not the current hold state. `task.data.isOnHold` exists in SDK types but is not populated at runtime.
- For multi-login: The SDK's `TaskStateMachine.ts` `CONNECTED` state had no handler for `HOLD_SUCCESS` (another system held), and `HELD` state had no handler for `UNHOLD_SUCCESS` (another system resumed). These events were silently dropped.
- **Fix (Widgets)**:
- `helper.ts` (`useCallControl` hook): Added `useState(isHeld)` initialized from `isInteractionOnHold(currentTask)`. Updated `holdCallback` to `setIsHeld(true)` and `resumeCallback` to `setIsHeld(false)`. Added `useEffect([currentTask])` to re-sync from `isInteractionOnHold` on task reference changes (covers multi-login `refreshTaskList`).
- `call-control.utils.ts`: Added `isHeld: boolean` parameter to `buildCallControlButtons()`. Hold button uses `isHeld ? 'play-bold' : 'pause-bold'` for icon and `isHeld ? RESUME_CALL : HOLD_CALL` for tooltip.
- `call-control.tsx`: Destructured `isHeld` from props, passed to `buildCallControlButtons()` and `handleToggleHoldUtil()`.
- `task.types.ts`: Added `'isHeld'` to `CallControlComponentProps` pick list.
- **Fix (SDK)**: Added `HOLD_SUCCESS` transition in `CONNECTED` state and `UNHOLD_SUCCESS` transition in `HELD` state of `TaskStateMachine.ts`, both with actions `['updateTaskData', 'setHoldState', 'emitTaskHold'/'emitTaskResume']`.
- **Result**: Hold button icon/tooltip toggles correctly on click. Multi-login hold/resume state syncs across systems via SDK state machine transitions.

### Fix: Restore `conferenceEnabled` Prop — Application-Level Conference Gating

- **Issue**: The `conferenceEnabled` prop was removed from widget APIs during migration. This is an application-level configuration (not a feature flag) passed from the consumer app that controls whether conference-related UI controls are available to the agent.
- **Root Cause**: The migration assumed all UI visibility is exclusively SDK-driven. However, `conferenceEnabled` is a consumer-level override independent of SDK state.
- **Design Decision**: Option A — widget-side override applied directly at the button builder level. When `conferenceEnabled` is `false`, the `isVisible` property of conference-related buttons (`conference`, `exitConference`, `merge`) is forced to `false` in `buildCallControlButtons()` and `createConsultButtons()`. Defaults to `true`.
- **Component-Layer Changes**:
- `task.types.ts`: Added `conferenceEnabled: boolean` to `ControlProps`, `CallControlComponentProps`, `CallControlConsultComponentsProps`
- `call-control.utils.ts`: Added `conferenceEnabled` param to `buildCallControlButtons()`, gated `conference` and `exitConference` buttons via `conferenceEnabled && (controls?.…isVisible)`
- `call-control-custom.utils.ts`: Added `conferenceEnabled` param to `createConsultButtons()`, gated `conference` (merge) button
- `call-control.tsx`: Destructured `conferenceEnabled` from props, passed to `buildCallControlButtons()`
- `call-control-consult.tsx`: Destructured `conferenceEnabled`, passed to `createConsultButtons()`
- `call-control-cad.tsx`: Destructured `conferenceEnabled`, passed to `CallControlConsultComponent`
- `cc-widgets/src/wc.ts`: Exposed `conferenceEnabled` as r2wc `boolean` prop on `WebCallControl` and `WebCallControlCAD`
- **No SDK changes required**: Gating is applied at the widget component layer directly on button definitions.
- **Result**: Conference merge and exit buttons are hidden when `conferenceEnabled={false}`. All other SDK-driven controls remain unaffected.
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,19 @@ const IncomingTaskComponent = ({ acceptControl, declineControl, onAccept, onReje
---

_Parent: [migration-overview.md](./migration-overview.md)_

---

## Migration Fix Log

### Fix: Restore `isDeclineButtonEnabled` from Store to Component Level

- **Issue**: During the task-refactor migration, `store.isDeclineButtonEnabled` was removed from the IncomingTask and TaskList component layers. The migration docs instructed replacing it with `task.uiControls.decline.isEnabled`. However, the store property is still set by `handleAutoAnswer` in `storeEventsWrapper.ts` and needs to be kept as an additional override for the decline button enabled state.
- **Root Cause**: The migration assumed `task.uiControls.decline.isEnabled` fully replaces `store.isDeclineButtonEnabled`, but the store property provides an additional auto-answer override that the SDK state machine may not account for in all scenarios.
- **Fix**:
- `task/src/helper.ts` (`useIncomingTask`): Reads `store.isDeclineButtonEnabled` and merges it with the SDK's `declineControl` — if either the SDK or the store says decline is enabled, the button is enabled: `isEnabled: sdkDeclineControl.isEnabled || store.isDeclineButtonEnabled`.
- `task/src/TaskList/index.tsx`: Reads `store.isDeclineButtonEnabled` and passes it as a prop to `TaskListComponent`.
- `cc-components/.../task.types.ts`: Added `isDeclineButtonEnabled?: boolean` to `TaskListComponentProps`.
- `cc-components/.../task-list.tsx`: Destructures `isDeclineButtonEnabled` and passes it to `extractTaskListItemData`.
- `cc-components/.../task-list.utils.ts`: `extractTaskListItemData` accepts `isDeclineButtonEnabled` param and merges it with `task.uiControls.decline.isEnabled`: `isEnabled: sdkDecline.isEnabled || !!isDeclineButtonEnabled`.
- **Result**: The decline button is enabled when either the SDK's `task.uiControls.decline.isEnabled` is `true` OR `store.isDeclineButtonEnabled` is `true` (set by auto-answer handler). Both IncomingTask and TaskList components respect this combined logic.
Loading