From 708abfdacc518bd16a7952cae13bfda646219886 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 12:51:54 +0530 Subject: [PATCH 01/15] =?UTF-8?q?docs(ai-docs):=20task=20refactor=20migrat?= =?UTF-8?q?ion=20=E2=80=94=20planning=20and=20reference=20(PR=204/4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add planning and reference documentation: - 011: Execution plan (10-milestone spec-first implementation plan) - 012: Task lifecycle flows (14 end-to-end scenarios, old vs new code paths) - 013: File inventory (25 files referencing old control names/types, cross-reference matrix) Made-with: Cursor --- ai-docs/migration/011-execution-plan.md | 438 ++++++++++++ .../012-task-lifecycle-flows-old-vs-new.md | 664 ++++++++++++++++++ ...3-file-inventory-old-control-references.md | 171 +++++ 3 files changed, 1273 insertions(+) create mode 100644 ai-docs/migration/011-execution-plan.md create mode 100644 ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md create mode 100644 ai-docs/migration/013-file-inventory-old-control-references.md diff --git a/ai-docs/migration/011-execution-plan.md b/ai-docs/migration/011-execution-plan.md new file mode 100644 index 000000000..9588b141c --- /dev/null +++ b/ai-docs/migration/011-execution-plan.md @@ -0,0 +1,438 @@ +# Migration Doc 011: Execution Plan (Spec-First) + +## Overview + +This plan uses the **spec-driven development** approach already established in CC Widgets. Each milestone starts with writing specs (tests), then implementing to make specs pass. This ensures parity between old and new behavior. + +--- + +## Prerequisites + +- [ ] CC SDK `task-refactor` branch merged and released (or linked locally) +- [ ] `TaskUIControls` type and `task:ui-controls-updated` event available in `@webex/contact-center` +- [ ] `task.uiControls` getter available on `ITask` +- [ ] Team alignment on migration approach + +--- + +## Milestone Overview + +| # | Milestone | Scope | Est. Effort | Risk | Depends On | +|---|-----------|-------|-------------|------|------------| +| M0 | SDK integration setup | Link SDK, verify types | 1 day | Low | SDK release | +| M1 | Types & constants alignment | Import new types, add adapters | 1-2 days | Low | M0 | +| M2 | Store event wiring simplification | Simplify event handlers, add `ui-controls-updated` | 2-3 days | Medium | M0 | +| M3 | Store task-utils thinning | Remove redundant utils | 1-2 days | Low | M2 | +| M3.5 | Timer utils migration | Update timer-utils to accept `TaskUIControls` | 1 day | Low | M3 | +| M4 | CallControl hook refactor | Core: replace `getControlsVisibility` with `task.uiControls` | 3-5 days | **High** | M1, M2, M3, M3.5 | +| M5 | Component layer update | Update `cc-components` prop interfaces | 2-3 days | Medium | M4 | +| M6 | IncomingTask migration | Use `task.uiControls.accept/decline` | 1 day | Low | M1 | +| M7 | TaskList migration | Optional status enhancement | 1 day | Low | M1 | +| M8 | Integration testing & cleanup | E2E, remove dead code, docs | 2-3 days | Medium | All | + +**Total estimated effort: 15–23 days** + +--- + +## Detailed Milestone Plans + +### M0: SDK Integration Setup (1 day) + +**Goal:** Verify the new SDK API is available and types compile. + +**Steps:** +1. Update `@webex/contact-center` dependency to task-refactor version +2. Verify `TaskUIControls` type is importable +3. Verify `task.uiControls` getter exists on `ITask` +4. Verify `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` constant exists +5. Run `yarn build` to confirm no type errors + +**Spec:** Write a minimal integration test that creates a mock task and reads `uiControls`. + +**Validation:** `yarn build` passes with new SDK version. + +--- + +### M1: Types & Constants Alignment (1-2 days) + +**Ref:** [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) + +**Goal:** Import new SDK types into widget packages without changing runtime behavior. + +**Spec first:** +```typescript +// Test: TaskUIControls type compatibility +import { TaskUIControls } from '@webex/contact-center'; +const controls: TaskUIControls = task.uiControls; +expect(controls.hold).toHaveProperty('isVisible'); +expect(controls.hold).toHaveProperty('isEnabled'); +``` + +**Steps:** +1. Add `TaskUIControls` import to `task/src/task.types.ts` +2. Create adapter type mapping old control names → new (for gradual migration) +3. Add `TASK_UI_CONTROLS_UPDATED` to store event constants +4. Review and annotate constants for deprecation + +**Validation:** All existing tests still pass. New types compile. + +--- + +### M2: Store Event Wiring Simplification (2-3 days) + +**Ref:** [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) + +**Goal:** Simplify store event handlers; add `task:ui-controls-updated` subscription. + +**Spec first:** +```typescript +// Test: Store registers ui-controls-updated listener +describe('registerTaskEventListeners', () => { + it('should register TASK_UI_CONTROLS_UPDATED handler', () => { + store.registerTaskEventListeners(mockTask); + expect(mockTask.on).toHaveBeenCalledWith( + TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, expect.any(Function) + ); + }); + + it('should NOT call refreshTaskList on TASK_HOLD', () => { + // Verify simplified handler + }); +}); +``` + +**Steps:** +1. Add `TASK_UI_CONTROLS_UPDATED` handler in `registerTaskEventListeners()` +2. Replace `refreshTaskList()` calls with callback-only for: TASK_HOLD, TASK_RESUME, TASK_CONSULT_END, all conference events +3. Keep `refreshTaskList()` only for: initialization, hydration +4. Update tests for each modified handler + +**Order (low risk → high risk):** +1. Add new `TASK_UI_CONTROLS_UPDATED` handler (additive, no breakage) +2. Simplify conference event handlers (less critical) +3. Simplify hold/resume handlers (medium impact) +4. Simplify consult handlers (medium impact) +5. Remove unnecessary `refreshTaskList()` calls (highest impact) + +**Validation:** All existing widget tests pass. Store correctly fires callbacks on events. + +--- + +### M3: Store Task-Utils Thinning (1-2 days) + +**Ref:** [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) + +**Goal:** Remove utility functions that are now handled by SDK. + +**Spec first:** +```typescript +// Test: Verify no consumers remain for removed functions +// (Static analysis — ensure no import of getConsultStatus, getIsConferenceInProgress, etc.) +``` + +**Steps:** +1. Search codebase for each function to verify consumers +2. Remove functions with zero consumers after M2 changes +3. Mark functions with remaining consumers for later removal (after M4/M5) +4. Keep display-only functions (`getTaskStatus`, `getConferenceParticipants`, etc.) + +**Validation:** Build succeeds. No runtime errors. + +--- + +### M3.5: Timer Utils Migration (1 day) + +**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md#timer-utils-migration) + +**Goal:** Update `calculateStateTimerData()` and `calculateConsultTimerData()` to accept `TaskUIControls`. + +**Why:** These functions accept `controlVisibility` (old shape) as a parameter and derive timer labels from it. They must be migrated before M4 since `useCallControl` depends on them. + +**Spec first:** +```typescript +describe('calculateStateTimerData with TaskUIControls', () => { + it('should return Wrap Up label when controls.wrapup.isVisible', () => { + const controls = { ...getDefaultUIControls(), wrapup: { isVisible: true, isEnabled: true } }; + const result = calculateStateTimerData(mockTask, controls, agentId); + expect(result.label).toBe('Wrap Up'); + }); +}); +``` + +**Steps:** +1. Update `calculateStateTimerData(task, controls, agentId)` signature +2. Replace `controlVisibility.isConsultInitiatedOrAccepted` → `controls.endConsult.isVisible` +3. Replace `controlVisibility.isHeld` → derive from task data via `findHoldStatus(task, 'mainCall', agentId)` (do NOT derive from `controls.hold.isEnabled` — hold can be disabled in consult/transition states even when call is not held) +4. Update `calculateConsultTimerData(task, controls, agentId)` similarly +5. Update all test cases + +**Also fix during this milestone:** +- Consolidate `findHoldTimestamp` dual signatures (store vs task-util versions) + +--- + +### M4: CallControl Hook Refactor (3-5 days) — CRITICAL PATH + +**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) + +**Goal:** Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl`. + +**Spec first (write ALL specs before implementation):** + +```typescript +describe('useCallControl with task.uiControls', () => { + // Parity specs: each scenario must produce identical control states + + describe('connected voice call', () => { + it('should show hold, mute, end, transfer, consult controls', () => { + mockTask.uiControls = { + hold: { isVisible: true, isEnabled: true }, + mute: { isVisible: true, isEnabled: true }, + end: { isVisible: true, isEnabled: true }, + transfer: { isVisible: true, isEnabled: true }, + consult: { isVisible: true, isEnabled: true }, + // ... all other controls disabled + }; + const { result } = renderHook(() => useCallControl(props)); + expect(result.current.controls.hold).toEqual({ isVisible: true, isEnabled: true }); + }); + }); + + describe('held voice call', () => { + it('should show hold (enabled=true for resume), disable end/mute', () => { /* ... */ }); + }); + + describe('consulting', () => { + it('should show endConsult, switchToMainCall, switchToConsult, mergeToConference', () => { /* ... */ }); + }); + + describe('conferencing', () => { + it('should show exitConference, disable hold', () => { /* ... */ }); + }); + + describe('wrapping up', () => { + it('should show only wrapup control', () => { /* ... */ }); + }); + + describe('digital channel', () => { + it('should show only accept, end, transfer, wrapup', () => { /* ... */ }); + }); + + describe('ui-controls-updated event', () => { + it('should re-render when task emits ui-controls-updated', () => { /* ... */ }); + }); + + describe('no task', () => { + it('should return default controls when no task', () => { /* ... */ }); + }); +}); +``` + +**Steps:** +1. Write comprehensive parity specs (30+ test cases covering all states) +2. Create `adaptSDKControls()` adapter function (maps SDK names to old names if needed during transition) +3. Replace `getControlsVisibility()` call in `useCallControl` with `task.uiControls` +4. Add `task:ui-controls-updated` subscription with `useEffect` +5. Update hook return type to use new control names +6. Remove old state flags from return +7. Run parity specs — fix any mismatches + +**Parity verification approach:** +- For each state (connected, held, consulting, conferencing, wrapping-up, offered): + - Mock task with known data + - Call old `getControlsVisibility()` → capture result + - Read `task.uiControls` → capture result + - Compare: every control must have same `isVisible`/`isEnabled` + - Document and resolve any differences (old bug vs new behavior) + +**Validation:** All 30+ parity specs pass. All existing hook tests pass (with updated assertions). + +--- + +### M5: Component Layer Update (2-3 days) + +**Ref:** [010-component-layer-migration.md](./010-component-layer-migration.md) + +**Goal:** Update `cc-components` to accept new control prop shape. + +**Spec first:** +```typescript +describe('CallControlComponent', () => { + it('should render hold button from controls.hold', () => { + render(); + expect(screen.getByTestId('hold-button')).toBeVisible(); + }); + + it('should hide mute button when controls.mute.isVisible=false', () => { + const controls = { ...mockControls, mute: { isVisible: false, isEnabled: false } }; + render(); + expect(screen.queryByTestId('mute-button')).not.toBeInTheDocument(); + }); +}); +``` + +**Steps:** +1. Update `CallControlComponentProps` to accept `controls: TaskUIControls` +2. Update `CallControlComponent` to read from `controls.*` +3. Update `CallControlConsult` component +4. Update `IncomingTaskComponent` if needed +5. Update all component tests + +**Validation:** All component tests pass. Visual output identical. + +--- + +### M6: IncomingTask Migration (1 day) + +**Ref:** [005-incoming-task-migration.md](./005-incoming-task-migration.md) + +**Goal:** Use `task.uiControls.accept/decline` in IncomingTask. + +**Spec first:** +```typescript +describe('useIncomingTask with uiControls', () => { + it('should derive accept visibility from task.uiControls.accept', () => { /* ... */ }); + it('should derive decline visibility from task.uiControls.decline', () => { /* ... */ }); +}); +``` + +**Steps:** +1. Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls` +2. Update component props +3. Update tests + +**Validation:** IncomingTask tests pass. Accept/decline work for voice and digital. + +--- + +### M7: TaskList Migration (1 day) + +**Ref:** [006-task-list-migration.md](./006-task-list-migration.md) + +**Goal:** Optionally enhance task status display; verify compatibility. + +**Steps:** +1. Verify `useTaskList` works with new SDK (should be compatible) +2. Optionally enhance `getTaskStatus()` to use SDK state info +3. Update tests if any changes made + +**Validation:** TaskList renders correctly with all task states. + +--- + +### M8: Integration Testing & Cleanup (2-3 days) + +**Goal:** End-to-end verification, dead code removal, documentation update. + +**Steps:** + +1. **E2E Test Matrix:** + +| Scenario | Widgets Involved | Verify | +|----------|-----------------|--------| +| Incoming voice call → accept → end | IncomingTask, CallControl | Accept button, end button | +| Incoming voice call → reject | IncomingTask | Decline button, RONA timer | +| Connected → hold → resume | CallControl | Hold toggle, timer | +| Connected → consult → end consult | CallControl | Consult flow controls | +| Connected → consult → conference | CallControl | Merge, conference controls | +| Conference → exit | CallControl | Exit conference | +| Conference → transfer conference | CallControl | Transfer conference | +| Connected → transfer (blind) | CallControl | Transfer popover | +| Connected → end → wrapup | CallControl | Wrapup button | +| Outdial → connected → end | OutdialCall, CallControl | Full outdial flow | +| Digital task → accept → end → wrapup | IncomingTask, CallControl | Digital controls | +| Multiple tasks in list | TaskList | Task selection, per-task controls | +| Page refresh → hydrate | All | Restore state correctly | + +2. **Bug fixes (found during analysis):** + - Fix recording callback cleanup mismatch (`TASK_RECORDING_PAUSED` vs `CONTACT_RECORDING_PAUSED`) + - Consolidate `findHoldTimestamp` dual signatures (store vs task-util) + - Add `task:wrapup` race guard if needed + +3. **Dead code removal:** + - Delete `task/src/Utils/task-util.ts` (or reduce to `findHoldTimestamp` only) + - Remove unused store utils + - Remove unused constants + - Remove unused type definitions + +4. **Documentation updates:** + - Update `task/ai-docs/widgets/CallControl/AGENTS.md` and `ARCHITECTURE.md` + - Update `task/ai-docs/widgets/IncomingTask/AGENTS.md` and `ARCHITECTURE.md` + - Update `store/ai-docs/AGENTS.md` and `ARCHITECTURE.md` + - Update `cc-components/ai-docs/AGENTS.md` + +5. **Final validation:** + - `yarn build` — no errors + - `yarn test:unit` — all pass + - `yarn test:styles` — no lint errors + - Sample apps (React + WC) work correctly + +--- + +## Risk Mitigation + +### High-Risk Areas +1. **CallControl hook refactor (M4)** — largest change, most complex logic + - **Mitigation:** Comprehensive parity specs written BEFORE implementation + - **Rollback:** Old `getControlsVisibility()` stays in codebase until M8 cleanup + +2. **Store event wiring (M2)** — removing `refreshTaskList()` could cause stale data + - **Mitigation:** Gradual removal; keep `refreshTaskList()` as fallback initially + - **Rollback:** Re-add `refreshTaskList()` calls if data staleness detected + +3. **Consult/Conference flows** — most complex state transitions + - **Mitigation:** Dedicated parity specs for every consult/conference scenario + - **Mitigation:** Test with Agent Desktop to verify identical behavior + +### Low-Risk Areas +- OutdialCall (no changes needed) +- IncomingTask (minimal changes) +- TaskList (minimal changes) +- Types alignment (additive, no runtime changes) + +--- + +## Spec-First Checklist + +For each milestone, complete these in order: + +1. [ ] Write spec file with test cases for NEW behavior +2. [ ] Write parity tests (old behavior == new behavior) where applicable +3. [ ] Run specs — verify they FAIL (red) +4. [ ] Implement changes +5. [ ] Run specs — verify they PASS (green) +6. [ ] Run ALL existing tests — verify no regressions +7. [ ] `yarn build` — verify compilation +8. [ ] Code review with team +9. [ ] Mark milestone complete + +--- + +## Recommended Order of Execution + +``` +M0 (SDK setup) + │ + ├── M1 (types) ─┐ + └── M2 (store events) ─┤ + │ + M3 (store utils) + │ + M3.5 (timer utils) + │ + M4 (CallControl hook) ← CRITICAL PATH + │ + M5 (components) + │ + ├── M6 (IncomingTask) + └── M7 (TaskList) + │ + M8 (integration + cleanup + bug fixes) +``` + +**M0 → M1 + M2 (parallel) → M3 → M3.5 → M4 → M5 → M6 + M7 (parallel) → M8** + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md new file mode 100644 index 000000000..0337952a6 --- /dev/null +++ b/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md @@ -0,0 +1,664 @@ +# Migration Doc 012: Task Lifecycle Flows — Complete Old vs New + +## Purpose + +This document traces **every task scenario from start to finish**, showing exactly what happens at each step in both the old and new approach. Each flow maps: +- User/system action +- SDK event chain +- Widget/store layer behavior +- UI controls shown +- State machine state (new only) + +--- + +## Flow 1: Incoming Voice Call → Accept → Connected + +### Old Flow +``` +1. WebSocket: AgentContactReserved +2. SDK emits: task:incoming (with ITask) +3. Store: handleIncomingTask() → refreshTaskList() → cc.taskManager.getAllTasks() + → runInAction: store.taskList updated, store.incomingTask set +4. Widget: IncomingTask (observer) re-renders +5. Hook: useIncomingTask registers callbacks (TASK_ASSIGNED, TASK_REJECT, etc.) +6. UI Controls: getAcceptButtonVisibility(isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel) + getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall) +7. User clicks Accept +8. Hook: incomingTask.accept() → SDK API call +9. WebSocket: AgentContactAssigned +10. SDK emits: task:assigned +11. Store: handleTaskAssigned() → refreshTaskList() → update taskList, set currentTask +12. Hook: TASK_ASSIGNED callback fires → onAccepted({task}) +13. Widget: CallControl appears +14. UI Controls: getControlsVisibility() computes all 22 controls from raw task data + → hold, mute, end, transfer, consult visible and enabled +``` + +### New Flow +``` +1. WebSocket: AgentContactReserved +2. SDK: TaskManager maps to TaskEvent.TASK_INCOMING +3. SDK: task.sendStateMachineEvent(TASK_INCOMING) → State: IDLE → OFFERED +4. SDK: computeUIControls(OFFERED, context) → accept/decline visible (WebRTC) +5. SDK emits: task:incoming, task:ui-controls-updated +6. Store: handleIncomingTask() → store.incomingTask set +7. Widget: IncomingTask (observer) re-renders +8. Hook: useIncomingTask reads task.uiControls.accept / task.uiControls.decline +9. User clicks Accept +10. Hook: incomingTask.accept() → SDK API call +11. WebSocket: AgentContactAssigned +12. SDK: TaskManager maps to TaskEvent.ASSIGN +13. SDK: task.sendStateMachineEvent(ASSIGN) → State: OFFERED → CONNECTED +14. SDK: computeUIControls(CONNECTED, context) → hold, mute, end, transfer, consult +15. SDK emits: task:assigned, task:ui-controls-updated +16. Store: handleTaskAssigned() → set currentTask +17. Widget: CallControl appears +18. Hook: useCallControl reads task.uiControls directly (no computation) +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Controls computation | Widget runs `getControlsVisibility()` on every render | SDK pre-computes `task.uiControls` on every state transition | +| Data freshness | `refreshTaskList()` re-fetches all tasks | SDK updates `task.data` in state machine action | +| Re-render trigger | MobX observable change after `refreshTaskList()` | `task:ui-controls-updated` event | + +--- + +## Flow 2: Incoming Voice Call → Reject / RONA (Timeout) + +### Old Flow +``` +1-6. Same as Flow 1 (incoming → show accept/decline) +7. User clicks Decline (or timer expires → auto-reject) +8. Hook: incomingTask.decline() → SDK API call +9. WebSocket: AgentContactReservedTimeout (RONA) or rejection +10. SDK emits: task:rejected +11. Store: handleTaskReject() → refreshTaskList() → remove task from list +12. Hook: TASK_REJECT callback fires → onRejected({task}) +13. Widget: IncomingTask unmounts +``` + +### New Flow +``` +1-8. Same as Flow 1 new (incoming → OFFERED state) +7. User clicks Decline (or timer expires → auto-reject) +8. Hook: incomingTask.decline() → SDK API call +9. WebSocket: RONA or rejection +10. SDK: task.sendStateMachineEvent(RONA) → State: OFFERED → TERMINATED +11. SDK: computeUIControls(TERMINATED) → all controls disabled +12. SDK emits: task:rejected, task:ui-controls-updated +13. Store: handleTaskReject() → remove task from list +14. Hook: TASK_REJECT callback fires → onRejected({task}) +15. Widget: IncomingTask unmounts +``` + +--- + +## Flow 3: Connected → Hold → Resume + +### Old Flow +``` +1. User clicks Hold +2. Hook: toggleHold(true) → currentTask.hold() +3. SDK: API call to backend +4. WebSocket: AgentContactHeld +5. SDK emits: task:hold +6. Store: refreshTaskList() → cc.taskManager.getAllTasks() → update store.taskList +7. Hook: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) +8. UI Controls: getControlsVisibility() recalculates: + → findHoldStatus(task, 'mainCall', agentId) returns true + → holdResume: { isVisible: true, isEnabled: true } (for resume) + → end: { isVisible: true, isEnabled: false } (disabled while held) + → mute: same +9. User clicks Resume +10. Hook: toggleHold(false) → currentTask.resume() +11. WebSocket: AgentContactUnheld +12. SDK emits: task:resume +13. Store: refreshTaskList() → update store.taskList +14. Hook: TASK_RESUME callback fires → onHoldResume({ isHeld: false }) +15. UI Controls: getControlsVisibility() recalculates → controls back to connected state +``` + +### New Flow +``` +1. User clicks Hold +2. Hook: toggleHold(true) → currentTask.hold() +3. SDK: sends TaskEvent.HOLD_INITIATED → State: CONNECTED → HOLD_INITIATING +4. SDK: computeUIControls(HOLD_INITIATING) → hold visible but transitioning +5. SDK emits: task:ui-controls-updated (optimistic) +6. SDK: API call to backend +7. WebSocket: AgentContactHeld +8. SDK: sends TaskEvent.HOLD_SUCCESS → State: HOLD_INITIATING → HELD +9. SDK: computeUIControls(HELD) → hold visible (for resume), end/mute disabled +10. SDK emits: task:hold, task:ui-controls-updated +11. Store: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) +12. Hook: controls state updated via task:ui-controls-updated listener +13. User clicks Resume +14. Hook: toggleHold(false) → currentTask.resume() +15. SDK: sends TaskEvent.UNHOLD_INITIATED → State: HELD → RESUME_INITIATING +16. SDK: API call to backend +17. WebSocket: AgentContactUnheld +18. SDK: sends TaskEvent.UNHOLD_SUCCESS → State: RESUME_INITIATING → CONNECTED +19. SDK: computeUIControls(CONNECTED) → all active controls enabled +20. SDK emits: task:resume, task:ui-controls-updated +21. Store: TASK_RESUME callback fires +22. Hook: controls state updated +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Hold initiation | Immediate API call, wait for response | Optimistic: HOLD_INITIATING state before API call | +| Intermediate states | None (binary: held or not) | HOLD_INITIATING, RESUME_INITIATING (UI can show spinner) | +| Controls update | After `refreshTaskList()` + `getControlsVisibility()` | After each state transition via `task:ui-controls-updated` | + +--- + +## Flow 4: Connected → Consult → End Consult + +### Old Flow +``` +1. User initiates consult +2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) +3. SDK: API call to backend +4. WebSocket: AgentConsultCreated +5. SDK emits: task:consultCreated +6. Store: handleConsultCreated() → refreshTaskList() → update taskList +7. UI Controls: getControlsVisibility() recalculates: + → getConsultStatus() returns CONSULT_INITIATED + → endConsult visible, consultTransfer visible, switchToMainCall visible + → hold disabled, transfer hidden +8. WebSocket: AgentConsulting (consult agent answered) +9. SDK emits: task:consulting +10. Store: handleConsulting() → refreshTaskList() +11. UI Controls: getControlsVisibility() recalculates: + → getConsultStatus() returns CONSULT_ACCEPTED + → mergeConference enabled, consultTransfer enabled + → switchToMainCall/switchToConsult available +12. User clicks End Consult +13. Hook: endConsultCall() → currentTask.endConsult(payload) +14. WebSocket: AgentConsultEnded +15. SDK emits: task:consultEnd +16. Store: refreshTaskList() +17. UI Controls: getControlsVisibility() → back to connected state controls +``` + +### New Flow +``` +1. User initiates consult +2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) +3. SDK: sends TaskEvent.CONSULT → State: CONNECTED → CONSULT_INITIATING +4. SDK: computeUIControls(CONSULT_INITIATING) → consult controls transitioning +5. SDK emits: task:ui-controls-updated +6. SDK: API call → success +7. SDK: sends TaskEvent.CONSULT_SUCCESS → stays CONSULT_INITIATING (waiting for agent) +8. WebSocket: AgentConsultCreated → TaskEvent.CONSULT_CREATED +9. SDK: task data updated +10. WebSocket: AgentConsulting → TaskEvent.CONSULTING_ACTIVE +11. SDK: State: CONSULT_INITIATING → CONSULTING +12. SDK: context.consultDestinationAgentJoined = true +13. SDK: computeUIControls(CONSULTING): + → endConsult visible+enabled, mergeToConference visible+enabled + → switchToMainCall visible, switchToConsult visible + → transfer visible (for consult transfer) + → hold disabled (in consult) +14. SDK emits: task:consulting, task:ui-controls-updated +15. Hook: controls updated via listener +16. User clicks End Consult +17. Hook: endConsultCall() → currentTask.endConsult(payload) +18. WebSocket: AgentConsultEnded → TaskEvent.CONSULT_END +19. SDK: State: CONSULTING → CONNECTED (or CONFERENCING if from conference) +20. SDK: context cleared (consultInitiator=false, consultDestinationAgentJoined=false) +21. SDK: computeUIControls(CONNECTED) → back to normal connected controls +22. SDK emits: task:consultEnd, task:ui-controls-updated +23. Hook: controls updated +``` + +### Key Difference +| Step | Old | New | +|------|-----|-----| +| Consult state tracking | `getConsultStatus()` inspects participants | State machine: CONSULT_INITIATING → CONSULTING | +| Agent joined detection | `ConsultStatus.CONSULT_ACCEPTED` from participant flags | `context.consultDestinationAgentJoined` set by action | +| Controls | Computed from raw data every render | Pre-computed on each state transition | + +--- + +## Flow 5: Consulting → Merge to Conference → Exit Conference + +### Old Flow +``` +1. In consulting state (Flow 4 steps 1-11) +2. User clicks Merge Conference +3. Hook: consultConference() → currentTask.consultConference() +4. SDK: API call +5. WebSocket: AgentConsultConferenced / ParticipantJoinedConference +6. SDK emits: task:conferenceStarted / task:participantJoined +7. Store: handleConferenceStarted() → refreshTaskList() +8. UI Controls: getControlsVisibility(): + → task.data.isConferenceInProgress = true + → exitConference visible+enabled + → consult visible+enabled (can add more agents) + → hold disabled (in conference) + → mergeConference hidden (already in conference) +9. User clicks Exit Conference +10. Hook: exitConference() → currentTask.exitConference() +11. WebSocket: ParticipantLeftConference / AgentConsultConferenceEnded +12. SDK emits: task:conferenceEnded / task:participantLeft +13. Store: handleConferenceEnded() → refreshTaskList() +14. UI Controls: getControlsVisibility() → may go to wrapup or connected +``` + +### New Flow +``` +1. In CONSULTING state (Flow 4 new steps 1-15) +2. User clicks Merge Conference +3. Hook: consultConference() → currentTask.consultConference() +4. SDK: sends TaskEvent.MERGE_TO_CONFERENCE → State: CONSULTING → CONF_INITIATING +5. SDK: computeUIControls(CONF_INITIATING) → transitioning controls +6. SDK emits: task:ui-controls-updated +7. WebSocket: AgentConsultConferenced → TaskEvent.CONFERENCE_START +8. SDK: State: CONF_INITIATING → CONFERENCING +9. SDK: computeUIControls(CONFERENCING): + → exitConference visible+enabled + → consult visible (can add more) + → hold disabled + → mergeToConference hidden +10. SDK emits: task:conferenceStarted, task:ui-controls-updated +11. User clicks Exit Conference +12. Hook: exitConference() → currentTask.exitConference() +13. WebSocket: ParticipantLeftConference → TaskEvent.PARTICIPANT_LEAVE +14. SDK: guards check: didCurrentAgentLeaveConference? shouldWrapUp? +15. SDK: State: CONFERENCING → WRAPPING_UP (if wrapup required) or CONNECTED +16. SDK: computeUIControls for new state +17. SDK emits: task:conferenceEnded, task:ui-controls-updated +``` + +--- + +## Flow 6: Connected → End → Wrapup → Complete + +### Old Flow +``` +1. User clicks End +2. Hook: endCall() → currentTask.end() +3. SDK: API call +4. WebSocket: ContactEnded + AgentWrapup +5. SDK emits: task:end, task:wrapup +6. Store: handleTaskEnd() may refresh; handleWrapup → refreshTaskList() +7. UI Controls: getControlsVisibility(): + → getWrapupButtonVisibility(task) → { isVisible: task.data.wrapUpRequired } + → all other controls hidden/disabled +8. Widget: Wrapup form appears +9. User selects reason, clicks Submit +10. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) +11. SDK: API call +12. WebSocket: AgentWrappedup +13. SDK emits: task:wrappedup +14. Store: refreshTaskList() → task removed or updated +15. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) +16. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) +``` + +### New Flow +``` +1. User clicks End +2. Hook: endCall() → currentTask.end() +3. SDK: API call +4. WebSocket: ContactEnded → TaskEvent.CONTACT_ENDED +5. SDK: guards: shouldWrapUp? → State: CONNECTED → WRAPPING_UP +6. SDK: computeUIControls(WRAPPING_UP) → only wrapup visible+enabled +7. SDK emits: task:end, task:wrapup, task:ui-controls-updated +8. Hook: controls updated → only wrapup control shown +9. Widget: Wrapup form appears +10. User selects reason, clicks Submit +11. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) +12. SDK: API call +13. WebSocket: AgentWrappedup → TaskEvent.WRAPUP_COMPLETE +14. SDK: State: WRAPPING_UP → COMPLETED +15. SDK: computeUIControls(COMPLETED) → all controls disabled +16. SDK: emitTaskWrappedup action → cleanupResources action +17. SDK emits: AGENT_WRAPPEDUP (AgentWrappedUp), task:ui-controls-updated, task:cleanup +18. Store: handleTaskEnd/cleanup → remove task from list +19. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) +20. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) +``` + +--- + +## Flow 7: Auto-Wrapup Timer + +### Old Flow +``` +1. Task enters wrapup state (Flow 6 steps 1-7) +2. Hook: useEffect detects currentTask.autoWrapup && controlVisibility.wrapup +3. Hook: reads currentTask.autoWrapup.getTimeLeftSeconds() +4. Hook: setInterval every 1s → decrements secondsUntilAutoWrapup +5. Widget: AutoWrapupTimer component shows countdown +6. If user clicks Cancel: cancelAutoWrapup() → currentTask.cancelAutoWrapupTimer() +7. If timer reaches 0: SDK auto-submits wrapup with default reason +``` + +### New Flow +``` +1. Task enters WRAPPING_UP state (Flow 6 new steps 1-8) +2. Hook: useEffect detects currentTask.autoWrapup && controls.wrapup.isVisible + (changed: controlVisibility.wrapup → controls.wrapup) +3-7. Same as old — auto-wrapup is a widget-layer timer concern, SDK unchanged +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Timer trigger condition | `controlVisibility.wrapup` (computed by widget) | `controls.wrapup.isVisible` (from SDK) | +| Timer behavior | Unchanged | Unchanged | +| Cancel action | Unchanged | Unchanged | + +--- + +## Flow 8: Recording Pause/Resume + +### Old Flow +``` +1. User clicks Pause Recording +2. Hook: toggleRecording() → currentTask.pauseRecording() +3. SDK: API call +4. WebSocket: ContactRecordingPaused +5. SDK emits: task:recordingPaused +6. Store: fires callback +7. Hook: pauseRecordingCallback() → setIsRecording(false), onRecordingToggle({isRecording: false}) +8. UI Controls: getPauseResumeRecordingButtonVisibility() unchanged (still visible) +9. Widget: Button label changes to "Resume Recording" +``` + +### New Flow +``` +1. User clicks Pause Recording +2. Hook: toggleRecording() → currentTask.pauseRecording() +3. SDK: sends TaskEvent.PAUSE_RECORDING → context.recordingInProgress = false +4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: true } + (visible and enabled = agent can click to resume recording) +5. SDK: API call +6. WebSocket: ContactRecordingPaused +7. SDK emits: task:recordingPaused, task:ui-controls-updated +8. Hook: setIsRecording(false), controls updated +9. Widget: Button label changes to "Resume Recording" +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Recording state tracking | Widget local state (`isRecording`) | SDK context (`recordingInProgress`) + widget local state | +| Recording button visibility | `getPauseResumeRecordingButtonVisibility()` | `controls.recording` from SDK | +| Recording indicator | Separate `recordingIndicator` control | Merged into `recording` control | + +--- + +## Flow 9: Blind Transfer + +### Old Flow +``` +1. User clicks Transfer, selects destination +2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) +3. SDK: API call +4. WebSocket: AgentBlindTransferred +5. SDK emits: (leads to task:end or task:wrapup depending on config) +6. Store: refreshTaskList() +7. UI Controls: getControlsVisibility() → wrapup or all disabled +``` + +### New Flow +``` +1. User clicks Transfer, selects destination +2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) +3. SDK: API call +4. WebSocket: AgentBlindTransferred → TaskEvent.TRANSFER_SUCCESS +5. SDK: guards: shouldWrapUpOrIsInitiator? +6. SDK: State → WRAPPING_UP (if wrapup required) or stays CONNECTED +7. SDK: computeUIControls for new state +8. SDK emits: task:end/task:wrapup, task:ui-controls-updated +``` + +--- + +## Flow 10: Digital Task (Chat/Email) — Accept → End → Wrapup + +### Old Flow +``` +1. WebSocket: AgentContactReserved (mediaType: chat) +2. SDK emits: task:incoming +3. Store: handleIncomingTask() +4. UI Controls: getAcceptButtonVisibility(): + → isBrowser && isDigitalChannel → accept visible + → decline NOT visible (digital channels) +5. User clicks Accept +6. incomingTask.accept() → task:assigned +7. UI Controls: getControlsVisibility(): + → end visible, transfer visible, wrapup hidden + → hold/mute/consult/conference/recording: all hidden (digital) +8. User clicks End +9. currentTask.end() → task:end, task:wrapup +10. UI Controls: wrapup visible (if wrapUpRequired) +``` + +### New Flow +``` +1. WebSocket: AgentContactReserved (mediaType: chat) +2. SDK: State: IDLE → OFFERED, channelType: DIGITAL +3. SDK: computeDigitalUIControls(OFFERED) → accept visible, decline hidden +4. SDK emits: task:incoming, task:ui-controls-updated +5. User clicks Accept +6. incomingTask.accept() → ASSIGN → State: OFFERED → CONNECTED +7. SDK: computeDigitalUIControls(CONNECTED) → end visible, transfer visible + → hold/mute/consult/conference/recording: all disabled (digital) +8. User clicks End +9. currentTask.end() → CONTACT_ENDED → WRAPPING_UP +10. SDK: computeDigitalUIControls(WRAPPING_UP) → wrapup visible +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Channel detection | Widget checks `mediaType === 'telephony'` vs chat/email | SDK checks `channelType: VOICE` vs `DIGITAL` | +| Digital controls | `getControlsVisibility()` with `isCall=false, isDigitalChannel=true` | `computeDigitalUIControls()` — much simpler logic | +| Controls shown | Same end result | Same end result (accept, end, transfer, wrapup only) | + +--- + +## Flow 11: Page Refresh → Hydration + +### Old Flow +``` +1. Agent refreshes browser page +2. SDK reconnects, receives AgentContact for active task +3. SDK emits: task:hydrate +4. Store: handleTaskHydrate() → refreshTaskList() → cc.taskManager.getAllTasks() +5. Store: sets currentTask, taskList from fetched data +6. Widgets: observer re-renders with restored task +7. UI Controls: getControlsVisibility() computes from raw task data + → Must correctly derive: held state, consult state, conference state + → Error-prone: depends on raw interaction data being complete +``` + +### New Flow +``` +1. Agent refreshes browser page +2. SDK reconnects, receives AgentContact for active task +3. SDK: TaskManager sends TaskEvent.HYDRATE with task data +4. SDK: State machine guards determine correct state: + → isInteractionTerminated? → WRAPPING_UP + → isInteractionConsulting? → CONSULTING + → isInteractionHeld? → HELD + → isInteractionConnected? → CONNECTED + → isConferencingByParticipants? → CONFERENCING + → default → IDLE (data update only) +5. SDK: computeUIControls for resolved state → correct controls for restored state +6. SDK emits: task:hydrate, task:ui-controls-updated +7. Store: handleTaskHydrate() → set currentTask, taskList +8. Widgets: observer re-renders; controls from task.uiControls are correct +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| State recovery | `refreshTaskList()` + `getControlsVisibility()` from raw data | State machine guards determine correct state | +| Reliability | Can show wrong controls if interaction data is incomplete | Guards explicitly check each condition; predictable | +| Conference recovery | Depends on `isConferenceInProgress` flag in data | Guard: `isConferencingByParticipants` counts agents | + +--- + +## Flow 12: Outdial → New Task + +### Old Flow +``` +1. User enters number, clicks Dial +2. Hook: startOutdial(destination, origin) → cc.startOutdial(destination, origin) +3. SDK: API call (CC-level, not task-level) +4. Backend creates task, sends AgentContactReserved +5. Flow continues as Flow 1 (Incoming → Accept → Connected) +``` + +### New Flow +``` +Identical — outdial initiation is CC-level. Once the task is created, +the state machine takes over and flows follow Flow 1 new approach. +No changes needed. +``` + +--- + +## Flow 13: Consult Transfer (from Consulting State) + +### Old Flow +``` +1. In consulting state (Flow 4 steps 1-11) +2. User clicks Consult Transfer +3. Hook: consultTransfer() checks currentTask.data.isConferenceInProgress: + → false: currentTask.consultTransfer() + → true: currentTask.transferConference() +4. SDK: API call +5. WebSocket: AgentConsultTransferred / AgentConferenceTransferred +6. SDK emits: task events (leads to end/wrapup) +7. Store: refreshTaskList() +8. UI Controls: wrapup or all disabled +``` + +### New Flow +``` +1. In CONSULTING state (Flow 4 new steps 1-15) +2. User clicks Transfer (transfer control now handles consult transfer) +3. Hook: consultTransfer() checks controls.transferConference.isVisible: + → false: currentTask.consultTransfer() + → true: currentTask.transferConference() +4. SDK: API call +5. WebSocket: → TaskEvent.TRANSFER_SUCCESS or TRANSFER_CONFERENCE_SUCCESS +6. SDK: State → WRAPPING_UP or TERMINATED +7. SDK: computeUIControls for new state +8. SDK emits: events + task:ui-controls-updated +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| Conference check | `currentTask.data.isConferenceInProgress` | `controls.transferConference.isVisible` (or keep data check) | +| Transfer button | Separate `consultTransferConsult` control | `controls.transfer` (consult transfer) / `controls.transferConference` (conference transfer) | + +--- + +## Flow 14: Switch Between Main Call and Consult Call + +### Old Flow +``` +1. In consulting state, agent is on consult leg +2. User clicks "Switch to Main Call" +3. Hook: switchToMainCall(): + → currentTask.resume(findMediaResourceId(task, 'consult')) + (resumes consult media → puts consult on hold → main call active) +4. WebSocket: AgentContactHeld (consult) + AgentContactUnheld (main) +5. SDK emits: task:hold, task:resume +6. Store: refreshTaskList() (twice) +7. UI Controls: getControlsVisibility(): + → consultCallHeld = true → switchToConsult visible + → switchToMainCall hidden +``` + +### New Flow +``` +1. In CONSULTING state, agent is on consult leg +2. User clicks "Switch to Main Call" +3. Hook: switchToMainCall(): + → currentTask.resume(findMediaResourceId(task, 'consult')) +4. SDK: HOLD_INITIATED / UNHOLD_INITIATED → state machine tracks +5. SDK: context.consultCallHeld updated +6. SDK: computeUIControls(CONSULTING, updated context): + → switchToConsult visible (consult is now held) + → switchToMainCall hidden +7. SDK emits: task:hold, task:resume, task:ui-controls-updated +8. Hook: controls updated via listener +``` + +### Key Difference +| Aspect | Old | New | +|--------|-----|-----| +| consultCallHeld tracking | `findHoldStatus(task, 'consult', agentId)` | `context.consultCallHeld` in state machine | +| Controls update | After 2x `refreshTaskList()` | Single `task:ui-controls-updated` after state settles | + +--- + +## State Machine States → Widget Controls Summary + +| TaskState (New) | Old Equivalent | Controls Visible | +|-----------------|---------------|-----------------| +| `IDLE` | No task / before incoming | All disabled | +| `OFFERED` | Incoming task shown | accept, decline (WebRTC voice); accept only (digital) | +| `CONNECTED` | Active call | hold, mute, end, transfer, consult, recording | +| `HOLD_INITIATING` | (no equivalent) | hold visible (transitioning) | +| `HELD` | `isHeld = true` | hold (for resume), transfer, consult | +| `RESUME_INITIATING` | (no equivalent) | hold visible (transitioning) | +| `CONSULT_INITIATING` | `ConsultStatus.CONSULT_INITIATED` | endConsult, switchToMainCall, switchToConsult | +| `CONSULTING` | `ConsultStatus.CONSULT_ACCEPTED` | endConsult, mergeToConference, transfer, switchToMainCall/Consult | +| `CONF_INITIATING` | (no equivalent) | conference transitioning | +| `CONFERENCING` | `isConferenceInProgress = true` | exitConference, consult, transferConference | +| `WRAPPING_UP` | `wrapUpRequired && interaction terminated` | wrapup only | +| `COMPLETED` | Task removed after wrapup | All disabled | +| `TERMINATED` | Task rejected / ended without wrapup | All disabled | + +--- + +## Event Chain Mapping: Old Widget Events → New SDK State Machine + +| Widget Action | Old Event Chain | New Event Chain | +|--------------|----------------|-----------------| +| Accept task | `accept()` → `task:assigned` | `accept()` → `ASSIGN` → `task:assigned` + `task:ui-controls-updated` | +| Decline task | `decline()` → `task:rejected` | `decline()` → `RONA` → `task:rejected` + `task:ui-controls-updated` | +| Hold | `hold()` → `task:hold` | `hold()` → `HOLD_INITIATED` → `HOLD_SUCCESS` → `task:hold` + `task:ui-controls-updated` | +| Resume | `resume()` → `task:resume` | `resume()` → `UNHOLD_INITIATED` → `UNHOLD_SUCCESS` → `task:resume` + `task:ui-controls-updated` | +| End call | `end()` → `task:end` | `end()` → `CONTACT_ENDED` → `task:end` + `task:ui-controls-updated` | +| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `AGENT_WRAPPEDUP` (AgentWrappedUp) + `task:ui-controls-updated` | +| Transfer | `transfer()` → `task:end` | `transfer()` → `TRANSFER_SUCCESS` → `task:end` + `task:ui-controls-updated` | +| Start consult | `consult()` → `task:consultCreated` | `consult()` → `CONSULT` → `CONSULT_SUCCESS` → `CONSULTING_ACTIVE` → `task:consulting` + `task:ui-controls-updated` | +| End consult | `endConsult()` → `task:consultEnd` | `endConsult()` → `CONSULT_END` → `task:consultEnd` + `task:ui-controls-updated` | +| Merge conference | `consultConference()` → `task:conferenceStarted` | `consultConference()` → `MERGE_TO_CONFERENCE` → `CONFERENCE_START` → `task:conferenceStarted` + `task:ui-controls-updated` | +| Exit conference | `exitConference()` → `task:conferenceEnded` | `exitConference()` → `PARTICIPANT_LEAVE` / `CONFERENCE_END` → `task:conferenceEnded` + `task:ui-controls-updated` | +| Pause recording | `pauseRecording()` → `task:recordingPaused` | `pauseRecording()` → `PAUSE_RECORDING` → `task:recordingPaused` + `task:ui-controls-updated` | +| Resume recording | `resumeRecording()` → `task:recordingResumed` | `resumeRecording()` → `RESUME_RECORDING` → `task:recordingResumed` + `task:ui-controls-updated` | + +### Universal New Pattern +Every action now follows: **User action → SDK method → State machine event(s) → task.uiControls recomputed → `task:ui-controls-updated` emitted**. Widgets never need to compute controls themselves. + +--- + +## Timer Utils: Old vs New Control References + +**File:** `packages/contact-center/task/src/Utils/timer-utils.ts` + +| Old Reference in Timer Utils | New Equivalent | +|-----------------------------|---------------| +| `controlVisibility.wrapup?.isVisible` | `controls.wrapup.isVisible` | +| `controlVisibility.consultCallHeld` | `controls.switchToConsult.isVisible` | +| `controlVisibility.isConsultInitiated` | `controls.endConsult.isVisible && !controls.mergeToConference.isEnabled` | + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/ai-docs/migration/013-file-inventory-old-control-references.md new file mode 100644 index 000000000..728155129 --- /dev/null +++ b/ai-docs/migration/013-file-inventory-old-control-references.md @@ -0,0 +1,171 @@ +# Migration Doc 013: Complete File Inventory — Old Control References + +## Purpose + +This is the definitive inventory of **every file** in CC Widgets that references old task control names, state flags, or the `ControlVisibility` type. Use this as a checklist during migration to ensure nothing is missed. + +--- + +## Summary + +| Category | Files with Old Refs | Files Unaffected | +|----------|-------------------|-----------------| +| Widget hooks (`task/src/`) | 4 | 1 (`index.ts`) | +| Widget utils (`task/src/Utils/`) | 4 | 0 | +| Widget entry points (`task/src/*/index.tsx`) | 4 | 1 (`OutdialCall`) | +| cc-components types | 1 (central type file) | 0 | +| cc-components utils | 2 | 3+ (unaffected) | +| cc-components components | 4 | 8+ (unaffected) | +| Store | 3 | 1 (`store.ts`) | +| **Total files to modify** | **~25** | **~13 unaffected** | + +--- + +## Files WITH Old Control References (Must Migrate) + +### Tier 1: Core Logic Files (Highest Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 1 | `task/src/Utils/task-util.ts` | `getControlsVisibility()` — the entire function (~650 lines). Computes all 22 controls + 7 state flags. Calls `getConsultStatus`, `findHoldStatus`, `getIsConferenceInProgress`, etc. | [Doc 002](./002-ui-controls-migration.md) | +| 2 | `task/src/helper.ts` (`useCallControl`) | `controlVisibility = useMemo(() => getControlsVisibility(...))`, references `controlVisibility.muteUnmute`, `controlVisibility.wrapup`, passes to `calculateStateTimerData`, `calculateConsultTimerData` | [Doc 004](./004-call-control-hook-migration.md) | +| 3 | `store/src/storeEventsWrapper.ts` | `refreshTaskList()` in 15+ event handlers; missing `TASK_UI_CONTROLS_UPDATED` subscription | [Doc 003](./003-store-event-wiring-migration.md) | +| 4 | `store/src/task-utils.ts` | `getConsultStatus()`, `findHoldStatus()`, `getIsConferenceInProgress()`, `getConferenceParticipantsCount()`, `getIsCustomerInCall()`, `getIsConsultInProgress()` — all used by `getControlsVisibility()` | [Doc 008](./008-store-task-utils-migration.md) | + +### Tier 2: Component Utility Files (High Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 5 | `cc-components/.../task/task.types.ts` | `ControlVisibility` interface (22 controls + 7 flags), `ControlProps.controlVisibility`, `CallControlComponentProps` picks `controlVisibility`, `CallControlConsultComponentsProps.controlVisibility`, `ConsultTransferPopoverComponentProps.isConferenceInProgress`, `ControlProps.isHeld`, `ControlProps.deviceType`, `ControlProps.featureFlags` | [Doc 009](./009-types-and-constants-migration.md), [Doc 010](./010-component-layer-migration.md) | +| 6 | `cc-components/.../call-control.utils.ts` | `buildCallControlButtons()` — 20+ references: `controlVisibility.muteUnmute`, `.holdResume`, `.isHeld`, `.consult`, `.transfer`, `.isConferenceInProgress`, `.consultTransfer`, `.mergeConference`, `.pauseResumeRecording`, `.exitConference`, `.end`, `.switchToConsult`. Also `filterButtonsForConsultation(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | +| 7 | `cc-components/.../call-control-custom.utils.ts` | `createConsultButtons()` — `controlVisibility.muteUnmuteConsult`, `.switchToMainCall`, `.isConferenceInProgress`, `.consultTransferConsult`, `.mergeConferenceConsult`, `.endConsult`. Also `getConsultStatusText(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | + +### Tier 3: Component Files (Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 8 | `cc-components/.../CallControl/call-control.tsx` | Receives `controlVisibility` as prop, passes to `buildCallControlButtons()`, `createConsultButtons()`, `filterButtonsForConsultation()` | [Doc 010](./010-component-layer-migration.md) | +| 9 | `cc-components/.../CallControlCustom/call-control-consult.tsx` | Receives `controlVisibility` from `CallControlConsultComponentsProps`, passes to `createConsultButtons()` | [Doc 010](./010-component-layer-migration.md) | +| 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | +| 10b | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Directly references `controlVisibility.isConferenceInProgress`, `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld`, `controlVisibility.recordingIndicator`, `controlVisibility.wrapup`, `controlVisibility.isConsultInitiatedOrAccepted` | [Doc 010](./010-component-layer-migration.md) | +| 10c | `cc-components/src/wc.ts` | Registers `WebCallControlCADComponent` with `commonPropsForCallControl` — props must align with new `TaskUIControls` shape | [Doc 010](./010-component-layer-migration.md) | +| 10d | `cc-components/.../TaskList/task-list.utils.ts` | `extractTaskListItemData(task, isBrowser, agentId)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation; `store.isDeclineButtonEnabled` | [Doc 006](./006-task-list-migration.md) | +| 10e | `cc-components/.../IncomingTask/incoming-task.utils.tsx` | `extractIncomingTaskData(incomingTask, isBrowser, logger, isDeclineButtonEnabled)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation | [Doc 005](./005-incoming-task-migration.md) | + +### Tier 4: Widget Entry Points (Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 11 | `task/src/CallControl/index.tsx` | Passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from store to `useCallControl` | [Doc 004](./004-call-control-hook-migration.md) | +| 12 | `task/src/CallControlCAD/index.tsx` | Same as #11 — passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 010](./010-component-layer-migration.md) | +| 13 | `task/src/TaskList/index.tsx` | Passes `deviceType` from store for `isBrowser` computation | [Doc 006](./006-task-list-migration.md) | +| 13b | `task/src/IncomingTask/index.tsx` | Passes `deviceType` from store to `useIncomingTask` — migrate to `task.uiControls.accept`/`decline` | [Doc 005](./005-incoming-task-migration.md) | + +### Tier 5: Utility Files (Low-Medium Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 14 | `task/src/Utils/timer-utils.ts` | `calculateStateTimerData(task, controlVisibility, agentId)` — uses `controlVisibility.wrapup`, `.consultCallHeld`, `.isConsultInitiated` | [Doc 004](./004-call-control-hook-migration.md#timer-utils-migration) | +| 15 | `task/src/Utils/useHoldTimer.ts` | Uses `findHoldTimestamp` from task-util.ts (dual signature issue) — NOT a control visibility reference, but part of consolidation | [Doc 008](./008-store-task-utils-migration.md) | +| 16 | `task/src/task.types.ts` | `useCallControlProps` interface — includes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 009](./009-types-and-constants-migration.md) | +| 17 | `task/src/Utils/constants.ts` | Timer label constants — no control refs, but check for unused consult state constants | [Doc 009](./009-types-and-constants-migration.md) | + +### Tier 6: Store Constants (Low Impact) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 18 | `store/src/constants.ts` | `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `CONSULT_STATE_INITIATED`, `CONSULT_STATE_COMPLETED`, etc. — used by `getConsultStatus()` | [Doc 009](./009-types-and-constants-migration.md) | +| 19 | `store/src/store.ts` | `refreshTaskList()` method, `isDeclineButtonEnabled` observable | [Doc 003](./003-store-event-wiring-migration.md) | + +### Tier 7: Test Files (Must Update After Implementation) + +| # | File | Old References | Migration Doc | +|---|------|---------------|--------------| +| 20 | `task/tests/**` | All `useCallControl` tests mock `getControlsVisibility()` return | [Doc 011](./011-execution-plan.md) | +| 21 | `cc-components/tests/**` | All CallControl component tests mock `controlVisibility` prop | [Doc 011](./011-execution-plan.md) | +| 22 | `store/tests/**` | Tests for event handlers, `refreshTaskList()`, task-utils | [Doc 011](./011-execution-plan.md) | + +--- + +## Files WITHOUT Old Control References (No Migration Needed) + +| File | Reason | +|------|--------| +| `task/src/OutdialCall/index.tsx` | CC-level API, no task controls | +| `task/src/index.ts` | Re-exports only | +| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | +| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.utils.ts` | Pure timer formatting | +| `cc-components/.../CallControlCustom/consult-transfer-popover-hooks.ts` | Pagination/search logic | +| `cc-components/.../CallControlCustom/consult-transfer-list-item.tsx` | Display only | +| `cc-components/.../CallControlCustom/consult-transfer-dial-number.tsx` | Input handling | +| `cc-components/.../CallControlCustom/consult-transfer-empty-state.tsx` | Display only | +| `cc-components/.../TaskTimer/index.tsx` | Timer display | +| `cc-components/.../Task/index.tsx` | Task card display | +| `cc-components/.../Task/task.utils.ts` | Task data extraction for display | +| `cc-components/.../OutdialCall/outdial-call.tsx` | No task controls | +| `cc-components/.../constants.ts` | UI string constants | +| `cc-components/.../OutdialCall/constants.ts` | Outdial constants | + +--- + +## Old Control Name → File Reference Matrix + +This shows exactly which files reference each old control name: + +| Old Control Name | Files That Reference It | +|------------------|------------------------| +| `muteUnmute` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `muteUnmuteConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `holdResume` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `pauseResumeRecording` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `recordingIndicator` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `mergeConference` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | +| `consultTransferConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `mergeConferenceConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | +| `isConferenceInProgress` | `task-util.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `task.types.ts`, `consult-transfer-popover.tsx` | +| `isConsultInitiated` | `task-util.ts`, `call-control.utils.ts`, `timer-utils.ts`, `task.types.ts` | +| `isConsultInitiatedAndAccepted` | `task-util.ts`, `task.types.ts` | +| `isConsultReceived` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `isConsultInitiatedOrAccepted` | `task-util.ts`, `helper.ts`, `timer-utils.ts`, `task.types.ts` | +| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts`, `call-control-cad.tsx` | +| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `call-control-cad.tsx`, `task.types.ts` | +| `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | +| `isBrowser` (legacy flag) | `task-list.utils.ts`, `incoming-task.utils.tsx`, `task-list.tsx`, `incoming-task.tsx` — replace with `task.uiControls.accept`/`decline` | +| `isDeclineButtonEnabled` (legacy flag) | `incoming-task.utils.tsx`, `incoming-task.tsx`, `task-list.utils.ts` — replace with `task.uiControls.decline.isEnabled` | + +--- + +## Migration Execution Order by File + +Based on dependencies: + +``` +1. task.types.ts (cc-components) — Define new TaskUIControls prop, keep ControlVisibility during transition +2. task/src/task.types.ts — Import TaskUIControls, update useCallControlProps +3. store/src/constants.ts — Mark deprecated constants +4. store/src/task-utils.ts — Remove redundant functions +5. store/src/storeEventsWrapper.ts — Add TASK_UI_CONTROLS_UPDATED, simplify handlers +6. task/src/Utils/timer-utils.ts — Accept TaskUIControls instead of ControlVisibility +7. task/src/Utils/task-util.ts — DELETE or reduce to findHoldTimestamp only +8. task/src/helper.ts — Replace getControlsVisibility() with task.uiControls +9. call-control.utils.ts — Update buildCallControlButtons() to new control names +10. call-control-custom.utils.ts — Update createConsultButtons() to new control names +11. call-control.tsx — Update to accept controls: TaskUIControls +12. call-control-consult.tsx — Update consult component props +13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation +13b. call-control-cad.tsx — Replace all controlVisibility refs (isHeld, isConferenceInProgress, recordingIndicator, wrapup, isConsultInitiatedOrAccepted, isConsultReceived, consultCallHeld) +13c. wc.ts — Update commonPropsForCallControl to align with TaskUIControls shape +13d. task-list.utils.ts — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic +13e. incoming-task.utils.tsx — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic +14. CallControl/index.tsx — Remove old props from useCallControl call +15. CallControlCAD/index.tsx — Remove old props from useCallControl call +16. TaskList/index.tsx — Remove deviceType usage +17. IncomingTask/index.tsx — Remove deviceType, migrate to task.uiControls +18. All test files — Update mocks and assertions +``` + +--- + +_Created: 2026-03-09_ +_Parent: [001-migration-overview.md](./001-migration-overview.md)_ From 021b370718c8d8e05f438bf8c381f17d9ffd0770 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 20:22:53 +0530 Subject: [PATCH 02/15] docs(ai-docs): move migration docs to packages/contact-center scope Move planning & reference migration docs to packages/contact-center/ai-docs/migration/. Made-with: Cursor --- .../contact-center/ai-docs}/migration/011-execution-plan.md | 0 .../ai-docs}/migration/012-task-lifecycle-flows-old-vs-new.md | 0 .../migration/013-file-inventory-old-control-references.md | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {ai-docs => packages/contact-center/ai-docs}/migration/011-execution-plan.md (100%) rename {ai-docs => packages/contact-center/ai-docs}/migration/012-task-lifecycle-flows-old-vs-new.md (100%) rename {ai-docs => packages/contact-center/ai-docs}/migration/013-file-inventory-old-control-references.md (100%) diff --git a/ai-docs/migration/011-execution-plan.md b/packages/contact-center/ai-docs/migration/011-execution-plan.md similarity index 100% rename from ai-docs/migration/011-execution-plan.md rename to packages/contact-center/ai-docs/migration/011-execution-plan.md diff --git a/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md similarity index 100% rename from ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md rename to packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md diff --git a/ai-docs/migration/013-file-inventory-old-control-references.md b/packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md similarity index 100% rename from ai-docs/migration/013-file-inventory-old-control-references.md rename to packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md From 05808a1f8ffb1ca4d55a2cf77167d0243e97b90a Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 11 Mar 2026 20:35:54 +0530 Subject: [PATCH 03/15] docs: remove findHoldTimestamp consolidation from execution plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not a task-refactor change — keep migration docs scoped to SDK state machine migration only. Made-with: Cursor --- .../contact-center/ai-docs/migration/011-execution-plan.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/011-execution-plan.md b/packages/contact-center/ai-docs/migration/011-execution-plan.md index 9588b141c..437852a38 100644 --- a/packages/contact-center/ai-docs/migration/011-execution-plan.md +++ b/packages/contact-center/ai-docs/migration/011-execution-plan.md @@ -166,9 +166,6 @@ describe('calculateStateTimerData with TaskUIControls', () => { 4. Update `calculateConsultTimerData(task, controls, agentId)` similarly 5. Update all test cases -**Also fix during this milestone:** -- Consolidate `findHoldTimestamp` dual signatures (store vs task-util versions) - --- ### M4: CallControl Hook Refactor (3-5 days) — CRITICAL PATH @@ -346,7 +343,6 @@ describe('useIncomingTask with uiControls', () => { 2. **Bug fixes (found during analysis):** - Fix recording callback cleanup mismatch (`TASK_RECORDING_PAUSED` vs `CONTACT_RECORDING_PAUSED`) - - Consolidate `findHoldTimestamp` dual signatures (store vs task-util) - Add `task:wrapup` race guard if needed 3. **Dead code removal:** From a2d7be4a2d823bd62484803a2d8783547b0188c6 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Thu, 12 Mar 2026 13:28:29 +0530 Subject: [PATCH 04/15] =?UTF-8?q?docs(ai-docs):=20repurpose=20PR=204=20?= =?UTF-8?q?=E2=80=94=20replace=20planning=20docs=20with=20task-list=20and?= =?UTF-8?q?=20component-layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed 011-execution-plan, 012-task-lifecycle-flows, 013-file-inventory. Added task-list-migration.md and component-layer-migration.md (moved from PR 3). Added ControlVisibility deletion details to component doc. Made-with: Cursor --- .../ai-docs/migration/011-execution-plan.md | 434 ------------ .../012-task-lifecycle-flows-old-vs-new.md | 664 ------------------ ...3-file-inventory-old-control-references.md | 171 ----- .../migration/component-layer-migration.md | 408 +++++++++++ .../ai-docs/migration/task-list-migration.md | 148 ++++ 5 files changed, 556 insertions(+), 1269 deletions(-) delete mode 100644 packages/contact-center/ai-docs/migration/011-execution-plan.md delete mode 100644 packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md delete mode 100644 packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md create mode 100644 packages/contact-center/ai-docs/migration/component-layer-migration.md create mode 100644 packages/contact-center/ai-docs/migration/task-list-migration.md diff --git a/packages/contact-center/ai-docs/migration/011-execution-plan.md b/packages/contact-center/ai-docs/migration/011-execution-plan.md deleted file mode 100644 index 437852a38..000000000 --- a/packages/contact-center/ai-docs/migration/011-execution-plan.md +++ /dev/null @@ -1,434 +0,0 @@ -# Migration Doc 011: Execution Plan (Spec-First) - -## Overview - -This plan uses the **spec-driven development** approach already established in CC Widgets. Each milestone starts with writing specs (tests), then implementing to make specs pass. This ensures parity between old and new behavior. - ---- - -## Prerequisites - -- [ ] CC SDK `task-refactor` branch merged and released (or linked locally) -- [ ] `TaskUIControls` type and `task:ui-controls-updated` event available in `@webex/contact-center` -- [ ] `task.uiControls` getter available on `ITask` -- [ ] Team alignment on migration approach - ---- - -## Milestone Overview - -| # | Milestone | Scope | Est. Effort | Risk | Depends On | -|---|-----------|-------|-------------|------|------------| -| M0 | SDK integration setup | Link SDK, verify types | 1 day | Low | SDK release | -| M1 | Types & constants alignment | Import new types, add adapters | 1-2 days | Low | M0 | -| M2 | Store event wiring simplification | Simplify event handlers, add `ui-controls-updated` | 2-3 days | Medium | M0 | -| M3 | Store task-utils thinning | Remove redundant utils | 1-2 days | Low | M2 | -| M3.5 | Timer utils migration | Update timer-utils to accept `TaskUIControls` | 1 day | Low | M3 | -| M4 | CallControl hook refactor | Core: replace `getControlsVisibility` with `task.uiControls` | 3-5 days | **High** | M1, M2, M3, M3.5 | -| M5 | Component layer update | Update `cc-components` prop interfaces | 2-3 days | Medium | M4 | -| M6 | IncomingTask migration | Use `task.uiControls.accept/decline` | 1 day | Low | M1 | -| M7 | TaskList migration | Optional status enhancement | 1 day | Low | M1 | -| M8 | Integration testing & cleanup | E2E, remove dead code, docs | 2-3 days | Medium | All | - -**Total estimated effort: 15–23 days** - ---- - -## Detailed Milestone Plans - -### M0: SDK Integration Setup (1 day) - -**Goal:** Verify the new SDK API is available and types compile. - -**Steps:** -1. Update `@webex/contact-center` dependency to task-refactor version -2. Verify `TaskUIControls` type is importable -3. Verify `task.uiControls` getter exists on `ITask` -4. Verify `TASK_EVENTS.TASK_UI_CONTROLS_UPDATED` constant exists -5. Run `yarn build` to confirm no type errors - -**Spec:** Write a minimal integration test that creates a mock task and reads `uiControls`. - -**Validation:** `yarn build` passes with new SDK version. - ---- - -### M1: Types & Constants Alignment (1-2 days) - -**Ref:** [009-types-and-constants-migration.md](./009-types-and-constants-migration.md) - -**Goal:** Import new SDK types into widget packages without changing runtime behavior. - -**Spec first:** -```typescript -// Test: TaskUIControls type compatibility -import { TaskUIControls } from '@webex/contact-center'; -const controls: TaskUIControls = task.uiControls; -expect(controls.hold).toHaveProperty('isVisible'); -expect(controls.hold).toHaveProperty('isEnabled'); -``` - -**Steps:** -1. Add `TaskUIControls` import to `task/src/task.types.ts` -2. Create adapter type mapping old control names → new (for gradual migration) -3. Add `TASK_UI_CONTROLS_UPDATED` to store event constants -4. Review and annotate constants for deprecation - -**Validation:** All existing tests still pass. New types compile. - ---- - -### M2: Store Event Wiring Simplification (2-3 days) - -**Ref:** [003-store-event-wiring-migration.md](./003-store-event-wiring-migration.md) - -**Goal:** Simplify store event handlers; add `task:ui-controls-updated` subscription. - -**Spec first:** -```typescript -// Test: Store registers ui-controls-updated listener -describe('registerTaskEventListeners', () => { - it('should register TASK_UI_CONTROLS_UPDATED handler', () => { - store.registerTaskEventListeners(mockTask); - expect(mockTask.on).toHaveBeenCalledWith( - TASK_EVENTS.TASK_UI_CONTROLS_UPDATED, expect.any(Function) - ); - }); - - it('should NOT call refreshTaskList on TASK_HOLD', () => { - // Verify simplified handler - }); -}); -``` - -**Steps:** -1. Add `TASK_UI_CONTROLS_UPDATED` handler in `registerTaskEventListeners()` -2. Replace `refreshTaskList()` calls with callback-only for: TASK_HOLD, TASK_RESUME, TASK_CONSULT_END, all conference events -3. Keep `refreshTaskList()` only for: initialization, hydration -4. Update tests for each modified handler - -**Order (low risk → high risk):** -1. Add new `TASK_UI_CONTROLS_UPDATED` handler (additive, no breakage) -2. Simplify conference event handlers (less critical) -3. Simplify hold/resume handlers (medium impact) -4. Simplify consult handlers (medium impact) -5. Remove unnecessary `refreshTaskList()` calls (highest impact) - -**Validation:** All existing widget tests pass. Store correctly fires callbacks on events. - ---- - -### M3: Store Task-Utils Thinning (1-2 days) - -**Ref:** [008-store-task-utils-migration.md](./008-store-task-utils-migration.md) - -**Goal:** Remove utility functions that are now handled by SDK. - -**Spec first:** -```typescript -// Test: Verify no consumers remain for removed functions -// (Static analysis — ensure no import of getConsultStatus, getIsConferenceInProgress, etc.) -``` - -**Steps:** -1. Search codebase for each function to verify consumers -2. Remove functions with zero consumers after M2 changes -3. Mark functions with remaining consumers for later removal (after M4/M5) -4. Keep display-only functions (`getTaskStatus`, `getConferenceParticipants`, etc.) - -**Validation:** Build succeeds. No runtime errors. - ---- - -### M3.5: Timer Utils Migration (1 day) - -**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md#timer-utils-migration) - -**Goal:** Update `calculateStateTimerData()` and `calculateConsultTimerData()` to accept `TaskUIControls`. - -**Why:** These functions accept `controlVisibility` (old shape) as a parameter and derive timer labels from it. They must be migrated before M4 since `useCallControl` depends on them. - -**Spec first:** -```typescript -describe('calculateStateTimerData with TaskUIControls', () => { - it('should return Wrap Up label when controls.wrapup.isVisible', () => { - const controls = { ...getDefaultUIControls(), wrapup: { isVisible: true, isEnabled: true } }; - const result = calculateStateTimerData(mockTask, controls, agentId); - expect(result.label).toBe('Wrap Up'); - }); -}); -``` - -**Steps:** -1. Update `calculateStateTimerData(task, controls, agentId)` signature -2. Replace `controlVisibility.isConsultInitiatedOrAccepted` → `controls.endConsult.isVisible` -3. Replace `controlVisibility.isHeld` → derive from task data via `findHoldStatus(task, 'mainCall', agentId)` (do NOT derive from `controls.hold.isEnabled` — hold can be disabled in consult/transition states even when call is not held) -4. Update `calculateConsultTimerData(task, controls, agentId)` similarly -5. Update all test cases - ---- - -### M4: CallControl Hook Refactor (3-5 days) — CRITICAL PATH - -**Ref:** [004-call-control-hook-migration.md](./004-call-control-hook-migration.md) - -**Goal:** Replace `getControlsVisibility()` with `task.uiControls` in `useCallControl`. - -**Spec first (write ALL specs before implementation):** - -```typescript -describe('useCallControl with task.uiControls', () => { - // Parity specs: each scenario must produce identical control states - - describe('connected voice call', () => { - it('should show hold, mute, end, transfer, consult controls', () => { - mockTask.uiControls = { - hold: { isVisible: true, isEnabled: true }, - mute: { isVisible: true, isEnabled: true }, - end: { isVisible: true, isEnabled: true }, - transfer: { isVisible: true, isEnabled: true }, - consult: { isVisible: true, isEnabled: true }, - // ... all other controls disabled - }; - const { result } = renderHook(() => useCallControl(props)); - expect(result.current.controls.hold).toEqual({ isVisible: true, isEnabled: true }); - }); - }); - - describe('held voice call', () => { - it('should show hold (enabled=true for resume), disable end/mute', () => { /* ... */ }); - }); - - describe('consulting', () => { - it('should show endConsult, switchToMainCall, switchToConsult, mergeToConference', () => { /* ... */ }); - }); - - describe('conferencing', () => { - it('should show exitConference, disable hold', () => { /* ... */ }); - }); - - describe('wrapping up', () => { - it('should show only wrapup control', () => { /* ... */ }); - }); - - describe('digital channel', () => { - it('should show only accept, end, transfer, wrapup', () => { /* ... */ }); - }); - - describe('ui-controls-updated event', () => { - it('should re-render when task emits ui-controls-updated', () => { /* ... */ }); - }); - - describe('no task', () => { - it('should return default controls when no task', () => { /* ... */ }); - }); -}); -``` - -**Steps:** -1. Write comprehensive parity specs (30+ test cases covering all states) -2. Create `adaptSDKControls()` adapter function (maps SDK names to old names if needed during transition) -3. Replace `getControlsVisibility()` call in `useCallControl` with `task.uiControls` -4. Add `task:ui-controls-updated` subscription with `useEffect` -5. Update hook return type to use new control names -6. Remove old state flags from return -7. Run parity specs — fix any mismatches - -**Parity verification approach:** -- For each state (connected, held, consulting, conferencing, wrapping-up, offered): - - Mock task with known data - - Call old `getControlsVisibility()` → capture result - - Read `task.uiControls` → capture result - - Compare: every control must have same `isVisible`/`isEnabled` - - Document and resolve any differences (old bug vs new behavior) - -**Validation:** All 30+ parity specs pass. All existing hook tests pass (with updated assertions). - ---- - -### M5: Component Layer Update (2-3 days) - -**Ref:** [010-component-layer-migration.md](./010-component-layer-migration.md) - -**Goal:** Update `cc-components` to accept new control prop shape. - -**Spec first:** -```typescript -describe('CallControlComponent', () => { - it('should render hold button from controls.hold', () => { - render(); - expect(screen.getByTestId('hold-button')).toBeVisible(); - }); - - it('should hide mute button when controls.mute.isVisible=false', () => { - const controls = { ...mockControls, mute: { isVisible: false, isEnabled: false } }; - render(); - expect(screen.queryByTestId('mute-button')).not.toBeInTheDocument(); - }); -}); -``` - -**Steps:** -1. Update `CallControlComponentProps` to accept `controls: TaskUIControls` -2. Update `CallControlComponent` to read from `controls.*` -3. Update `CallControlConsult` component -4. Update `IncomingTaskComponent` if needed -5. Update all component tests - -**Validation:** All component tests pass. Visual output identical. - ---- - -### M6: IncomingTask Migration (1 day) - -**Ref:** [005-incoming-task-migration.md](./005-incoming-task-migration.md) - -**Goal:** Use `task.uiControls.accept/decline` in IncomingTask. - -**Spec first:** -```typescript -describe('useIncomingTask with uiControls', () => { - it('should derive accept visibility from task.uiControls.accept', () => { /* ... */ }); - it('should derive decline visibility from task.uiControls.decline', () => { /* ... */ }); -}); -``` - -**Steps:** -1. Replace `getAcceptButtonVisibility()` / `getDeclineButtonVisibility()` with `task.uiControls` -2. Update component props -3. Update tests - -**Validation:** IncomingTask tests pass. Accept/decline work for voice and digital. - ---- - -### M7: TaskList Migration (1 day) - -**Ref:** [006-task-list-migration.md](./006-task-list-migration.md) - -**Goal:** Optionally enhance task status display; verify compatibility. - -**Steps:** -1. Verify `useTaskList` works with new SDK (should be compatible) -2. Optionally enhance `getTaskStatus()` to use SDK state info -3. Update tests if any changes made - -**Validation:** TaskList renders correctly with all task states. - ---- - -### M8: Integration Testing & Cleanup (2-3 days) - -**Goal:** End-to-end verification, dead code removal, documentation update. - -**Steps:** - -1. **E2E Test Matrix:** - -| Scenario | Widgets Involved | Verify | -|----------|-----------------|--------| -| Incoming voice call → accept → end | IncomingTask, CallControl | Accept button, end button | -| Incoming voice call → reject | IncomingTask | Decline button, RONA timer | -| Connected → hold → resume | CallControl | Hold toggle, timer | -| Connected → consult → end consult | CallControl | Consult flow controls | -| Connected → consult → conference | CallControl | Merge, conference controls | -| Conference → exit | CallControl | Exit conference | -| Conference → transfer conference | CallControl | Transfer conference | -| Connected → transfer (blind) | CallControl | Transfer popover | -| Connected → end → wrapup | CallControl | Wrapup button | -| Outdial → connected → end | OutdialCall, CallControl | Full outdial flow | -| Digital task → accept → end → wrapup | IncomingTask, CallControl | Digital controls | -| Multiple tasks in list | TaskList | Task selection, per-task controls | -| Page refresh → hydrate | All | Restore state correctly | - -2. **Bug fixes (found during analysis):** - - Fix recording callback cleanup mismatch (`TASK_RECORDING_PAUSED` vs `CONTACT_RECORDING_PAUSED`) - - Add `task:wrapup` race guard if needed - -3. **Dead code removal:** - - Delete `task/src/Utils/task-util.ts` (or reduce to `findHoldTimestamp` only) - - Remove unused store utils - - Remove unused constants - - Remove unused type definitions - -4. **Documentation updates:** - - Update `task/ai-docs/widgets/CallControl/AGENTS.md` and `ARCHITECTURE.md` - - Update `task/ai-docs/widgets/IncomingTask/AGENTS.md` and `ARCHITECTURE.md` - - Update `store/ai-docs/AGENTS.md` and `ARCHITECTURE.md` - - Update `cc-components/ai-docs/AGENTS.md` - -5. **Final validation:** - - `yarn build` — no errors - - `yarn test:unit` — all pass - - `yarn test:styles` — no lint errors - - Sample apps (React + WC) work correctly - ---- - -## Risk Mitigation - -### High-Risk Areas -1. **CallControl hook refactor (M4)** — largest change, most complex logic - - **Mitigation:** Comprehensive parity specs written BEFORE implementation - - **Rollback:** Old `getControlsVisibility()` stays in codebase until M8 cleanup - -2. **Store event wiring (M2)** — removing `refreshTaskList()` could cause stale data - - **Mitigation:** Gradual removal; keep `refreshTaskList()` as fallback initially - - **Rollback:** Re-add `refreshTaskList()` calls if data staleness detected - -3. **Consult/Conference flows** — most complex state transitions - - **Mitigation:** Dedicated parity specs for every consult/conference scenario - - **Mitigation:** Test with Agent Desktop to verify identical behavior - -### Low-Risk Areas -- OutdialCall (no changes needed) -- IncomingTask (minimal changes) -- TaskList (minimal changes) -- Types alignment (additive, no runtime changes) - ---- - -## Spec-First Checklist - -For each milestone, complete these in order: - -1. [ ] Write spec file with test cases for NEW behavior -2. [ ] Write parity tests (old behavior == new behavior) where applicable -3. [ ] Run specs — verify they FAIL (red) -4. [ ] Implement changes -5. [ ] Run specs — verify they PASS (green) -6. [ ] Run ALL existing tests — verify no regressions -7. [ ] `yarn build` — verify compilation -8. [ ] Code review with team -9. [ ] Mark milestone complete - ---- - -## Recommended Order of Execution - -``` -M0 (SDK setup) - │ - ├── M1 (types) ─┐ - └── M2 (store events) ─┤ - │ - M3 (store utils) - │ - M3.5 (timer utils) - │ - M4 (CallControl hook) ← CRITICAL PATH - │ - M5 (components) - │ - ├── M6 (IncomingTask) - └── M7 (TaskList) - │ - M8 (integration + cleanup + bug fixes) -``` - -**M0 → M1 + M2 (parallel) → M3 → M3.5 → M4 → M5 → M6 + M7 (parallel) → M8** - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md b/packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md deleted file mode 100644 index 0337952a6..000000000 --- a/packages/contact-center/ai-docs/migration/012-task-lifecycle-flows-old-vs-new.md +++ /dev/null @@ -1,664 +0,0 @@ -# Migration Doc 012: Task Lifecycle Flows — Complete Old vs New - -## Purpose - -This document traces **every task scenario from start to finish**, showing exactly what happens at each step in both the old and new approach. Each flow maps: -- User/system action -- SDK event chain -- Widget/store layer behavior -- UI controls shown -- State machine state (new only) - ---- - -## Flow 1: Incoming Voice Call → Accept → Connected - -### Old Flow -``` -1. WebSocket: AgentContactReserved -2. SDK emits: task:incoming (with ITask) -3. Store: handleIncomingTask() → refreshTaskList() → cc.taskManager.getAllTasks() - → runInAction: store.taskList updated, store.incomingTask set -4. Widget: IncomingTask (observer) re-renders -5. Hook: useIncomingTask registers callbacks (TASK_ASSIGNED, TASK_REJECT, etc.) -6. UI Controls: getAcceptButtonVisibility(isBrowser, isPhoneDevice, webRtcEnabled, isCall, isDigitalChannel) - getDeclineButtonVisibility(isBrowser, webRtcEnabled, isCall) -7. User clicks Accept -8. Hook: incomingTask.accept() → SDK API call -9. WebSocket: AgentContactAssigned -10. SDK emits: task:assigned -11. Store: handleTaskAssigned() → refreshTaskList() → update taskList, set currentTask -12. Hook: TASK_ASSIGNED callback fires → onAccepted({task}) -13. Widget: CallControl appears -14. UI Controls: getControlsVisibility() computes all 22 controls from raw task data - → hold, mute, end, transfer, consult visible and enabled -``` - -### New Flow -``` -1. WebSocket: AgentContactReserved -2. SDK: TaskManager maps to TaskEvent.TASK_INCOMING -3. SDK: task.sendStateMachineEvent(TASK_INCOMING) → State: IDLE → OFFERED -4. SDK: computeUIControls(OFFERED, context) → accept/decline visible (WebRTC) -5. SDK emits: task:incoming, task:ui-controls-updated -6. Store: handleIncomingTask() → store.incomingTask set -7. Widget: IncomingTask (observer) re-renders -8. Hook: useIncomingTask reads task.uiControls.accept / task.uiControls.decline -9. User clicks Accept -10. Hook: incomingTask.accept() → SDK API call -11. WebSocket: AgentContactAssigned -12. SDK: TaskManager maps to TaskEvent.ASSIGN -13. SDK: task.sendStateMachineEvent(ASSIGN) → State: OFFERED → CONNECTED -14. SDK: computeUIControls(CONNECTED, context) → hold, mute, end, transfer, consult -15. SDK emits: task:assigned, task:ui-controls-updated -16. Store: handleTaskAssigned() → set currentTask -17. Widget: CallControl appears -18. Hook: useCallControl reads task.uiControls directly (no computation) -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Controls computation | Widget runs `getControlsVisibility()` on every render | SDK pre-computes `task.uiControls` on every state transition | -| Data freshness | `refreshTaskList()` re-fetches all tasks | SDK updates `task.data` in state machine action | -| Re-render trigger | MobX observable change after `refreshTaskList()` | `task:ui-controls-updated` event | - ---- - -## Flow 2: Incoming Voice Call → Reject / RONA (Timeout) - -### Old Flow -``` -1-6. Same as Flow 1 (incoming → show accept/decline) -7. User clicks Decline (or timer expires → auto-reject) -8. Hook: incomingTask.decline() → SDK API call -9. WebSocket: AgentContactReservedTimeout (RONA) or rejection -10. SDK emits: task:rejected -11. Store: handleTaskReject() → refreshTaskList() → remove task from list -12. Hook: TASK_REJECT callback fires → onRejected({task}) -13. Widget: IncomingTask unmounts -``` - -### New Flow -``` -1-8. Same as Flow 1 new (incoming → OFFERED state) -7. User clicks Decline (or timer expires → auto-reject) -8. Hook: incomingTask.decline() → SDK API call -9. WebSocket: RONA or rejection -10. SDK: task.sendStateMachineEvent(RONA) → State: OFFERED → TERMINATED -11. SDK: computeUIControls(TERMINATED) → all controls disabled -12. SDK emits: task:rejected, task:ui-controls-updated -13. Store: handleTaskReject() → remove task from list -14. Hook: TASK_REJECT callback fires → onRejected({task}) -15. Widget: IncomingTask unmounts -``` - ---- - -## Flow 3: Connected → Hold → Resume - -### Old Flow -``` -1. User clicks Hold -2. Hook: toggleHold(true) → currentTask.hold() -3. SDK: API call to backend -4. WebSocket: AgentContactHeld -5. SDK emits: task:hold -6. Store: refreshTaskList() → cc.taskManager.getAllTasks() → update store.taskList -7. Hook: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) -8. UI Controls: getControlsVisibility() recalculates: - → findHoldStatus(task, 'mainCall', agentId) returns true - → holdResume: { isVisible: true, isEnabled: true } (for resume) - → end: { isVisible: true, isEnabled: false } (disabled while held) - → mute: same -9. User clicks Resume -10. Hook: toggleHold(false) → currentTask.resume() -11. WebSocket: AgentContactUnheld -12. SDK emits: task:resume -13. Store: refreshTaskList() → update store.taskList -14. Hook: TASK_RESUME callback fires → onHoldResume({ isHeld: false }) -15. UI Controls: getControlsVisibility() recalculates → controls back to connected state -``` - -### New Flow -``` -1. User clicks Hold -2. Hook: toggleHold(true) → currentTask.hold() -3. SDK: sends TaskEvent.HOLD_INITIATED → State: CONNECTED → HOLD_INITIATING -4. SDK: computeUIControls(HOLD_INITIATING) → hold visible but transitioning -5. SDK emits: task:ui-controls-updated (optimistic) -6. SDK: API call to backend -7. WebSocket: AgentContactHeld -8. SDK: sends TaskEvent.HOLD_SUCCESS → State: HOLD_INITIATING → HELD -9. SDK: computeUIControls(HELD) → hold visible (for resume), end/mute disabled -10. SDK emits: task:hold, task:ui-controls-updated -11. Store: TASK_HOLD callback fires → onHoldResume({ isHeld: true }) -12. Hook: controls state updated via task:ui-controls-updated listener -13. User clicks Resume -14. Hook: toggleHold(false) → currentTask.resume() -15. SDK: sends TaskEvent.UNHOLD_INITIATED → State: HELD → RESUME_INITIATING -16. SDK: API call to backend -17. WebSocket: AgentContactUnheld -18. SDK: sends TaskEvent.UNHOLD_SUCCESS → State: RESUME_INITIATING → CONNECTED -19. SDK: computeUIControls(CONNECTED) → all active controls enabled -20. SDK emits: task:resume, task:ui-controls-updated -21. Store: TASK_RESUME callback fires -22. Hook: controls state updated -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Hold initiation | Immediate API call, wait for response | Optimistic: HOLD_INITIATING state before API call | -| Intermediate states | None (binary: held or not) | HOLD_INITIATING, RESUME_INITIATING (UI can show spinner) | -| Controls update | After `refreshTaskList()` + `getControlsVisibility()` | After each state transition via `task:ui-controls-updated` | - ---- - -## Flow 4: Connected → Consult → End Consult - -### Old Flow -``` -1. User initiates consult -2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) -3. SDK: API call to backend -4. WebSocket: AgentConsultCreated -5. SDK emits: task:consultCreated -6. Store: handleConsultCreated() → refreshTaskList() → update taskList -7. UI Controls: getControlsVisibility() recalculates: - → getConsultStatus() returns CONSULT_INITIATED - → endConsult visible, consultTransfer visible, switchToMainCall visible - → hold disabled, transfer hidden -8. WebSocket: AgentConsulting (consult agent answered) -9. SDK emits: task:consulting -10. Store: handleConsulting() → refreshTaskList() -11. UI Controls: getControlsVisibility() recalculates: - → getConsultStatus() returns CONSULT_ACCEPTED - → mergeConference enabled, consultTransfer enabled - → switchToMainCall/switchToConsult available -12. User clicks End Consult -13. Hook: endConsultCall() → currentTask.endConsult(payload) -14. WebSocket: AgentConsultEnded -15. SDK emits: task:consultEnd -16. Store: refreshTaskList() -17. UI Controls: getControlsVisibility() → back to connected state controls -``` - -### New Flow -``` -1. User initiates consult -2. Hook: consultCall(destination, type, allowInteract) → currentTask.consult(payload) -3. SDK: sends TaskEvent.CONSULT → State: CONNECTED → CONSULT_INITIATING -4. SDK: computeUIControls(CONSULT_INITIATING) → consult controls transitioning -5. SDK emits: task:ui-controls-updated -6. SDK: API call → success -7. SDK: sends TaskEvent.CONSULT_SUCCESS → stays CONSULT_INITIATING (waiting for agent) -8. WebSocket: AgentConsultCreated → TaskEvent.CONSULT_CREATED -9. SDK: task data updated -10. WebSocket: AgentConsulting → TaskEvent.CONSULTING_ACTIVE -11. SDK: State: CONSULT_INITIATING → CONSULTING -12. SDK: context.consultDestinationAgentJoined = true -13. SDK: computeUIControls(CONSULTING): - → endConsult visible+enabled, mergeToConference visible+enabled - → switchToMainCall visible, switchToConsult visible - → transfer visible (for consult transfer) - → hold disabled (in consult) -14. SDK emits: task:consulting, task:ui-controls-updated -15. Hook: controls updated via listener -16. User clicks End Consult -17. Hook: endConsultCall() → currentTask.endConsult(payload) -18. WebSocket: AgentConsultEnded → TaskEvent.CONSULT_END -19. SDK: State: CONSULTING → CONNECTED (or CONFERENCING if from conference) -20. SDK: context cleared (consultInitiator=false, consultDestinationAgentJoined=false) -21. SDK: computeUIControls(CONNECTED) → back to normal connected controls -22. SDK emits: task:consultEnd, task:ui-controls-updated -23. Hook: controls updated -``` - -### Key Difference -| Step | Old | New | -|------|-----|-----| -| Consult state tracking | `getConsultStatus()` inspects participants | State machine: CONSULT_INITIATING → CONSULTING | -| Agent joined detection | `ConsultStatus.CONSULT_ACCEPTED` from participant flags | `context.consultDestinationAgentJoined` set by action | -| Controls | Computed from raw data every render | Pre-computed on each state transition | - ---- - -## Flow 5: Consulting → Merge to Conference → Exit Conference - -### Old Flow -``` -1. In consulting state (Flow 4 steps 1-11) -2. User clicks Merge Conference -3. Hook: consultConference() → currentTask.consultConference() -4. SDK: API call -5. WebSocket: AgentConsultConferenced / ParticipantJoinedConference -6. SDK emits: task:conferenceStarted / task:participantJoined -7. Store: handleConferenceStarted() → refreshTaskList() -8. UI Controls: getControlsVisibility(): - → task.data.isConferenceInProgress = true - → exitConference visible+enabled - → consult visible+enabled (can add more agents) - → hold disabled (in conference) - → mergeConference hidden (already in conference) -9. User clicks Exit Conference -10. Hook: exitConference() → currentTask.exitConference() -11. WebSocket: ParticipantLeftConference / AgentConsultConferenceEnded -12. SDK emits: task:conferenceEnded / task:participantLeft -13. Store: handleConferenceEnded() → refreshTaskList() -14. UI Controls: getControlsVisibility() → may go to wrapup or connected -``` - -### New Flow -``` -1. In CONSULTING state (Flow 4 new steps 1-15) -2. User clicks Merge Conference -3. Hook: consultConference() → currentTask.consultConference() -4. SDK: sends TaskEvent.MERGE_TO_CONFERENCE → State: CONSULTING → CONF_INITIATING -5. SDK: computeUIControls(CONF_INITIATING) → transitioning controls -6. SDK emits: task:ui-controls-updated -7. WebSocket: AgentConsultConferenced → TaskEvent.CONFERENCE_START -8. SDK: State: CONF_INITIATING → CONFERENCING -9. SDK: computeUIControls(CONFERENCING): - → exitConference visible+enabled - → consult visible (can add more) - → hold disabled - → mergeToConference hidden -10. SDK emits: task:conferenceStarted, task:ui-controls-updated -11. User clicks Exit Conference -12. Hook: exitConference() → currentTask.exitConference() -13. WebSocket: ParticipantLeftConference → TaskEvent.PARTICIPANT_LEAVE -14. SDK: guards check: didCurrentAgentLeaveConference? shouldWrapUp? -15. SDK: State: CONFERENCING → WRAPPING_UP (if wrapup required) or CONNECTED -16. SDK: computeUIControls for new state -17. SDK emits: task:conferenceEnded, task:ui-controls-updated -``` - ---- - -## Flow 6: Connected → End → Wrapup → Complete - -### Old Flow -``` -1. User clicks End -2. Hook: endCall() → currentTask.end() -3. SDK: API call -4. WebSocket: ContactEnded + AgentWrapup -5. SDK emits: task:end, task:wrapup -6. Store: handleTaskEnd() may refresh; handleWrapup → refreshTaskList() -7. UI Controls: getControlsVisibility(): - → getWrapupButtonVisibility(task) → { isVisible: task.data.wrapUpRequired } - → all other controls hidden/disabled -8. Widget: Wrapup form appears -9. User selects reason, clicks Submit -10. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) -11. SDK: API call -12. WebSocket: AgentWrappedup -13. SDK emits: task:wrappedup -14. Store: refreshTaskList() → task removed or updated -15. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) -16. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) -``` - -### New Flow -``` -1. User clicks End -2. Hook: endCall() → currentTask.end() -3. SDK: API call -4. WebSocket: ContactEnded → TaskEvent.CONTACT_ENDED -5. SDK: guards: shouldWrapUp? → State: CONNECTED → WRAPPING_UP -6. SDK: computeUIControls(WRAPPING_UP) → only wrapup visible+enabled -7. SDK emits: task:end, task:wrapup, task:ui-controls-updated -8. Hook: controls updated → only wrapup control shown -9. Widget: Wrapup form appears -10. User selects reason, clicks Submit -11. Hook: wrapupCall(reason, auxCodeId) → currentTask.wrapup({wrapUpReason, auxCodeId}) -12. SDK: API call -13. WebSocket: AgentWrappedup → TaskEvent.WRAPUP_COMPLETE -14. SDK: State: WRAPPING_UP → COMPLETED -15. SDK: computeUIControls(COMPLETED) → all controls disabled -16. SDK: emitTaskWrappedup action → cleanupResources action -17. SDK emits: AGENT_WRAPPEDUP (AgentWrappedUp), task:ui-controls-updated, task:cleanup -18. Store: handleTaskEnd/cleanup → remove task from list -19. Hook: AGENT_WRAPPEDUP callback fires → onWrapUp({task, wrapUpReason}) -20. Post-wrapup: store.setCurrentTask(nextTask), store.setState(ENGAGED) -``` - ---- - -## Flow 7: Auto-Wrapup Timer - -### Old Flow -``` -1. Task enters wrapup state (Flow 6 steps 1-7) -2. Hook: useEffect detects currentTask.autoWrapup && controlVisibility.wrapup -3. Hook: reads currentTask.autoWrapup.getTimeLeftSeconds() -4. Hook: setInterval every 1s → decrements secondsUntilAutoWrapup -5. Widget: AutoWrapupTimer component shows countdown -6. If user clicks Cancel: cancelAutoWrapup() → currentTask.cancelAutoWrapupTimer() -7. If timer reaches 0: SDK auto-submits wrapup with default reason -``` - -### New Flow -``` -1. Task enters WRAPPING_UP state (Flow 6 new steps 1-8) -2. Hook: useEffect detects currentTask.autoWrapup && controls.wrapup.isVisible - (changed: controlVisibility.wrapup → controls.wrapup) -3-7. Same as old — auto-wrapup is a widget-layer timer concern, SDK unchanged -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Timer trigger condition | `controlVisibility.wrapup` (computed by widget) | `controls.wrapup.isVisible` (from SDK) | -| Timer behavior | Unchanged | Unchanged | -| Cancel action | Unchanged | Unchanged | - ---- - -## Flow 8: Recording Pause/Resume - -### Old Flow -``` -1. User clicks Pause Recording -2. Hook: toggleRecording() → currentTask.pauseRecording() -3. SDK: API call -4. WebSocket: ContactRecordingPaused -5. SDK emits: task:recordingPaused -6. Store: fires callback -7. Hook: pauseRecordingCallback() → setIsRecording(false), onRecordingToggle({isRecording: false}) -8. UI Controls: getPauseResumeRecordingButtonVisibility() unchanged (still visible) -9. Widget: Button label changes to "Resume Recording" -``` - -### New Flow -``` -1. User clicks Pause Recording -2. Hook: toggleRecording() → currentTask.pauseRecording() -3. SDK: sends TaskEvent.PAUSE_RECORDING → context.recordingInProgress = false -4. SDK: computeUIControls → recording: { isVisible: true, isEnabled: true } - (visible and enabled = agent can click to resume recording) -5. SDK: API call -6. WebSocket: ContactRecordingPaused -7. SDK emits: task:recordingPaused, task:ui-controls-updated -8. Hook: setIsRecording(false), controls updated -9. Widget: Button label changes to "Resume Recording" -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Recording state tracking | Widget local state (`isRecording`) | SDK context (`recordingInProgress`) + widget local state | -| Recording button visibility | `getPauseResumeRecordingButtonVisibility()` | `controls.recording` from SDK | -| Recording indicator | Separate `recordingIndicator` control | Merged into `recording` control | - ---- - -## Flow 9: Blind Transfer - -### Old Flow -``` -1. User clicks Transfer, selects destination -2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) -3. SDK: API call -4. WebSocket: AgentBlindTransferred -5. SDK emits: (leads to task:end or task:wrapup depending on config) -6. Store: refreshTaskList() -7. UI Controls: getControlsVisibility() → wrapup or all disabled -``` - -### New Flow -``` -1. User clicks Transfer, selects destination -2. Hook: transferCall(to, type) → currentTask.transfer({to, destinationType}) -3. SDK: API call -4. WebSocket: AgentBlindTransferred → TaskEvent.TRANSFER_SUCCESS -5. SDK: guards: shouldWrapUpOrIsInitiator? -6. SDK: State → WRAPPING_UP (if wrapup required) or stays CONNECTED -7. SDK: computeUIControls for new state -8. SDK emits: task:end/task:wrapup, task:ui-controls-updated -``` - ---- - -## Flow 10: Digital Task (Chat/Email) — Accept → End → Wrapup - -### Old Flow -``` -1. WebSocket: AgentContactReserved (mediaType: chat) -2. SDK emits: task:incoming -3. Store: handleIncomingTask() -4. UI Controls: getAcceptButtonVisibility(): - → isBrowser && isDigitalChannel → accept visible - → decline NOT visible (digital channels) -5. User clicks Accept -6. incomingTask.accept() → task:assigned -7. UI Controls: getControlsVisibility(): - → end visible, transfer visible, wrapup hidden - → hold/mute/consult/conference/recording: all hidden (digital) -8. User clicks End -9. currentTask.end() → task:end, task:wrapup -10. UI Controls: wrapup visible (if wrapUpRequired) -``` - -### New Flow -``` -1. WebSocket: AgentContactReserved (mediaType: chat) -2. SDK: State: IDLE → OFFERED, channelType: DIGITAL -3. SDK: computeDigitalUIControls(OFFERED) → accept visible, decline hidden -4. SDK emits: task:incoming, task:ui-controls-updated -5. User clicks Accept -6. incomingTask.accept() → ASSIGN → State: OFFERED → CONNECTED -7. SDK: computeDigitalUIControls(CONNECTED) → end visible, transfer visible - → hold/mute/consult/conference/recording: all disabled (digital) -8. User clicks End -9. currentTask.end() → CONTACT_ENDED → WRAPPING_UP -10. SDK: computeDigitalUIControls(WRAPPING_UP) → wrapup visible -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Channel detection | Widget checks `mediaType === 'telephony'` vs chat/email | SDK checks `channelType: VOICE` vs `DIGITAL` | -| Digital controls | `getControlsVisibility()` with `isCall=false, isDigitalChannel=true` | `computeDigitalUIControls()` — much simpler logic | -| Controls shown | Same end result | Same end result (accept, end, transfer, wrapup only) | - ---- - -## Flow 11: Page Refresh → Hydration - -### Old Flow -``` -1. Agent refreshes browser page -2. SDK reconnects, receives AgentContact for active task -3. SDK emits: task:hydrate -4. Store: handleTaskHydrate() → refreshTaskList() → cc.taskManager.getAllTasks() -5. Store: sets currentTask, taskList from fetched data -6. Widgets: observer re-renders with restored task -7. UI Controls: getControlsVisibility() computes from raw task data - → Must correctly derive: held state, consult state, conference state - → Error-prone: depends on raw interaction data being complete -``` - -### New Flow -``` -1. Agent refreshes browser page -2. SDK reconnects, receives AgentContact for active task -3. SDK: TaskManager sends TaskEvent.HYDRATE with task data -4. SDK: State machine guards determine correct state: - → isInteractionTerminated? → WRAPPING_UP - → isInteractionConsulting? → CONSULTING - → isInteractionHeld? → HELD - → isInteractionConnected? → CONNECTED - → isConferencingByParticipants? → CONFERENCING - → default → IDLE (data update only) -5. SDK: computeUIControls for resolved state → correct controls for restored state -6. SDK emits: task:hydrate, task:ui-controls-updated -7. Store: handleTaskHydrate() → set currentTask, taskList -8. Widgets: observer re-renders; controls from task.uiControls are correct -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| State recovery | `refreshTaskList()` + `getControlsVisibility()` from raw data | State machine guards determine correct state | -| Reliability | Can show wrong controls if interaction data is incomplete | Guards explicitly check each condition; predictable | -| Conference recovery | Depends on `isConferenceInProgress` flag in data | Guard: `isConferencingByParticipants` counts agents | - ---- - -## Flow 12: Outdial → New Task - -### Old Flow -``` -1. User enters number, clicks Dial -2. Hook: startOutdial(destination, origin) → cc.startOutdial(destination, origin) -3. SDK: API call (CC-level, not task-level) -4. Backend creates task, sends AgentContactReserved -5. Flow continues as Flow 1 (Incoming → Accept → Connected) -``` - -### New Flow -``` -Identical — outdial initiation is CC-level. Once the task is created, -the state machine takes over and flows follow Flow 1 new approach. -No changes needed. -``` - ---- - -## Flow 13: Consult Transfer (from Consulting State) - -### Old Flow -``` -1. In consulting state (Flow 4 steps 1-11) -2. User clicks Consult Transfer -3. Hook: consultTransfer() checks currentTask.data.isConferenceInProgress: - → false: currentTask.consultTransfer() - → true: currentTask.transferConference() -4. SDK: API call -5. WebSocket: AgentConsultTransferred / AgentConferenceTransferred -6. SDK emits: task events (leads to end/wrapup) -7. Store: refreshTaskList() -8. UI Controls: wrapup or all disabled -``` - -### New Flow -``` -1. In CONSULTING state (Flow 4 new steps 1-15) -2. User clicks Transfer (transfer control now handles consult transfer) -3. Hook: consultTransfer() checks controls.transferConference.isVisible: - → false: currentTask.consultTransfer() - → true: currentTask.transferConference() -4. SDK: API call -5. WebSocket: → TaskEvent.TRANSFER_SUCCESS or TRANSFER_CONFERENCE_SUCCESS -6. SDK: State → WRAPPING_UP or TERMINATED -7. SDK: computeUIControls for new state -8. SDK emits: events + task:ui-controls-updated -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| Conference check | `currentTask.data.isConferenceInProgress` | `controls.transferConference.isVisible` (or keep data check) | -| Transfer button | Separate `consultTransferConsult` control | `controls.transfer` (consult transfer) / `controls.transferConference` (conference transfer) | - ---- - -## Flow 14: Switch Between Main Call and Consult Call - -### Old Flow -``` -1. In consulting state, agent is on consult leg -2. User clicks "Switch to Main Call" -3. Hook: switchToMainCall(): - → currentTask.resume(findMediaResourceId(task, 'consult')) - (resumes consult media → puts consult on hold → main call active) -4. WebSocket: AgentContactHeld (consult) + AgentContactUnheld (main) -5. SDK emits: task:hold, task:resume -6. Store: refreshTaskList() (twice) -7. UI Controls: getControlsVisibility(): - → consultCallHeld = true → switchToConsult visible - → switchToMainCall hidden -``` - -### New Flow -``` -1. In CONSULTING state, agent is on consult leg -2. User clicks "Switch to Main Call" -3. Hook: switchToMainCall(): - → currentTask.resume(findMediaResourceId(task, 'consult')) -4. SDK: HOLD_INITIATED / UNHOLD_INITIATED → state machine tracks -5. SDK: context.consultCallHeld updated -6. SDK: computeUIControls(CONSULTING, updated context): - → switchToConsult visible (consult is now held) - → switchToMainCall hidden -7. SDK emits: task:hold, task:resume, task:ui-controls-updated -8. Hook: controls updated via listener -``` - -### Key Difference -| Aspect | Old | New | -|--------|-----|-----| -| consultCallHeld tracking | `findHoldStatus(task, 'consult', agentId)` | `context.consultCallHeld` in state machine | -| Controls update | After 2x `refreshTaskList()` | Single `task:ui-controls-updated` after state settles | - ---- - -## State Machine States → Widget Controls Summary - -| TaskState (New) | Old Equivalent | Controls Visible | -|-----------------|---------------|-----------------| -| `IDLE` | No task / before incoming | All disabled | -| `OFFERED` | Incoming task shown | accept, decline (WebRTC voice); accept only (digital) | -| `CONNECTED` | Active call | hold, mute, end, transfer, consult, recording | -| `HOLD_INITIATING` | (no equivalent) | hold visible (transitioning) | -| `HELD` | `isHeld = true` | hold (for resume), transfer, consult | -| `RESUME_INITIATING` | (no equivalent) | hold visible (transitioning) | -| `CONSULT_INITIATING` | `ConsultStatus.CONSULT_INITIATED` | endConsult, switchToMainCall, switchToConsult | -| `CONSULTING` | `ConsultStatus.CONSULT_ACCEPTED` | endConsult, mergeToConference, transfer, switchToMainCall/Consult | -| `CONF_INITIATING` | (no equivalent) | conference transitioning | -| `CONFERENCING` | `isConferenceInProgress = true` | exitConference, consult, transferConference | -| `WRAPPING_UP` | `wrapUpRequired && interaction terminated` | wrapup only | -| `COMPLETED` | Task removed after wrapup | All disabled | -| `TERMINATED` | Task rejected / ended without wrapup | All disabled | - ---- - -## Event Chain Mapping: Old Widget Events → New SDK State Machine - -| Widget Action | Old Event Chain | New Event Chain | -|--------------|----------------|-----------------| -| Accept task | `accept()` → `task:assigned` | `accept()` → `ASSIGN` → `task:assigned` + `task:ui-controls-updated` | -| Decline task | `decline()` → `task:rejected` | `decline()` → `RONA` → `task:rejected` + `task:ui-controls-updated` | -| Hold | `hold()` → `task:hold` | `hold()` → `HOLD_INITIATED` → `HOLD_SUCCESS` → `task:hold` + `task:ui-controls-updated` | -| Resume | `resume()` → `task:resume` | `resume()` → `UNHOLD_INITIATED` → `UNHOLD_SUCCESS` → `task:resume` + `task:ui-controls-updated` | -| End call | `end()` → `task:end` | `end()` → `CONTACT_ENDED` → `task:end` + `task:ui-controls-updated` | -| Wrapup | `wrapup()` → `task:wrappedup` | `wrapup()` → `WRAPUP_COMPLETE` → `AGENT_WRAPPEDUP` (AgentWrappedUp) + `task:ui-controls-updated` | -| Transfer | `transfer()` → `task:end` | `transfer()` → `TRANSFER_SUCCESS` → `task:end` + `task:ui-controls-updated` | -| Start consult | `consult()` → `task:consultCreated` | `consult()` → `CONSULT` → `CONSULT_SUCCESS` → `CONSULTING_ACTIVE` → `task:consulting` + `task:ui-controls-updated` | -| End consult | `endConsult()` → `task:consultEnd` | `endConsult()` → `CONSULT_END` → `task:consultEnd` + `task:ui-controls-updated` | -| Merge conference | `consultConference()` → `task:conferenceStarted` | `consultConference()` → `MERGE_TO_CONFERENCE` → `CONFERENCE_START` → `task:conferenceStarted` + `task:ui-controls-updated` | -| Exit conference | `exitConference()` → `task:conferenceEnded` | `exitConference()` → `PARTICIPANT_LEAVE` / `CONFERENCE_END` → `task:conferenceEnded` + `task:ui-controls-updated` | -| Pause recording | `pauseRecording()` → `task:recordingPaused` | `pauseRecording()` → `PAUSE_RECORDING` → `task:recordingPaused` + `task:ui-controls-updated` | -| Resume recording | `resumeRecording()` → `task:recordingResumed` | `resumeRecording()` → `RESUME_RECORDING` → `task:recordingResumed` + `task:ui-controls-updated` | - -### Universal New Pattern -Every action now follows: **User action → SDK method → State machine event(s) → task.uiControls recomputed → `task:ui-controls-updated` emitted**. Widgets never need to compute controls themselves. - ---- - -## Timer Utils: Old vs New Control References - -**File:** `packages/contact-center/task/src/Utils/timer-utils.ts` - -| Old Reference in Timer Utils | New Equivalent | -|-----------------------------|---------------| -| `controlVisibility.wrapup?.isVisible` | `controls.wrapup.isVisible` | -| `controlVisibility.consultCallHeld` | `controls.switchToConsult.isVisible` | -| `controlVisibility.isConsultInitiated` | `controls.endConsult.isVisible && !controls.mergeToConference.isEnabled` | - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ diff --git a/packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md b/packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md deleted file mode 100644 index 728155129..000000000 --- a/packages/contact-center/ai-docs/migration/013-file-inventory-old-control-references.md +++ /dev/null @@ -1,171 +0,0 @@ -# Migration Doc 013: Complete File Inventory — Old Control References - -## Purpose - -This is the definitive inventory of **every file** in CC Widgets that references old task control names, state flags, or the `ControlVisibility` type. Use this as a checklist during migration to ensure nothing is missed. - ---- - -## Summary - -| Category | Files with Old Refs | Files Unaffected | -|----------|-------------------|-----------------| -| Widget hooks (`task/src/`) | 4 | 1 (`index.ts`) | -| Widget utils (`task/src/Utils/`) | 4 | 0 | -| Widget entry points (`task/src/*/index.tsx`) | 4 | 1 (`OutdialCall`) | -| cc-components types | 1 (central type file) | 0 | -| cc-components utils | 2 | 3+ (unaffected) | -| cc-components components | 4 | 8+ (unaffected) | -| Store | 3 | 1 (`store.ts`) | -| **Total files to modify** | **~25** | **~13 unaffected** | - ---- - -## Files WITH Old Control References (Must Migrate) - -### Tier 1: Core Logic Files (Highest Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 1 | `task/src/Utils/task-util.ts` | `getControlsVisibility()` — the entire function (~650 lines). Computes all 22 controls + 7 state flags. Calls `getConsultStatus`, `findHoldStatus`, `getIsConferenceInProgress`, etc. | [Doc 002](./002-ui-controls-migration.md) | -| 2 | `task/src/helper.ts` (`useCallControl`) | `controlVisibility = useMemo(() => getControlsVisibility(...))`, references `controlVisibility.muteUnmute`, `controlVisibility.wrapup`, passes to `calculateStateTimerData`, `calculateConsultTimerData` | [Doc 004](./004-call-control-hook-migration.md) | -| 3 | `store/src/storeEventsWrapper.ts` | `refreshTaskList()` in 15+ event handlers; missing `TASK_UI_CONTROLS_UPDATED` subscription | [Doc 003](./003-store-event-wiring-migration.md) | -| 4 | `store/src/task-utils.ts` | `getConsultStatus()`, `findHoldStatus()`, `getIsConferenceInProgress()`, `getConferenceParticipantsCount()`, `getIsCustomerInCall()`, `getIsConsultInProgress()` — all used by `getControlsVisibility()` | [Doc 008](./008-store-task-utils-migration.md) | - -### Tier 2: Component Utility Files (High Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 5 | `cc-components/.../task/task.types.ts` | `ControlVisibility` interface (22 controls + 7 flags), `ControlProps.controlVisibility`, `CallControlComponentProps` picks `controlVisibility`, `CallControlConsultComponentsProps.controlVisibility`, `ConsultTransferPopoverComponentProps.isConferenceInProgress`, `ControlProps.isHeld`, `ControlProps.deviceType`, `ControlProps.featureFlags` | [Doc 009](./009-types-and-constants-migration.md), [Doc 010](./010-component-layer-migration.md) | -| 6 | `cc-components/.../call-control.utils.ts` | `buildCallControlButtons()` — 20+ references: `controlVisibility.muteUnmute`, `.holdResume`, `.isHeld`, `.consult`, `.transfer`, `.isConferenceInProgress`, `.consultTransfer`, `.mergeConference`, `.pauseResumeRecording`, `.exitConference`, `.end`, `.switchToConsult`. Also `filterButtonsForConsultation(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | -| 7 | `cc-components/.../call-control-custom.utils.ts` | `createConsultButtons()` — `controlVisibility.muteUnmuteConsult`, `.switchToMainCall`, `.isConferenceInProgress`, `.consultTransferConsult`, `.mergeConferenceConsult`, `.endConsult`. Also `getConsultStatusText(consultInitiated)` | [Doc 010](./010-component-layer-migration.md) | - -### Tier 3: Component Files (Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 8 | `cc-components/.../CallControl/call-control.tsx` | Receives `controlVisibility` as prop, passes to `buildCallControlButtons()`, `createConsultButtons()`, `filterButtonsForConsultation()` | [Doc 010](./010-component-layer-migration.md) | -| 9 | `cc-components/.../CallControlCustom/call-control-consult.tsx` | Receives `controlVisibility` from `CallControlConsultComponentsProps`, passes to `createConsultButtons()` | [Doc 010](./010-component-layer-migration.md) | -| 10 | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Receives `isConferenceInProgress` prop | [Doc 010](./010-component-layer-migration.md) | -| 10b | `cc-components/.../CallControlCAD/call-control-cad.tsx` | Directly references `controlVisibility.isConferenceInProgress`, `controlVisibility.isHeld`, `controlVisibility.isConsultReceived`, `controlVisibility.consultCallHeld`, `controlVisibility.recordingIndicator`, `controlVisibility.wrapup`, `controlVisibility.isConsultInitiatedOrAccepted` | [Doc 010](./010-component-layer-migration.md) | -| 10c | `cc-components/src/wc.ts` | Registers `WebCallControlCADComponent` with `commonPropsForCallControl` — props must align with new `TaskUIControls` shape | [Doc 010](./010-component-layer-migration.md) | -| 10d | `cc-components/.../TaskList/task-list.utils.ts` | `extractTaskListItemData(task, isBrowser, agentId)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation; `store.isDeclineButtonEnabled` | [Doc 006](./006-task-list-migration.md) | -| 10e | `cc-components/.../IncomingTask/incoming-task.utils.tsx` | `extractIncomingTaskData(incomingTask, isBrowser, logger, isDeclineButtonEnabled)` — uses `isBrowser` for accept/decline text, `disableAccept`, `disableDecline` computation | [Doc 005](./005-incoming-task-migration.md) | - -### Tier 4: Widget Entry Points (Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 11 | `task/src/CallControl/index.tsx` | Passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` from store to `useCallControl` | [Doc 004](./004-call-control-hook-migration.md) | -| 12 | `task/src/CallControlCAD/index.tsx` | Same as #11 — passes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 010](./010-component-layer-migration.md) | -| 13 | `task/src/TaskList/index.tsx` | Passes `deviceType` from store for `isBrowser` computation | [Doc 006](./006-task-list-migration.md) | -| 13b | `task/src/IncomingTask/index.tsx` | Passes `deviceType` from store to `useIncomingTask` — migrate to `task.uiControls.accept`/`decline` | [Doc 005](./005-incoming-task-migration.md) | - -### Tier 5: Utility Files (Low-Medium Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 14 | `task/src/Utils/timer-utils.ts` | `calculateStateTimerData(task, controlVisibility, agentId)` — uses `controlVisibility.wrapup`, `.consultCallHeld`, `.isConsultInitiated` | [Doc 004](./004-call-control-hook-migration.md#timer-utils-migration) | -| 15 | `task/src/Utils/useHoldTimer.ts` | Uses `findHoldTimestamp` from task-util.ts (dual signature issue) — NOT a control visibility reference, but part of consolidation | [Doc 008](./008-store-task-utils-migration.md) | -| 16 | `task/src/task.types.ts` | `useCallControlProps` interface — includes `deviceType`, `featureFlags`, `agentId`, `conferenceEnabled` | [Doc 009](./009-types-and-constants-migration.md) | -| 17 | `task/src/Utils/constants.ts` | Timer label constants — no control refs, but check for unused consult state constants | [Doc 009](./009-types-and-constants-migration.md) | - -### Tier 6: Store Constants (Low Impact) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 18 | `store/src/constants.ts` | `TASK_STATE_CONSULT`, `TASK_STATE_CONSULTING`, `CONSULT_STATE_INITIATED`, `CONSULT_STATE_COMPLETED`, etc. — used by `getConsultStatus()` | [Doc 009](./009-types-and-constants-migration.md) | -| 19 | `store/src/store.ts` | `refreshTaskList()` method, `isDeclineButtonEnabled` observable | [Doc 003](./003-store-event-wiring-migration.md) | - -### Tier 7: Test Files (Must Update After Implementation) - -| # | File | Old References | Migration Doc | -|---|------|---------------|--------------| -| 20 | `task/tests/**` | All `useCallControl` tests mock `getControlsVisibility()` return | [Doc 011](./011-execution-plan.md) | -| 21 | `cc-components/tests/**` | All CallControl component tests mock `controlVisibility` prop | [Doc 011](./011-execution-plan.md) | -| 22 | `store/tests/**` | Tests for event handlers, `refreshTaskList()`, task-utils | [Doc 011](./011-execution-plan.md) | - ---- - -## Files WITHOUT Old Control References (No Migration Needed) - -| File | Reason | -|------|--------| -| `task/src/OutdialCall/index.tsx` | CC-level API, no task controls | -| `task/src/index.ts` | Re-exports only | -| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.tsx` | Uses `secondsUntilAutoWrapup` only | -| `cc-components/.../AutoWrapupTimer/AutoWrapupTimer.utils.ts` | Pure timer formatting | -| `cc-components/.../CallControlCustom/consult-transfer-popover-hooks.ts` | Pagination/search logic | -| `cc-components/.../CallControlCustom/consult-transfer-list-item.tsx` | Display only | -| `cc-components/.../CallControlCustom/consult-transfer-dial-number.tsx` | Input handling | -| `cc-components/.../CallControlCustom/consult-transfer-empty-state.tsx` | Display only | -| `cc-components/.../TaskTimer/index.tsx` | Timer display | -| `cc-components/.../Task/index.tsx` | Task card display | -| `cc-components/.../Task/task.utils.ts` | Task data extraction for display | -| `cc-components/.../OutdialCall/outdial-call.tsx` | No task controls | -| `cc-components/.../constants.ts` | UI string constants | -| `cc-components/.../OutdialCall/constants.ts` | Outdial constants | - ---- - -## Old Control Name → File Reference Matrix - -This shows exactly which files reference each old control name: - -| Old Control Name | Files That Reference It | -|------------------|------------------------| -| `muteUnmute` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `muteUnmuteConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `holdResume` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `pauseResumeRecording` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `recordingIndicator` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `mergeConference` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts` | -| `consultTransferConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `mergeConferenceConsult` | `task-util.ts`, `call-control-custom.utils.ts`, `task.types.ts` | -| `isConferenceInProgress` | `task-util.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `task.types.ts`, `consult-transfer-popover.tsx` | -| `isConsultInitiated` | `task-util.ts`, `call-control.utils.ts`, `timer-utils.ts`, `task.types.ts` | -| `isConsultInitiatedAndAccepted` | `task-util.ts`, `task.types.ts` | -| `isConsultReceived` | `task-util.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `isConsultInitiatedOrAccepted` | `task-util.ts`, `helper.ts`, `timer-utils.ts`, `task.types.ts` | -| `isHeld` | `task-util.ts`, `call-control.utils.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `consultCallHeld` | `task-util.ts`, `timer-utils.ts`, `task.types.ts`, `call-control-cad.tsx` | -| `controlVisibility` (param name) | `helper.ts`, `timer-utils.ts`, `call-control.utils.ts`, `call-control-custom.utils.ts`, `call-control.tsx`, `call-control-consult.tsx`, `call-control-cad.tsx`, `task.types.ts` | -| `ControlVisibility` (type) | `task.types.ts` (definition), `call-control.utils.ts`, `call-control-custom.utils.ts` (imports) | -| `isBrowser` (legacy flag) | `task-list.utils.ts`, `incoming-task.utils.tsx`, `task-list.tsx`, `incoming-task.tsx` — replace with `task.uiControls.accept`/`decline` | -| `isDeclineButtonEnabled` (legacy flag) | `incoming-task.utils.tsx`, `incoming-task.tsx`, `task-list.utils.ts` — replace with `task.uiControls.decline.isEnabled` | - ---- - -## Migration Execution Order by File - -Based on dependencies: - -``` -1. task.types.ts (cc-components) — Define new TaskUIControls prop, keep ControlVisibility during transition -2. task/src/task.types.ts — Import TaskUIControls, update useCallControlProps -3. store/src/constants.ts — Mark deprecated constants -4. store/src/task-utils.ts — Remove redundant functions -5. store/src/storeEventsWrapper.ts — Add TASK_UI_CONTROLS_UPDATED, simplify handlers -6. task/src/Utils/timer-utils.ts — Accept TaskUIControls instead of ControlVisibility -7. task/src/Utils/task-util.ts — DELETE or reduce to findHoldTimestamp only -8. task/src/helper.ts — Replace getControlsVisibility() with task.uiControls -9. call-control.utils.ts — Update buildCallControlButtons() to new control names -10. call-control-custom.utils.ts — Update createConsultButtons() to new control names -11. call-control.tsx — Update to accept controls: TaskUIControls -12. call-control-consult.tsx — Update consult component props -13. consult-transfer-popover.tsx — Update isConferenceInProgress derivation -13b. call-control-cad.tsx — Replace all controlVisibility refs (isHeld, isConferenceInProgress, recordingIndicator, wrapup, isConsultInitiatedOrAccepted, isConsultReceived, consultCallHeld) -13c. wc.ts — Update commonPropsForCallControl to align with TaskUIControls shape -13d. task-list.utils.ts — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic -13e. incoming-task.utils.tsx — Replace isBrowser/isDeclineButtonEnabled with task.uiControls for accept/decline logic -14. CallControl/index.tsx — Remove old props from useCallControl call -15. CallControlCAD/index.tsx — Remove old props from useCallControl call -16. TaskList/index.tsx — Remove deviceType usage -17. IncomingTask/index.tsx — Remove deviceType, migrate to task.uiControls -18. All test files — Update mocks and assertions -``` - ---- - -_Created: 2026-03-09_ -_Parent: [001-migration-overview.md](./001-migration-overview.md)_ 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..7542d0af9 --- /dev/null +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -0,0 +1,408 @@ +# 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). + +--- + +## 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` imported from SDK. + +```typescript +// OLD — DELETE this interface +export interface ControlVisibility { + accept: Visibility; + decline: Visibility; + end: Visibility; + muteUnmute: Visibility; // → mute + muteUnmuteConsult: Visibility; // → REMOVE (use mute) + holdResume: Visibility; // → hold + consult: Visibility; + transfer: Visibility; + conference: Visibility; + wrapup: Visibility; + pauseResumeRecording: Visibility; // → recording + endConsult: Visibility; + recordingIndicator: Visibility; // → REMOVE (merged into recording) + exitConference: Visibility; + mergeConference: Visibility; // → mergeToConference + consultTransfer: Visibility; + mergeConferenceConsult: Visibility; // → REMOVE (use mergeToConference) + consultTransferConsult: Visibility; // → REMOVE (use transfer) + switchToMainCall: Visibility; + switchToConsult: Visibility; + isConferenceInProgress: boolean; // → derive from controls.exitConference.isVisible + isConsultInitiated: boolean; // → derive from controls.endConsult.isVisible + isConsultInitiatedAndAccepted: boolean; // → REMOVE + isConsultReceived: boolean; // → REMOVE + isConsultInitiatedOrAccepted: boolean; // → REMOVE + isHeld: boolean; // → derive from findHoldStatus(task, 'mainCall', agentId) + consultCallHeld: boolean; // → derive from controls.switchToConsult.isVisible +} + +// NEW — import from SDK +import type { TaskUIControls } from '@webex/contact-center'; +``` + +--- + +## 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** | +| `consultTransferConsult` | `transfer` / `transferConference` | **Split** — use `transfer` for consult transfer, `transferConference` for conference transfer | +| `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | +| `muteUnmuteConsult` | — | **Remove** (use `mute`) | +| `isConferenceInProgress` | — | **Remove** (derive from controls) | +| `isConsultInitiated` | — | **Remove** (derive from controls) | +| `isConsultInitiatedAndAccepted` | — | **Remove** | +| `isConsultReceived` | — | **Remove** | +| `isConsultInitiatedOrAccepted` | — | **Remove** | +| `isHeld` | — | **Remove** (derive from controls) | +| `consultCallHeld` | — | **Remove** | + +#### Proposed New Interface + +```typescript +interface CallControlComponentProps { + controls: TaskUIControls; // All 17 controls from SDK + // Widget-layer state (not from SDK) + isMuted: boolean; + isRecording: boolean; + holdTime: number; + secondsUntilAutoWrapup: number; + buddyAgents: Agent[]; + consultAgentName: string; + // Actions + onToggleHold: () => void; + onToggleMute: () => void; + onToggleRecording: () => void; + onEndCall: () => void; + onWrapupCall: (reason: string, auxCodeId: string) => void; + onTransferCall: (payload: TransferPayLoad) => void; + onConsultCall: (payload: ConsultPayload) => void; + 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: may use existing `getTaskStatus()` or enhance + +### 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 SDK + isMuted, isRecording, holdTime, + onToggleHold, onToggleMute, onEndCall, onEndConsult, + onConsultTransfer, onConsultConference, onExitConference, + onSwitchToMainCall, onSwitchToConsult, ... +}: CallControlComponentProps) => { + // Derive display-only flags from controls (replaces old state flag props) + const isConsulting = controls.endConsult.isVisible; + const isConferencing = controls.exitConference.isVisible; + + return ( +
+ {controls.hold.isVisible && ( + + )} + {controls.mute.isVisible && ( + + )} + {controls.end.isVisible && ( + + )} + {/* Transfer and Consult initiation */} + {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 && ( + + )} + {/* Wrapup */} + {controls.wrapup.isVisible && ( + + )} +
+ ); +}; +``` + +--- + +## Deriving State Flags from Controls + +Components that previously relied on state flags can derive them: + +```typescript +// Old: isConferenceInProgress (boolean prop) +// New: derive from controls +const isConferenceInProgress = controls.exitConference.isVisible; + +// Old: isConsultInitiatedOrAccepted (boolean prop) +// New: derive from controls +const isConsulting = controls.endConsult.isVisible; + +// Old: isHeld (boolean state flag from getControlsVisibility) +// New: derive from task data, NOT from control enabled state +// IMPORTANT: Do NOT use `controls.hold.isEnabled` to determine held state — +// hold can be disabled in consult/transition states even when call is not held. +const isHeld = findHoldStatus(currentTask, 'mainCall', agentId); +// (Uses task.data.interaction.participants to check actual hold state) +``` + +--- + +## 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` | Derive from task data: `findHoldStatus(task, 'mainCall', agentId)` | +| `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` | Derive: `controls.exitConference.isVisible` | +| `controlVisibility.consultTransfer.isEnabled` | `controls.consultTransfer.isEnabled` | +| `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` | Derive: `controls.exitConference.isVisible` | +| `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 +// NEW: derive consultInitiated from controls.endConsult.isVisible +``` + +### 4. `getConsultStatusText()` — call-control-custom.utils.ts + +```typescript +// OLD: uses consultInitiated boolean +// NEW: derive from controls.endConsult.isVisible && !controls.mergeToConference.isEnabled +``` + +--- + +## 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: derive from controls.exitConference.isVisible +``` + +### `ControlProps` — task.types.ts (Master Interface) +- `controlVisibility: ControlVisibility` → `controls: TaskUIControls` +- `isHeld: boolean` → derive from `findHoldStatus` +- `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` Widget — task/src/CallControlCAD/index.tsx +Remove `deviceType`, `featureFlags`, `conferenceEnabled` from `useCallControl` call. Retain `agentId` for timer participant lookup. + +### 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 + +| File | Action | Impact | +|------|--------|--------| +| `cc-components/.../task/task.types.ts` | Replace `ControlVisibility` with `TaskUIControls`; update `ControlProps`, `CallControlComponentProps`, etc. | **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 | **LOW** | +| `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **LOW** | +| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `conferenceEnabled` (retain `agentId`) | **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 + +--- + +_Parent: [migration-overview.md](./migration-overview.md)_ 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..6d56ac6b4 --- /dev/null +++ b/packages/contact-center/ai-docs/migration/task-list-migration.md @@ -0,0 +1,148 @@ +# 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 opportunity is to simplify how task status is derived for display. + +--- + +## 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. Task status derived via `getTaskStatus(task, agentId)` from store's `task-utils.ts` +5. Task display data extracted by `cc-components/task/Task/task.utils.ts` + +--- + +## New Approach + +### What Changes +1. **Task status derivation** can potentially use state machine state instead of `getTaskStatus()` +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` + +### Minimal Changes Required +- If `getTaskStatus()` is used for display, consider using SDK task state info +- Accept/decline button visibility per task can use `task.uiControls.accept` +- Task selection logic unchanged + +--- + +## 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) | +| Task status | `getTaskStatus(task, agentId)` from store utils | SDK task state or `task.uiControls` for button states | +| Accept action | `task.accept()` | `task.accept()` (unchanged) | +| Decline action | `task.decline()` | `task.decline()` (unchanged) | +| Select action | `store.setCurrentTask(task, isClicked)` | Unchanged | + +--- + +## 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/.../Task/task.utils.ts` | Update task data extraction if status source changes | +| `store/src/task-utils.ts` (`getTaskStatus`) | Consider deprecation if SDK provides equivalent | + +--- + +## 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 + +--- + +_Parent: [migration-overview.md](./migration-overview.md)_ From 9b8e97e15ae12065097c65db8fe41adff016bf95 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 14:41:24 +0530 Subject: [PATCH 05/15] docs(migration): align task-list and component-layer docs with task-refactor --- .../migration/component-layer-migration.md | 21 +++++++++++-------- .../ai-docs/migration/task-list-migration.md | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 7542d0af9..2cceeb6dd 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -36,12 +36,12 @@ export interface ControlVisibility { switchToMainCall: Visibility; switchToConsult: Visibility; isConferenceInProgress: boolean; // → derive from controls.exitConference.isVisible - isConsultInitiated: boolean; // → derive from controls.endConsult.isVisible + isConsultInitiated: boolean; // → Do NOT use endConsult.isVisible as "initiated only"; it covers both initiated and accepted. Use task/participant state if you need that distinction. isConsultInitiatedAndAccepted: boolean; // → REMOVE isConsultReceived: boolean; // → REMOVE isConsultInitiatedOrAccepted: boolean; // → REMOVE isHeld: boolean; // → derive from findHoldStatus(task, 'mainCall', agentId) - consultCallHeld: boolean; // → derive from controls.switchToConsult.isVisible + consultCallHeld: boolean; // → derive from findHoldStatus(task, 'consult', agentId). Do NOT use controls.switchToConsult.isVisible (that is button visibility, not hold state). } // NEW — import from SDK @@ -72,15 +72,16 @@ import type { TaskUIControls } from '@webex/contact-center'; | `isConsultInitiatedAndAccepted` | — | **Remove** | | `isConsultReceived` | — | **Remove** | | `isConsultInitiatedOrAccepted` | — | **Remove** | -| `isHeld` | — | **Remove** (derive from controls) | -| `consultCallHeld` | — | **Remove** | +| `isHeld` | `isHeld` | **Retain** — parent derives via `findHoldStatus(task, 'mainCall', agentId)` and passes to component. Do NOT derive from `controls.hold.isEnabled`. | +| `consultCallHeld` | — | **Remove** (derive from `findHoldStatus(task, 'consult', agentId)` in parent if needed for display) | #### Proposed New Interface ```typescript interface CallControlComponentProps { controls: TaskUIControls; // All 17 controls from SDK - // Widget-layer state (not from SDK) + // Widget-layer state (not from SDK). isHeld must be derived by parent via findHoldStatus(task, 'mainCall', agentId). + isHeld: boolean; isMuted: boolean; isRecording: boolean; holdTime: number; @@ -192,6 +193,7 @@ const CallControlComponent = ({ // call-control.tsx — new approach const CallControlComponent = ({ controls, // TaskUIControls — all 17 controls from SDK + isHeld, // From parent: findHoldStatus(task, 'mainCall', agentId) isMuted, isRecording, holdTime, onToggleHold, onToggleMute, onEndCall, onEndConsult, onConsultTransfer, onConsultConference, onExitConference, @@ -201,11 +203,12 @@ const CallControlComponent = ({ const isConsulting = controls.endConsult.isVisible; const isConferencing = controls.exitConference.isVisible; + // isHeld must be passed from parent, derived via findHoldStatus(task, 'mainCall', agentId). Do NOT use controls.hold.isEnabled for toggle — hold can be disabled in consult/conference without the call being held. return (
{controls.hold.isVisible && ( - )} {controls.mute.isVisible && ( @@ -298,7 +301,7 @@ This function builds the main call control button array. It references 12 old co | `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | | `controlVisibility.consult.isVisible` | `controls.consult.isVisible` | | `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | -| `controlVisibility.consultTransfer.isEnabled` | `controls.consultTransfer.isEnabled` | +| `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` | @@ -355,7 +358,7 @@ This function builds the main call control button array. It references 12 old co - `agentId: string` → RETAIN (needed for timer participant lookup) ### `CallControlCAD` Widget — task/src/CallControlCAD/index.tsx -Remove `deviceType`, `featureFlags`, `conferenceEnabled` from `useCallControl` call. Retain `agentId` for timer participant lookup. +Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for the feature-flag overlay (`applyFeatureGates`). Retain `agentId` for timer participant lookup and for deriving `isHeld` via `findHoldStatus(task, 'mainCall', agentId)` to pass to CallControl component. ### Files NOT Impacted (Confirmed) diff --git a/packages/contact-center/ai-docs/migration/task-list-migration.md b/packages/contact-center/ai-docs/migration/task-list-migration.md index 6d56ac6b4..3f73c8170 100644 --- a/packages/contact-center/ai-docs/migration/task-list-migration.md +++ b/packages/contact-center/ai-docs/migration/task-list-migration.md @@ -31,8 +31,9 @@ The TaskList widget displays all active tasks and allows accept/decline/select. ### Minimal Changes Required - If `getTaskStatus()` is used for display, consider using SDK task state info -- Accept/decline button visibility per task can use `task.uiControls.accept` +- Accept/decline button visibility per task: use `task.uiControls?.accept` and `task.uiControls?.decline` (each has `isVisible`, `isEnabled`) - 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) --- From 70b8a7d1a18ce79cfd7190171f7801803851cc62 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 15:13:04 +0530 Subject: [PATCH 06/15] docs(migration): fix onToggleHold signature and CallControlCAD table (Codex PR 648) --- .../ai-docs/migration/component-layer-migration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 2cceeb6dd..5a937c15b 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -88,8 +88,8 @@ interface CallControlComponentProps { secondsUntilAutoWrapup: number; buddyAgents: Agent[]; consultAgentName: string; - // Actions - onToggleHold: () => void; + // 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; @@ -387,7 +387,7 @@ Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for | `cc-components/.../CallControlCustom/consult-transfer-popover.tsx` | Update `isConferenceInProgress` prop | **LOW** | | `cc-components/.../IncomingTask/incoming-task.tsx` | Minor prop updates | **LOW** | | `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **LOW** | -| `task/src/CallControlCAD/index.tsx` | Remove `deviceType`, `featureFlags`, `conferenceEnabled` (retain `agentId`) | **MEDIUM** | +| `task/src/CallControlCAD/index.tsx` | **Retain** `deviceType`, `featureFlags`, `conferenceEnabled` for `applyFeatureGates` overlay; retain `agentId` | **MEDIUM** | | All test files for above | Update mocks and assertions | **HIGH** | --- From 67eea18031e039a8625ef9c184a079f4b3aaaa20 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 15:24:46 +0530 Subject: [PATCH 07/15] docs(migration): align onEndConsultCall name and add TaskList utils to checklist (Codex PR 648) --- .../ai-docs/migration/component-layer-migration.md | 4 ++-- .../contact-center/ai-docs/migration/task-list-migration.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 5a937c15b..f14772ee0 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -195,7 +195,7 @@ const CallControlComponent = ({ controls, // TaskUIControls — all 17 controls from SDK isHeld, // From parent: findHoldStatus(task, 'mainCall', agentId) isMuted, isRecording, holdTime, - onToggleHold, onToggleMute, onEndCall, onEndConsult, + onToggleHold, onToggleMute, onEndCall, onEndConsultCall, onConsultTransfer, onConsultConference, onExitConference, onSwitchToMainCall, onSwitchToConsult, ... }: CallControlComponentProps) => { @@ -228,7 +228,7 @@ const CallControlComponent = ({ )} {/* Active consult controls */} {controls.endConsult.isVisible && ( - + )} {controls.mergeToConference.isVisible && ( diff --git a/packages/contact-center/ai-docs/migration/task-list-migration.md b/packages/contact-center/ai-docs/migration/task-list-migration.md index 3f73c8170..a5895deec 100644 --- a/packages/contact-center/ai-docs/migration/task-list-migration.md +++ b/packages/contact-center/ai-docs/migration/task-list-migration.md @@ -130,6 +130,7 @@ export const useTaskList = (props: UseTaskListProps) => { | `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 status source changes | | `store/src/task-utils.ts` (`getTaskStatus`) | Consider deprecation if SDK provides equivalent | From 1c5a4d2ab2efc4828c4b503097df2fc25de1747f Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 15:39:45 +0530 Subject: [PATCH 08/15] docs(migration): TaskUIControls via cc-store, wire Transfer/Consult handlers (Codex PR 648) --- .../ai-docs/migration/component-layer-migration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index f14772ee0..065d0f6f5 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -44,8 +44,8 @@ export interface ControlVisibility { consultCallHeld: boolean; // → derive from findHoldStatus(task, 'consult', agentId). Do NOT use controls.switchToConsult.isVisible (that is button visibility, not hold state). } -// NEW — import from SDK -import type { TaskUIControls } from '@webex/contact-center'; +// NEW — import via store to preserve layering (cc-components → store → SDK). Store re-exports TaskUIControls from SDK. +import type { TaskUIControls } from '@webex/cc-store'; ``` --- @@ -221,10 +221,10 @@ const CallControlComponent = ({ )} {/* Transfer and Consult initiation */} {controls.transfer.isVisible && ( - + )} {controls.consult.isVisible && ( - + )} {/* Active consult controls */} {controls.endConsult.isVisible && ( From fbaff4765bb8c5d6a964190cc64b6629d9ff61cd Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 15:54:27 +0530 Subject: [PATCH 09/15] docs(migration): align transfer/consult button wiring with payload handler signatures (Codex PR 648) --- .../ai-docs/migration/component-layer-migration.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 065d0f6f5..4f825e27d 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -94,8 +94,8 @@ interface CallControlComponentProps { onToggleRecording: () => void; onEndCall: () => void; onWrapupCall: (reason: string, auxCodeId: string) => void; - onTransferCall: (payload: TransferPayLoad) => void; - onConsultCall: (payload: ConsultPayload) => void; + 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; @@ -199,6 +199,7 @@ const CallControlComponent = ({ onConsultTransfer, onConsultConference, onExitConference, onSwitchToMainCall, onSwitchToConsult, ... }: CallControlComponentProps) => { + // Implement openTransferPopover / openConsultPopover (e.g. set state to show popover); popover on submit calls onTransferCall(payload) / onConsultCall(payload). // Derive display-only flags from controls (replaces old state flag props) const isConsulting = controls.endConsult.isVisible; const isConferencing = controls.exitConference.isVisible; @@ -219,12 +220,12 @@ const CallControlComponent = ({ {controls.end.isVisible && ( )} - {/* Transfer and Consult initiation */} + {/* 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 && ( From d147f474d4ebb8ca2e57742c3f9beccba5baacca Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 16:08:17 +0530 Subject: [PATCH 10/15] docs(migration): fix broken parent link to migration overview (Codex PR 648) --- .../ai-docs/migration/component-layer-migration.md | 2 +- .../contact-center/ai-docs/migration/task-list-migration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 4f825e27d..b1033082a 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -409,4 +409,4 @@ Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for --- -_Parent: [migration-overview.md](./migration-overview.md)_ +_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 index a5895deec..2e19d51ac 100644 --- a/packages/contact-center/ai-docs/migration/task-list-migration.md +++ b/packages/contact-center/ai-docs/migration/task-list-migration.md @@ -147,4 +147,4 @@ export const useTaskList = (props: UseTaskListProps) => { --- -_Parent: [migration-overview.md](./migration-overview.md)_ +_Part of the task refactor migration doc set (overview in PR 1/4)._ From 3375c74f52cb5066a684d1bf5b4a2d307139ba96 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 16:25:41 +0530 Subject: [PATCH 11/15] docs(migration): wire Wrap Up button to wrap-up callback --- .../ai-docs/migration/component-layer-migration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index b1033082a..0c32b2d15 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -93,7 +93,7 @@ interface CallControlComponentProps { onToggleMute: () => void; onToggleRecording: () => void; onEndCall: () => void; - onWrapupCall: (reason: string, auxCodeId: string) => 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; @@ -199,7 +199,7 @@ const CallControlComponent = ({ onConsultTransfer, onConsultConference, onExitConference, onSwitchToMainCall, onSwitchToConsult, ... }: CallControlComponentProps) => { - // Implement openTransferPopover / openConsultPopover (e.g. set state to show popover); popover on submit calls onTransferCall(payload) / onConsultCall(payload). + // 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; const isConferencing = controls.exitConference.isVisible; @@ -253,9 +253,9 @@ const CallControlComponent = ({ {isRecording ? 'Pause' : 'Resume'} Recording )} - {/* Wrapup */} + {/* Wrap Up: button opens wrap-up UI; UI on submit calls onWrapupCall(reason, auxCodeId) */} {controls.wrapup.isVisible && ( - + )}
); From 085d8153c63902b308a3be82a73c0e184ac50bf7 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Mon, 16 Mar 2026 16:38:36 +0530 Subject: [PATCH 12/15] docs(migration): do not derive consult-init from control visibility --- .../ai-docs/migration/component-layer-migration.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 0c32b2d15..10e0d207a 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -324,14 +324,17 @@ This function builds the main call control button array. It references 12 old co ```typescript // OLD: uses consultInitiated flag -// NEW: derive consultInitiated from controls.endConsult.isVisible +// NEW: do NOT derive consult-init state from controls.endConsult.isVisible (it spans both initiated and accepted). +// Use task/participant state or SDK consult phase for "initiated only" if needed. ``` ### 4. `getConsultStatusText()` — call-control-custom.utils.ts ```typescript // OLD: uses consultInitiated boolean -// NEW: derive from controls.endConsult.isVisible && !controls.mergeToConference.isEnabled +// NEW: do NOT derive from control visibility (endConsult.isVisible, mergeToConference.isEnabled); +// visibility can change for feature gating or temporary disable and misclassify phase. +// Use task/participant state or SDK consult phase for accurate status/timer text. ``` --- From 425255fde661fd6fae852c6b351a06e03842a5db Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 18 Mar 2026 10:42:14 +0530 Subject: [PATCH 13/15] docs(migration): add-utils-wc-layer-and-prop-types-to-component-checklist Made-with: Cursor --- .../migration/component-layer-migration.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 10e0d207a..acb8904ca 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -361,8 +361,9 @@ This function builds the main call control button array. It references 12 old co - `conferenceEnabled: boolean` → REMOVE (SDK handles) - `agentId: string` → RETAIN (needed for timer participant lookup) -### `CallControlCAD` Widget — task/src/CallControlCAD/index.tsx -Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for the feature-flag overlay (`applyFeatureGates`). Retain `agentId` for timer participant lookup and for deriving `isHeld` via `findHoldStatus(task, 'mainCall', agentId)` to pass to CallControl component. +### `CallControlCAD` — task package and cc-components view +- **task/src/CallControlCAD/index.tsx:** Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for the feature-flag overlay (`applyFeatureGates`). Retain `agentId` for timer participant lookup and for deriving `isHeld` via `findHoldStatus(task, 'mainCall', agentId)` to pass to CallControl component. +- **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) @@ -381,16 +382,22 @@ Retain `deviceType`, `featureFlags`, `conferenceEnabled` in `useCallControl` for ## 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. + | File | Action | Impact | |------|--------|--------| -| `cc-components/.../task/task.types.ts` | Replace `ControlVisibility` with `TaskUIControls`; update `ControlProps`, `CallControlComponentProps`, etc. | **HIGH** | +| `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 | **LOW** | -| `cc-components/.../TaskList/task-list.tsx` | Minor prop updates | **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` | **Retain** `deviceType`, `featureFlags`, `conferenceEnabled` for `applyFeatureGates` overlay; retain `agentId` | **MEDIUM** | | All test files for above | Update mocks and assertions | **HIGH** | From a97cdacebe09e0e255323c63237fcd734e54fa02 Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 18 Mar 2026 10:52:18 +0530 Subject: [PATCH 14/15] docs(migration): add-before-after-for-utils-callcontrolcad-view-and-wc-layer --- .../migration/component-layer-migration.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index acb8904ca..4e21f7a02 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -384,6 +384,175 @@ This function builds the main call control button array. It references 12 old co **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` (and `isHeld` from parent if retained). Replace all `controlVisibility.*` with the new shape: use `controls.wrapup.isVisible`, `controls.recording.isVisible`, etc.; derive conference/consult display from controls or task state as documented in CallControlComponent section; pass `controls` to CallControlComponent. + +```tsx +// call-control-cad.tsx +{controls.exitConference?.isVisible && !controls.wrapup.isVisible && ( + // ... +)} +{isHeld && !controls.consult?.isVisible && ( + // consultCallHeld derived by parent via findHoldStatus(task, 'consult', agentId) if needed + // ... +)} +{controls.recording?.isVisible && ( + // ... +)} +{controls.endConsult?.isVisible && ( + // isConsultInitiatedOrAccepted replaced by control visibility or task state + // ... +)} + +``` + +### 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** | From 791b189ea7be96273635870f35e1303a8e549c3e Mon Sep 17 00:00:00 2001 From: Akula Uday Date: Wed, 18 Mar 2026 11:25:41 +0530 Subject: [PATCH 15/15] docs(migration): address-codex-pr-648-conference-state-buddy-type-hold-chip --- .../migration/component-layer-migration.md | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/contact-center/ai-docs/migration/component-layer-migration.md b/packages/contact-center/ai-docs/migration/component-layer-migration.md index 4e21f7a02..bad88ad15 100644 --- a/packages/contact-center/ai-docs/migration/component-layer-migration.md +++ b/packages/contact-center/ai-docs/migration/component-layer-migration.md @@ -35,7 +35,7 @@ export interface ControlVisibility { consultTransferConsult: Visibility; // → REMOVE (use transfer) switchToMainCall: Visibility; switchToConsult: Visibility; - isConferenceInProgress: boolean; // → derive from controls.exitConference.isVisible + isConferenceInProgress: boolean; // → derive from task/participant state (e.g. conference in progress); 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/participant state if you need that distinction. isConsultInitiatedAndAccepted: boolean; // → REMOVE isConsultReceived: boolean; // → REMOVE @@ -67,8 +67,8 @@ import type { TaskUIControls } from '@webex/cc-store'; | `consultTransferConsult` | `transfer` / `transferConference` | **Split** — use `transfer` for consult transfer, `transferConference` for conference transfer | | `mergeConferenceConsult` | — | **Remove** (use `mergeToConference`) | | `muteUnmuteConsult` | — | **Remove** (use `mute`) | -| `isConferenceInProgress` | — | **Remove** (derive from controls) | -| `isConsultInitiated` | — | **Remove** (derive from controls) | +| `isConferenceInProgress` | — | **Remove** (derive from task/participant state, not controls.exitConference.isVisible) | +| `isConsultInitiated` | — | **Remove** (derive from controls or task state) | | `isConsultInitiatedAndAccepted` | — | **Remove** | | `isConsultReceived` | — | **Remove** | | `isConsultInitiatedOrAccepted` | — | **Remove** | @@ -86,7 +86,7 @@ interface CallControlComponentProps { isRecording: boolean; holdTime: number; secondsUntilAutoWrapup: number; - buddyAgents: Agent[]; + 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; @@ -202,7 +202,8 @@ const CallControlComponent = ({ // 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; - const isConferencing = controls.exitConference.isVisible; + // Derive from task/participant state; do not use controls.exitConference.isVisible as sole source + const isConferencing = /* task/participant conference state */; // isHeld must be passed from parent, derived via findHoldStatus(task, 'mainCall', agentId). Do NOT use controls.hold.isEnabled for toggle — hold can be disabled in consult/conference without the call being held. return ( @@ -270,8 +271,9 @@ Components that previously relied on state flags can derive them: ```typescript // Old: isConferenceInProgress (boolean prop) -// New: derive from controls -const isConferenceInProgress = controls.exitConference.isVisible; +// New: derive from task/participant state (e.g. conference-in-progress), NOT from controls.exitConference.isVisible — +// exitConference.isVisible can be forced false when consult is active even if conference is in progress. +const isConferenceInProgress = /* derive from task.data or participants */; // Old: isConsultInitiatedOrAccepted (boolean prop) // New: derive from controls @@ -301,7 +303,7 @@ This function builds the main call control button array. It references 12 old co | `controlVisibility.holdResume.isVisible` | `controls.hold.isVisible` | | `controlVisibility.consult.isEnabled` | `controls.consult.isEnabled` | | `controlVisibility.consult.isVisible` | `controls.consult.isVisible` | -| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | +| `controlVisibility.isConferenceInProgress` | Derive from task/participant state (not `controls.exitConference.isVisible` — that is an action flag and 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` | @@ -315,7 +317,7 @@ This function builds the main call control button array. It references 12 old co |--------------|---------------| | `controlVisibility.muteUnmuteConsult` | `controls.mute` | | `controlVisibility.switchToMainCall` | `controls.switchToMainCall` | -| `controlVisibility.isConferenceInProgress` | Derive: `controls.exitConference.isVisible` | +| `controlVisibility.isConferenceInProgress` | Derive from task/participant state (not `controls.exitConference.isVisible`) | | `controlVisibility.consultTransferConsult` | `controls.transfer` / `controls.transferConference` | | `controlVisibility.mergeConferenceConsult` | `controls.mergeToConference` | | `controlVisibility.endConsult` | `controls.endConsult` | @@ -350,7 +352,7 @@ This function builds the main call control button array. It references 12 old co ### `ConsultTransferPopoverComponentProps` — task.types.ts ```typescript // OLD: isConferenceInProgress?: boolean -// NEW: derive from controls.exitConference.isVisible +// NEW: derive from task/participant state (not controls.exitConference.isVisible) ``` ### `ControlProps` — task.types.ts (Master Interface) @@ -484,15 +486,16 @@ export const extractTaskListItemData = ( ``` -**After:** Component receives `controls: TaskUIControls` (and `isHeld` from parent if retained). Replace all `controlVisibility.*` with the new shape: use `controls.wrapup.isVisible`, `controls.recording.isVisible`, etc.; derive conference/consult display from controls or task state as documented in CallControlComponent section; pass `controls` to CallControlComponent. +**After:** Component receives `controls: TaskUIControls`, `isHeld` from parent, and **consult-state flags** (`isConsultReceived`, `consultCallHeld`) from parent — do NOT use `controls.consult.isVisible` for hold-chip gating, as that is an action-availability flag, not consult state. Derive conference display from task/participant state (not solely `controls.exitConference.isVisible`). Pass `controls` to CallControlComponent. ```tsx // call-control-cad.tsx -{controls.exitConference?.isVisible && !controls.wrapup.isVisible && ( +// Conference: derive from task/participant state (e.g. isConferenceInProgress from parent), not controls.exitConference.isVisible +{isConferenceInProgress && !controls.wrapup.isVisible && ( // ... )} -{isHeld && !controls.consult?.isVisible && ( - // consultCallHeld derived by parent via findHoldStatus(task, 'consult', agentId) if needed +// Hold chip: keep consult-state gating; parent passes isConsultReceived and consultCallHeld (or derives via findHoldStatus(task, 'consult', agentId)) +{isHeld && !isConsultReceived && !consultCallHeld && ( // ... )} {controls.recording?.isVisible && (