diff --git a/.gitignore b/.gitignore index 0afe52a0e..563a57023 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ yarn-debug.log* yarn-error.log* .node_repl_history reports/ +.generated/ #IDEs .project diff --git a/ai-docs/features/agent-desktop-consult-transfer-list-policy/spec/feature-spec.md b/ai-docs/features/agent-desktop-consult-transfer-list-policy/spec/feature-spec.md new file mode 100644 index 000000000..040afb0cc --- /dev/null +++ b/ai-docs/features/agent-desktop-consult-transfer-list-policy/spec/feature-spec.md @@ -0,0 +1,292 @@ +--- +type: Feature Spec +title: Agent Desktop consult and transfer list policy +description: Keep consult and transfer destination eligibility and ordering aligned with Agent Desktop while leaving widgets as a thin SDK consumer. +tags: [feature, specification, contact-center, consult-transfer] +--- + +# Agent Desktop consult and transfer list policy + +This document owns the feature's what and why. The paired SDK delta owns the reusable destination-list policy; this repository owns only action context, current-task context, UI loading, and error presentation. + +Related context: [repository architecture](../../../ARCHITECTURE.md) · [specification index](../../../SPEC_INDEX.md) · [repository instructions](../../../../AGENTS.md) + +## Metadata + +| Field | Value | +| --- | --- | +| Feature key | `CAI-8354` | +| Owner | Webex Contact Center widgets maintainers | +| Status | Approved and implemented; generator-side conformance PASS; independent validation pending | +| Work type | Defect | +| Change class | Contract / UI | +| Source/intake | Developer-approved Agent Desktop parity review and current code/tests | +| Last verified | 2026-08-19 in a working tree based on `4b928847` | + +## Applicability + +| Condition ID | Status | Evidence or reason | Owned section | +| --- | --- | --- | --- | +| `feature.feature_nontrivial` | Applicable | `packages/contact-center/store/src/storeEventsWrapper.ts` | Feasibility and risks | +| `feature.feature_interactions` | Applicable | `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` | Interaction and scenario matrix | +| `feature.touches_data_shapes` | Applicable | `packages/contact-center/store/src/store.types.ts` | Requested data and fields | +| `feature.backward_compat` | Applicable | `packages/contact-center/store/package.json` | Migration expectations | +| `feature.perf_critical` | N/A | This change does not add client-side processing or a new request fan-out. | Scale and performance | +| `feature.security_compliance` | N/A | The store forwards authenticated SDK calls and adds no credential or authorization ownership. | Security and compliance | +| `feature.needs_rollout` | N/A | No widget feature flag or staged runtime path is introduced. | Rollout and feature controls | +| `feature.serviceability` | Applicable | `packages/contact-center/task/src/helper.ts` | Serviceability | +| `feature.doc_obligations` | Applicable | `packages/contact-center/store/ai-docs/store-spec.md` | Documentation obligations | +| `feature.changes_ui` | Applicable | `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx` | UI flow and design | +| `feature.changes_api` | Applicable | `packages/contact-center/store/src/store.types.ts` | API contract delta | +| `feature.changes_events` | N/A | No event name, payload, producer, consumer, or delivery order changes. | Event contract delta | +| `feature.changes_public_api` | Applicable | `packages/contact-center/cc-components/src/components/task/task.types.ts` | Public API and semver impact | +| `feature.cross_package` | Applicable | `packages/contact-center/store/package.json` | Cross-package impact | + +## Problem and goal + +The widgets previously embedded destination policy in the store: buddy agents were always restricted to Available, queues were fetched through the generic SDK API and filtered in memory by channel, and list response metadata was reconstructed. That made widget behavior diverge from Agent Desktop and made list order dependent on widget-side transformation. + +The goal is for widgets to supply only the user's action, pagination/search input, and the current task media needed by the queue policy. The SDK returns eligible, backend-ordered lists and their metadata. Widgets must not choose ordering, entry-point media defaults, reusable eligibility, or pagination semantics. + +## Stakeholders and open questions + +| Stakeholder | Need or decision | Status | +| --- | --- | --- | +| Contact Center agents | Consult and transfer destination lists match Agent Desktop eligibility and order. | Decided | +| Widget maintainers | Destination business policy remains outside React and MobX UI code. | Decided | +| SDK maintainers | SDK services own list ordering defaults and specialized methods own consult/transfer eligibility. | Decided in the paired SDK delta | +| Host applications | Existing widget UI behavior remains compatible apart from the corrected destination results. | Decided | + +There are no open product decisions for this delta. + +## Scope + +### In scope + +- Pass `Consult` or `Transfer` from the call-control menu and reload action to the SDK through the task hook and store. +- Forward only pagination, page size, and search text for entry-point and dial-number lists; forward current-task media only for queues, where it selects the task channel. +- Load dial numbers through the generic SDK AddressBook service and rely on its default backend ordering. +- Preserve the SDK's `data` order and pagination metadata without local sorting, channel filtering, or metadata reconstruction. +- Keep loading, empty, and error behavior in the existing widget layers. +- Use the locally linked SDK worktree while verifying the coordinated change. + +### Out of scope + +- Reimplementing queue, entry-point, or buddy-agent eligibility in widgets. +- Sorting any destination array in React, the task hook, or the store. +- Supplying `sortBy` or `sortOrder` from the consult/transfer widget path. +- Changing backend ordering or adding a widget feature flag. +- Committing, publishing, or pushing either repository. + +## Prior work and evidence + +| Source | What it establishes | Decision or disposition | +| --- | --- | --- | +| `packages/contact-center/store/src/storeEventsWrapper.ts` | The old store filtered queue data and rebuilt pagination metadata; the new store delegates to specialized SDK methods. | Used | +| `packages/contact-center/task/src/helper.ts` | The task hook owns UI loading/error state and forwards list input. | Used | +| `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx` | The selected menu identifies Consult versus Transfer. | Used | +| `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx` | Reload has the same action context as initial list loading. | Used | +| `packages/contact-center/store/tests/storeEventsWrapper.ts` | Store delegation, action forwarding, metadata preservation, and error propagation are asserted. | Used | +| `packages/contact-center/task/tests/helper.ts` | Hook forwarding and safe empty-page fallback are asserted. | Used | +| `packages/contact-center/cc-components/tests/components/task/CallControl` | Initial load and reload preserve action context. | Used | + +## Requirements + +| ID | WHAT | WHY | Source evidence | Test or example evidence | Assumptions or gaps | Confidence | +| --- | --- | --- | --- | --- | --- | --- | +| `WIDGET-LIST-R-001` | The store must use the SDK's specialized consult/transfer queue and entry-point methods, use the generic SDK AddressBook service for dial numbers, and must not apply local eligibility filters, sorting, or pagination reconstruction. | SDK-owned defaults prevent drift while avoiding a redundant consult-specific dial-number API. | `packages/contact-center/store/src/storeEventsWrapper.ts` | `packages/contact-center/store/tests/storeEventsWrapper.ts` | Requires the paired SDK delta at runtime. | Present | +| `WIDGET-LIST-R-002` | Opening or reloading the Agents list must forward the active `Consult` or `Transfer` action through the component, hook, and store. | Agent eligibility differs by action, so losing the action would silently return the wrong population. | `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx`, `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `packages/contact-center/task/src/helper.ts` | `packages/contact-center/cc-components/tests/components/task/CallControl`, `packages/contact-center/task/tests/helper.ts` | None. | Present | +| `WIDGET-LIST-R-003` | Queue requests must forward page, page size, search, and current-task media; entry-point and dial-number requests must forward only pagination/search. | Queue media comes from the active task, while telephony entry-point behavior and both list-order defaults are SDK decisions that widgets must not duplicate. | `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/task/src/helper.ts` | `packages/contact-center/store/tests/storeEventsWrapper.ts`, `packages/contact-center/task/tests/helper.ts` | When queue media is absent, the SDK default applies. | Present | +| `WIDGET-LIST-R-004` | Widgets must render destination arrays in the order supplied by the SDK and preserve the SDK pagination metadata object. | Backend-selected ordering must not be changed or made inconsistent by a second client sort. | `packages/contact-center/store/src/storeEventsWrapper.ts` | `packages/contact-center/store/tests/storeEventsWrapper.ts` | Presentational virtualized-list behavior remains unchanged. | Present | +| `WIDGET-LIST-R-005` | Store failures must be logged and rethrown; the task hook must convert destination-page failures to an empty page and buddy-agent failures to an empty agent list while ending loading state. | Existing UI boundaries need predictable empty/error behavior without hiding failures at the SDK/store boundary. | `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/task/src/helper.ts` | `packages/contact-center/store/tests/storeEventsWrapper.ts`, `packages/contact-center/task/tests/helper.ts` | Existing UI error presentation remains unchanged. | Present | +| `WIDGET-LIST-R-006` | Store and component types must expose the action-aware loader and the specialized SDK request/response shapes without `any`. | Compile-time alignment prevents the widgets from recreating SDK policy through loosely typed escape hatches. | `packages/contact-center/store/src/store.types.ts`, `packages/contact-center/cc-components/src/components/task/task.types.ts` | `packages/contact-center/store/tests/storeEventsWrapper.ts`, `packages/contact-center/task/tests/helper.ts`, `packages/contact-center/cc-components/tests/components/task/CallControl` | The local SDK link is required until a released SDK contains the new exports. | Present | + +## Defect context (when applicable) + +- Observed versus expected behavior: widgets could show a different destination population/order because they always requested Available agents, used generic queue/entry-point APIs, locally filtered queues, and rebuilt metadata; expected behavior is the Agent Desktop request policy with no widget-side sorting or filtering. +- Reproduction and environment: open Consult and Transfer destination popovers for the same active task and compare Agents, Queues, and Entry Points with Agent Desktop. +- Regression range or last known good state: unknown; the previous widget implementation predates this coordinated SDK policy. +- Severity, frequency, and workaround: user-visible whenever backend order or action eligibility differs; no reliable host-side workaround. +- Diagnostic evidence: `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/store/tests/storeEventsWrapper.ts`. + +## MODIFIED Requirements + +### MOD-001 — Store list delegation (`STORE-R-015`) + +- **WHAT**: Replace widget-owned queue filtering/metadata reconstruction and generic entry-point calls with thin delegation to the SDK's consult/transfer queue/entry-point methods. Keep dial numbers on the generic AddressBook service. Pass current-task media only to queues and pass no ordering inputs; preserve each SDK response as returned. +- **WHY**: Eligibility, backend query flags, and ordering defaults are reusable domain policy and must not diverge across UI clients. +- **Evidence:** `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/store/tests/storeEventsWrapper.ts`. +- **Acceptance:** No queue, entry-point, or dial-number `.sort()`/`.filter()` exists in the store list path, and tests prove response order and metadata are unchanged. + +### MOD-002 — Task consult/transfer orchestration (`TASK-R-011` through `TASK-R-014`) + +- **WHAT**: Extend the existing consult/transfer flow so list loading carries `Consult` or `Transfer`; queue loading no longer derives and passes an independent media argument from the hook. +- **WHY**: The action affects buddy-agent eligibility, while media and request policy must be resolved once at the store/SDK boundary. +- **Evidence:** `packages/contact-center/task/src/helper.ts`, `packages/contact-center/task/tests/helper.ts`. +- **Acceptance:** Transfer loading reaches the store as `Transfer`, Consult remains the default, and paginated queue inputs contain only page, page size, and search. + +### MOD-003 — Call-control action context (`CC-COMPONENTS-R-006`) + +- **WHAT**: Initial menu opening and agent-list reload must call the loader with the active menu action. +- **WHY**: A reload must not silently revert Transfer eligibility to Consult eligibility. +- **Evidence:** `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx`, `packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `packages/contact-center/cc-components/tests/components/task/CallControl`. +- **Acceptance:** Component tests cover both the initial Transfer load and action-preserving reload. + +## Acceptance criteria + +- [x] Consult agent loading forwards `Consult`; Transfer agent loading forwards `Transfer` on initial open and reload (`MOD-002`, `MOD-003`, `WIDGET-LIST-R-002`). +- [x] Queue and entry-point fetchers delegate to specialized SDK methods, while dial numbers use AddressBook, without local sort/filter/metadata logic (`MOD-001`, `WIDGET-LIST-R-001`, `WIDGET-LIST-R-004`). +- [x] Only queue requests include current-task media when available; entry-point and dial-number requests contain no widget-selected media or sorting (`MOD-001`, `WIDGET-LIST-R-003`). +- [x] Store errors are rethrown and task-hook errors retain the existing empty-result behavior (`WIDGET-LIST-R-005`). +- [x] Store, task, test-fixtures, and cc-components build/type surfaces agree with the linked SDK (`WIDGET-LIST-R-006`). +- [x] Store, task, and cc-components unit suites pass with the coordinated SDK worktree. + +## Scenarios and applicable change views + +| Scenario | Actor | Preconditions | Expected behavior | Failure or boundary behavior | Requirements | +| --- | --- | --- | --- | --- | --- | +| Open Consult Agents | Agent | Active task and Consult selected | UI forwards `Consult`; SDK result order is rendered unchanged. | SDK failure produces an empty agent list and clears loading. | `WIDGET-LIST-R-002`, `WIDGET-LIST-R-005` | +| Open Transfer Agents | Agent | Active task and Transfer selected | UI forwards `Transfer`; SDK applies transfer eligibility. | Reload retains `Transfer`; it does not fall back to Consult. | `WIDGET-LIST-R-002` | +| Search Queues | Agent | Active task with media context | Page/search input plus media reach the SDK; response and metadata are preserved. | Missing task media lets the SDK default to telephony. | `WIDGET-LIST-R-003`, `WIDGET-LIST-R-004` | +| Search Entry Points | Agent | Entry-point tab visible | Page/search input reaches the SDK and backend order is rendered. | Failure becomes the existing empty paginated result. | `WIDGET-LIST-R-004`, `WIDGET-LIST-R-005` | +| Search Dial Numbers | Agent | Address book enabled | Page/search input reaches AddressBook and its SDK-default backend order is rendered. | Failure becomes the existing empty paginated result. | `WIDGET-LIST-R-001`, `WIDGET-LIST-R-004`, `WIDGET-LIST-R-005` | + +### Interaction and scenario matrix + +| Context or interacting state | Trigger | Expected result | Invalid or conflicting result | Requirements | +| --- | --- | --- | --- | --- | +| Consult + Agents | Open or reload | `loadBuddyAgents('Consult')` | Applying Transfer-only availability filtering | `WIDGET-LIST-R-002` | +| Transfer + Agents | Open or reload | `loadBuddyAgents('Transfer')` | Reloading with the default Consult action | `WIDGET-LIST-R-002` | +| Queue + current task media | Fetch/search/page | Store forwards media once to the specialized SDK call | Hook/store filters returned rows or sorts them again | `WIDGET-LIST-R-001`, `WIDGET-LIST-R-003`, `WIDGET-LIST-R-004` | +| No current task media | Queue fetch | SDK receives no media override and uses its default | Widget invents a backend channel policy | `WIDGET-LIST-R-003` | +| Entry point or dial number | Fetch/search/page | Store forwards pagination/search only and preserves SDK order | Widget supplies media or sort policy | `WIDGET-LIST-R-001`, `WIDGET-LIST-R-003`, `WIDGET-LIST-R-004` | + +### UI flow and design + +The visible popover, tabs, row presentation, pagination, loading indicators, empty states, and accessibility labels do not change. The only UI contract change is that initial and reload actions carry the active Consult/Transfer intent. The rendered list order is exactly the SDK response order. + +### API contract delta + +| API or operation | Change | Consumer impact | Compatibility expectation | Canonical definition | +| --- | --- | --- | --- | --- | +| Store buddy-agent loader | Accepts optional `Consult`/`Transfer` action instead of a media argument. | Task and component layers pass user intent. | Coordinated widgets release required. | `packages/contact-center/store/src/store.types.ts` | +| Store queue loader | Accepts pagination/search only and delegates to the SDK's specialized method. | Callers no longer supply independent media policy. | Coordinated widgets/SDK release required. | `packages/contact-center/store/src/store.types.ts` | +| Store entry-point loader | Delegates pagination/search to the SDK's specialized method. | No caller-owned media, profile, filter, or sort flags. | Additive at the SDK boundary; coordinated store update. | `packages/contact-center/store/src/store.types.ts` | +| Store dial-number loader | Delegates pagination/search to the generic SDK AddressBook service. | The SDK default supplies backend name ordering. | Existing SDK surface with a corrected default. | `packages/contact-center/store/src/store.types.ts` | + +### Public API and semver impact + +| Export or entry point | Change | Affected consumers | Required version change | Deprecation or migration | +| --- | --- | --- | --- | --- | +| `@webex/cc-store` loader types | Action-aware and specialized request shapes | Internal widget packages and any direct store consumer | Semver-sensitive; coordinate under repository release policy | Direct consumers must pass action rather than media to the buddy loader. | +| `@webex/cc-components` call-control loader prop | Optional action parameter | Call-control consumers | Additive callback argument for compatible functions; coordinate typings | Consumers may ignore the argument, but action-aware loaders should use it. | + +### Cross-package impact + +| Package | Change | Dependency direction | Release sequencing | Owner | +| --- | --- | --- | --- | --- | +| `@webex/contact-center` | Supplies specialized queue/entry-point APIs plus AddressBook and EntryPoint ordering defaults. | SDK → store | Build/link first. | SDK maintainers | +| `@webex/cc-store` | Thin delegation and typed boundary. | store → SDK | Release with a compatible SDK version. | Widgets maintainers | +| `@webex/cc-task` | Carries action and pagination/search. | task → store | Release after store types. | Widgets maintainers | +| `@webex/cc-components` | Carries menu action on open/reload. | components → task callback | Release with task package. | Widgets maintainers | + +## Contracts delta + +**Provides — MODIFIED:** The widget packages provide action-aware list loading and preserve SDK result order/metadata without owning eligibility policy. + +**Requires — MODIFIED:** The store requires an SDK that exports `ConsultTransferAction`, `ConsultTransferListSearchParams`, `ConsultTransferQueueSearchParams`, `getConsultTransferQueues`, and `getConsultTransferEntryPoints`. + +No event contract changes. + +## Success and guardrail metrics + +| Metric | Baseline | Target | Measurement source | +| --- | --- | --- | --- | +| Widget-side consult/transfer queue filters or sorts | Present | 0 | `packages/contact-center/store/src/storeEventsWrapper.ts` | +| Action loss on initial/reload agent fetch | Possible | 0 covered paths | `packages/contact-center/cc-components/tests/components/task/CallControl` | +| Touched package unit failures | Unknown before change | 0 | Store, task, and cc-components unit suites | +| Touched package build/style failures | Unknown before change | 0 | Store, task, test-fixtures, and cc-components build/style commands | + +## Requested data and fields + +| Entity or payload | Requested field or shape | Purpose | Ownership | Privacy, retention, or compatibility constraint | +| --- | --- | --- | --- | --- | +| Buddy-agent request | `action`, optional current task `mediaType` | Distinguish Consult and Transfer eligibility. | SDK contract; widgets supply runtime context. | No new retained data or credentials. | +| Queue list request | `page`, `pageSize`, `search`, optional current task `mediaType` | Paginated destination discovery. | SDK owns reusable defaults. | No widget-owned eligibility flags. | +| Entry-point list request | `page`, `pageSize`, `search` | Paginated destination discovery. | SDK owns telephony eligibility and backend name ordering. | No widget-owned media, eligibility, or sort flags. | +| Dial-number list request | `page`, `pageSize`, `search` | Paginated address-book destination discovery. | AddressBook owns backend name ordering. | No widget-owned sort flags. | +| Paginated response | SDK `data` and `meta` unchanged | Preserve backend order and pagination truth. | SDK/backend | No client reconstruction. | + +## Impacted domains + +| Repository or module | Impact | Owner | +| --- | --- | --- | +| `packages/contact-center/store` | SDK boundary and response preservation | Widgets maintainers | +| `packages/contact-center/task` | Action and list-input forwarding | Widgets maintainers | +| `packages/contact-center/cc-components` | Initial/reload action propagation | Widgets maintainers | +| `packages/contact-center/test-fixtures` | Type-compatible fixtures | Widgets maintainers | + +## Feasibility and risks + +| Risk or assumption | Evidence | Mitigation or decision owner | +| --- | --- | --- | +| Widgets are run with an SDK lacking the new methods. | `packages/contact-center/store/package.json` | Coordinate the SDK dependency/release; use the approved local worktree link for testing. | +| A future widget reintroduces local sorting/filtering. | `packages/contact-center/store/tests/storeEventsWrapper.ts` | Retain delegation and exact-response assertions. | +| Transfer action is lost during reload. | `packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx` | Retain the action-specific reload assertion. | + +## Error Matrix + +| Failure | Store behavior | Task/UI behavior | Evidence | +| --- | --- | --- | --- | +| Buddy-agent SDK rejection | Log and rethrow. | Log, clear agents, end loading. | `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/task/src/helper.ts` | +| Queue SDK rejection | Log and rethrow. | Log and return an empty paginated result. | `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/task/src/helper.ts` | +| Entry-point SDK rejection | Log and rethrow. | Log and return an empty paginated result. | `packages/contact-center/store/src/storeEventsWrapper.ts`, `packages/contact-center/task/src/helper.ts` | +| Missing current task media | Omit media from the SDK request. | Existing UI flow continues. | `packages/contact-center/store/src/storeEventsWrapper.ts` | + +## Resilience + +- The change adds no retry or duplicate request loop; existing component reload remains the explicit retry mechanism. +- Empty fallbacks remain confined to the task/UI boundary, while the store preserves rejection semantics for other consumers. +- Response order and pagination are not cached or reconstructed by widgets. + +## Observability + +- Existing store and task error logs remain the diagnostic surface. +- Successful buddy-agent loading retains the count-only informational log; no agent identities or list contents are logged. +- No new metric, trace, alert, or PII-bearing log is introduced. + +## Operations + +- Build/link the paired SDK before widgets so the new exports resolve. +- Run the store, task, and cc-components unit suites plus touched package builds/styles before release. +- Roll back widgets and SDK together if the specialized methods are unavailable; no data migration or cleanup is required. + +## Migration expectations + +- Compatibility: the SDK additions are additive, but direct `@webex/cc-store` callers of the changed loader signatures must migrate with the widgets release. +- Data or consumer transition: release a compatible SDK before or with the store, then task and component packages. +- Coexistence period: local development uses the approved SDK worktree link; published packages must use a released compatible SDK. +- Completion and rollback outcome: all packages resolve the same types and list methods; rollback is a coordinated dependency/code rollback with no persisted state. + +## Serviceability + +| Signal or support surface | Required change | Consumer or operator | Acceptance evidence | +| --- | --- | --- | --- | +| Store error log | Preserve SDK rejection context without list contents. | Widget maintainers | `packages/contact-center/store/tests/storeEventsWrapper.ts` | +| Task error log | Preserve list kind and operation context. | Widget maintainers | `packages/contact-center/task/tests/helper.ts` | +| Buddy load info log | Record count only. | Widget maintainers | `packages/contact-center/task/src/helper.ts` | + +## Documentation obligations + +- This approved delta modifies `STORE-R-015`, the task consult/transfer requirement family, and `CC-COMPONENTS-R-006` without overwriting the draft canonical module specs. +- The paired SDK feature spec remains the canonical owner for filter, projection, ordering, profile-view, media mapping, and cache-bypass decisions. +- A future canonical-spec promotion must fold this delta into the routed module specs and reconcile the delta path rather than duplicate the requirements. + +## Decision and change log + +| Date | Decision or change | Rationale | Owner | +| --- | --- | --- | --- | +| 2026-08-19 | Approved this exact MODIFIED delta path. | Avoid overwriting draft canonical module specs while keeping spec-currency with the implementation. | Developer | +| 2026-08-19 | Assigned reusable list policy to the SDK and retained only UI/runtime context in widgets. | Prevent policy duplication and ordering drift. | Developer + Codex | +| 2026-08-19 | Explicitly prohibited widget-side sorting/filtering of SDK destination results. | Preserve the backend order selected by the SDK request. | Developer + Codex | +| 2026-08-19 | Removed widget selection of entry-point media and the consult-specific dial-number helper. | EntryPoint and AddressBook defaults must work for widgets out of the box; other SDK consumers can pass explicit overrides. | Developer + Codex | diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts index c827bc1de..70fc7a691 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts @@ -30,7 +30,7 @@ import {DEFAULT_PAGE_SIZE} from '../../constants'; * @param logger - Optional logger instance for diagnostics. * @returns An object containing the transformed data (U[]), pagination state and helpers. */ -export const usePaginatedData = ( +export const usePaginatedData = ( fetchFunction: FetchPaginatedList | undefined, transformFunction: TransformPaginatedData, categoryName: string, diff --git a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 32e1ab744..e9abfd649 100644 --- a/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -11,6 +11,12 @@ import { getAgentsForDisplay, } from './call-control-custom.utils'; import {useConsultTransferPopover} from './consult-transfer-popover-hooks'; +import { + isAgentsTabVisible, + isEntryPointTabVisible, + isQueuesTabVisible, + ConsultTransferAction, +} from './consult-transfer-tab.utils'; import { SEARCH_PLACEHOLDER, @@ -34,12 +40,26 @@ const ConsultTransferPopoverComponent: React.FC { const {showDialNumberTab = true, showEntryPointTab = true} = consultTransferOptions || {}; - const isEntryPointTabVisible = showEntryPointTab && heading === 'Consult'; + const action: ConsultTransferAction = heading === 'Transfer' ? 'Transfer' : 'Consult'; + const isQueueTabVisible = isQueuesTabVisible( + action, + allowConsultToQueue, + accessQueue, + interactionContext ?? {}, + isTelephony + ); + const isAgentsTabVisibleFlag = isAgentsTabVisible(accessBuddyTeam); + const isEntryPointTabVisibleFlag = isEntryPointTabVisible(showEntryPointTab, accessEntryPoint, isTelephony); const { selectedCategory, searchQuery, @@ -61,7 +81,7 @@ const ConsultTransferPopoverComponent: React.FC ); - const noQueues = !allowConsultToQueue || queuesData.length === 0; + const noQueues = !isQueueTabVisible || queuesData.length === 0; const noDialNumbers = !showDialNumberTab || dialNumbers.length === 0; - const noEntryPoints = !isEntryPointTabVisible || entryPoints.length === 0; + const noEntryPoints = !isEntryPointTabVisibleFlag || entryPoints.length === 0; const consultTransferManualAction = shouldAddConsultTransferAction( selectedCategory, - isEntryPointTabVisible, + isEntryPointTabVisibleFlag, allowParticipantsToInteract, searchQuery, entryPoints, @@ -133,7 +153,7 @@ const ConsultTransferPopoverComponent: React.FC { if (selectedCategory === CATEGORY_AGENTS && loadBuddyAgents) { - loadBuddyAgents(); + loadBuddyAgents(action); } else { handleReload(); } @@ -180,17 +200,19 @@ const ConsultTransferPopoverComponent: React.FC
- - {allowConsultToQueue && ( + {isAgentsTabVisibleFlag && ( + + )} + {isQueueTabVisible && ( +
+
+
+
`; -exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false 1`] = ` +exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elements and visual states of ConsultTransferPopoverComponent component should render with allowConsultToQueue false for Consult 1`] = `
@@ -1411,7 +1483,7 @@ exports[`ConsultTransferPopoverComponent Snapshots Rendering - Tests for UI elem tagname="h3" type="body-large-bold" > - Select an Agent + Consult
+
+
+
+
+
+
+
+
+
+
+
{ expect(container).toMatchSnapshot(); }); - it('should render with allowConsultToQueue false', async () => { - const noQueueConsultProps = {...defaultProps, allowConsultToQueue: false}; + it('should render with allowConsultToQueue false for Consult', async () => { + const noQueueConsultProps = {...defaultProps, heading: 'Consult', allowConsultToQueue: false}; let screen; await act(async () => { screen = render(); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx index 375b76089..b1fec9365 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx @@ -223,9 +223,10 @@ describe('ConsultTransferPopoverComponent', () => { expect(screen.container.querySelectorAll('.call-control-list-item').length).toBe(0); }); - it('hides queue tab when allowConsultToQueue is false', async () => { + it('hides queue tab when allowConsultToQueue is false for Consult', async () => { const propsWithoutQueue = { ...baseProps, + heading: 'Consult', allowConsultToQueue: false, }; @@ -234,6 +235,33 @@ describe('ConsultTransferPopoverComponent', () => { expect(maybeQueuesButton).toBeNull(); }); + it('shows queue tab for Transfer inbound when consultToQueue is off and accessQueue is SPECIFIC (AVERA)', async () => { + const averaProps = { + ...baseProps, + heading: 'Transfer', + allowConsultToQueue: false, + accessQueue: 'SPECIFIC', + interactionContext: {contactDirectionType: 'INBOUND', mediaType: 'telephony'}, + isTelephony: true, + }; + + const screen = await render(); + expect(screen.getByRole('button', {name: 'Queues'})).toBeInTheDocument(); + }); + + it('shows entry point tab on Transfer when accessEntryPoint allows it', async () => { + const transferProps = { + ...baseProps, + heading: 'Transfer', + accessEntryPoint: 'SPECIFIC', + isTelephony: true, + consultTransferOptions: {showEntryPointTab: true}, + }; + + const screen = await render(); + expect(screen.getByRole('button', {name: 'Entry Point'})).toBeInTheDocument(); + }); + it('covers edge case for empty items in renderList (line 50)', async () => { const propsWithEmptyAgents = { ...baseProps, @@ -333,6 +361,7 @@ describe('ConsultTransferPopoverComponent', () => { fireEvent.click(reloadButton); expect(mockLoadBuddyAgents).toHaveBeenCalledTimes(1); + expect(mockLoadBuddyAgents).toHaveBeenCalledWith('Consult'); }); it('reloads queues when reload button clicked on Queues tab', async () => { diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts new file mode 100644 index 000000000..dd38f1256 --- /dev/null +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils.ts @@ -0,0 +1,81 @@ +import { + ConsultTransferInteractionContext, + isAgentsTabVisible, + isCollaborationAccessEnabled, + isEntryPointTabVisible, + isQueueEnabled, + isQueuesTabVisible, +} from '../../../../../src/components/task/CallControl/CallControlCustom/consult-transfer-tab.utils'; + +describe('consult-transfer-tab.utils', () => { + const inboundVoice: ConsultTransferInteractionContext = { + contactDirectionType: 'INBOUND', + mediaType: 'telephony', + }; + + const outboundVoiceTransferDisabled: ConsultTransferInteractionContext = { + contactDirectionType: 'OUTBOUND', + outdialTransferToQueueEnabled: false, + mediaType: 'telephony', + }; + + const outboundVoiceTransferEnabled: ConsultTransferInteractionContext = { + contactDirectionType: 'OUTBOUND', + outdialTransferToQueueEnabled: true, + mediaType: 'telephony', + }; + + describe('isCollaborationAccessEnabled', () => { + it('returns false when access is NONE (case-insensitive)', () => { + expect(isCollaborationAccessEnabled('NONE')).toBe(false); + expect(isCollaborationAccessEnabled('none')).toBe(false); + }); + + it('returns true for ALL, SPECIFIC, or undefined', () => { + expect(isCollaborationAccessEnabled('ALL')).toBe(true); + expect(isCollaborationAccessEnabled('SPECIFIC')).toBe(true); + expect(isCollaborationAccessEnabled(undefined)).toBe(true); + }); + }); + + describe('isQueueEnabled', () => { + it('requires allowConsultToQueue for Consult on voice', () => { + expect(isQueueEnabled('Consult', false, inboundVoice, true)).toBe(false); + expect(isQueueEnabled('Consult', true, inboundVoice, true)).toBe(true); + }); + + it('shows queues for Transfer on inbound voice even when consultToQueue is off (AVERA case)', () => { + expect(isQueueEnabled('Transfer', false, inboundVoice, true)).toBe(true); + }); + + it('gates Transfer outbound voice on outdialTransferToQueueEnabled', () => { + expect(isQueueEnabled('Transfer', false, outboundVoiceTransferDisabled, true)).toBe(false); + expect(isQueueEnabled('Transfer', false, outboundVoiceTransferEnabled, true)).toBe(true); + }); + + it('returns true for non-voice media', () => { + expect(isQueueEnabled('Consult', false, {mediaType: 'chat'}, false)).toBe(true); + }); + }); + + describe('tab visibility helpers', () => { + it('hides agents tab when accessBuddyTeam is NONE', () => { + expect(isAgentsTabVisible('NONE')).toBe(false); + expect(isAgentsTabVisible('SPECIFIC')).toBe(true); + }); + + it('shows queue tab for Transfer inbound when accessQueue is SPECIFIC and consultToQueue is off', () => { + expect(isQueuesTabVisible('Transfer', false, 'SPECIFIC', inboundVoice, true)).toBe(true); + }); + + it('hides queue tab when accessQueue is NONE even if queue transfer is otherwise enabled', () => { + expect(isQueuesTabVisible('Transfer', false, 'NONE', inboundVoice, true)).toBe(false); + }); + + it('shows entry point tab on voice when showEntryPointTab and accessEntryPoint allow it', () => { + expect(isEntryPointTabVisible(true, 'SPECIFIC', true)).toBe(true); + expect(isEntryPointTabVisible(true, 'NONE', true)).toBe(false); + expect(isEntryPointTabVisible(true, 'SPECIFIC', false)).toBe(false); + }); + }); +}); diff --git a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx index 8363fb2ec..01272d0ae 100644 --- a/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx +++ b/packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx @@ -338,6 +338,7 @@ describe('CallControlComponent', () => { // After clicking, the popover should be expanded expect(transferButton).toHaveAttribute('aria-expanded', 'true'); + expect(modifiedProps.loadBuddyAgents).toHaveBeenCalledWith('Transfer'); // Verify buttons maintain their CSS classes after interactions expect(transferButton).toHaveClass('call-control-button'); @@ -476,6 +477,7 @@ describe('CallControlComponent', () => { fireEvent.click(consultButton); await screen.findByRole('button', {name: 'Agents'}); + expect(defaultProps.loadBuddyAgents).toHaveBeenCalledWith('Consult'); expect(screen.getByRole('button', {name: 'Queues'})).toBeInTheDocument(); expect(screen.getByRole('button', {name: 'Entry Point'})).toBeInTheDocument(); expect(screen.queryByRole('button', {name: 'Dial Number'})).not.toBeInTheDocument(); diff --git a/packages/contact-center/store/ai-docs/store-spec.md b/packages/contact-center/store/ai-docs/store-spec.md index e0a3dbd84..713552968 100644 --- a/packages/contact-center/store/ai-docs/store-spec.md +++ b/packages/contact-center/store/ai-docs/store-spec.md @@ -281,14 +281,16 @@ classDiagram - **UC-3 Observe agent/session state in React:** Widget wraps in `observer()` and reads `store.agentId`, `store.isAgentLoggedIn`, `store.deviceType`, `store.currentState` → re-renders on mutation. Evidence: `src/storeEventsWrapper.ts:56-187`, `_archive/.../AGENTS.md` usage. - **UC-4 Handle an incoming task through to wrap-up:** SDK `task:incoming` → listeners registered + `onIncomingTask` fired → `task:assigned` sets ENGAGED/current → `task:end` + `handleTaskRemove` cleans up. Evidence: `src/storeEventsWrapper.ts:585-762`, `tests/storeEventsWrapper.ts` ("events reactions"). - **UC-5 Campaign-preview accept flow:** `task:campaignPreviewReservation` puts a preview in RESERVED; preview stays out of `currentTask` until accepted (`acceptedCampaignIds`), then transitions to ENGAGED. Evidence: `src/storeEventsWrapper.ts:243-283,537-583,772-795`, `tests/storeEventsWrapper.ts` ("campaign preview task lifecycle"). -- **UC-6 Fetch a domain list for a widget dropdown:** Transfer/Consult widget calls `getBuddyAgents()`/`getQueues()`; Outdial calls `getEntryPoints()`/`getAddressBookEntries()` → store proxies the SDK, transforms/filters, returns. Evidence: `src/storeEventsWrapper.ts:924-1001`, `tests/storeEventsWrapper.ts`. +- **UC-6 Fetch a domain list for a widget dropdown:** Transfer/Consult widget calls `getBuddyAgents()`/`getQueues()`; Outdial calls `getEntryPoints()`/`getAddressBookEntries()` → store proxies the SDK, transforms/filters, returns. `getQueues`/`getEntryPoints` merge `desktopProfileFilter: true`. Evidence: `src/storeEventsWrapper.ts:924-1001`, `tests/storeEventsWrapper.ts`. ## State Model The store is a single MobX `makeAutoObservable` instance. Observable slices (all in `src/store.ts:23-56`): - **Session / profile:** `agentId`, `agentProfile`, `isAgentLoggedIn`, `deviceType`, `dialNumber`, `teamId`, `teams`, `loginOptions`, `idleCodes`, `wrapupCodes`, `featureFlags`, `dataCenter`. - **Agent state:** `currentState`, `customState`, `lastStateChangeTimestamp`, `lastIdleCodeChangeTimestamp`, `showMultipleLoginAlert`. - **Tasks:** `taskList` (`Record`), `currentTask`, `acceptedCampaignIds` (`Set`), `realtimeTranscriptionData`. -- **Call/consult control:** `isMuted`, `callControlAudio`, `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`, `isDeclineButtonEnabled`, `isEndConsultEnabled`, `allowConsultToQueue`, `isDigitalChannelsInitialized`. +- **Call/consult control:** `isMuted`, `callControlAudio`, `isQueueConsultInProgress`, `currentConsultQueueId`, `consultStartTimeStamp`, `isDeclineButtonEnabled`, `isEndConsultEnabled`, `allowConsultToQueue`, `accessQueue`, `accessEntryPoint`, `accessBuddyTeam`, `isDigitalChannelsInitialized`. +- **`getQueues` / `getEntryPoints`:** merge `desktopProfileFilter: true` into SDK search params. List sort deferred pending team decision on CMS/BFF sort param parity. +- **`getBuddyAgents`:** returns agents sorted by `agentName` ascending (client-side; full list, no pagination). - **Misc:** `currentTheme`, `cc` (`observable.ref` — not deeply observed), `isAddressBookEnabled`. Transition triggers: SDK CC/task events drive the session/agent/task slices via the wrapper's handlers (`handleStateChange`, `handleTaskAssigned`, `refreshTaskList`, `cleanUpStore`, campaign-preview handlers). Widget-initiated mutators (`setDeviceType`, `setDialNumber`, `setTeamId`, `setState`, `setCurrentTheme`, etc.) drive UI-local slices. All writes pass through `runInAction`. diff --git a/packages/contact-center/store/src/store.ts b/packages/contact-center/store/src/store.ts index 52654bbd7..6434ec3e5 100644 --- a/packages/contact-center/store/src/store.ts +++ b/packages/contact-center/store/src/store.ts @@ -50,6 +50,9 @@ class Store implements IStore { isEndConsultEnabled: boolean = false; isAddressBookEnabled: boolean = false; allowConsultToQueue: boolean = false; + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; agentProfile: AgentLoginProfile = {}; isMuted: boolean = false; isDigitalChannelsInitialized: boolean = false; @@ -118,6 +121,14 @@ class Store implements IStore { // TODO: Remove this once SDK performs the validation this.isAddressBookEnabled = Boolean(response.addressBookId); this.allowConsultToQueue = response.allowConsultToQueue; + const collaborationProfile = response as Profile & { + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; + }; + this.accessQueue = collaborationProfile.accessQueue; + this.accessEntryPoint = collaborationProfile.accessEntryPoint; + this.accessBuddyTeam = collaborationProfile.accessBuddyTeam; this.agentProfile.agentName = response.agentName; this.agentProfile.isTimeoutDesktopInactivityEnabled = response.isTimeoutDesktopInactivityEnabled; this.agentProfile.timeoutDesktopInactivityMins = response.timeoutDesktopInactivityMins; diff --git a/packages/contact-center/store/src/store.types.ts b/packages/contact-center/store/src/store.types.ts index 61dac9e40..f7325c363 100644 --- a/packages/contact-center/store/src/store.types.ts +++ b/packages/contact-center/store/src/store.types.ts @@ -11,6 +11,10 @@ import { EntryPointRecord, EntryPointListResponse, EntryPointSearchParams, + ConsultTransferAction, + ConsultTransferListSearchParams, + ConsultTransferQueueSearchParams, + ConsultTransferEntryPointSearchParams, AddressBookEntry, AddressBookEntriesResponse, AddressBookEntrySearchParams, @@ -62,6 +66,8 @@ interface IContactCenter { getBuddyAgents(data: BuddyAgents): Promise; getQueues(params?: ContactServiceQueueSearchParams): Promise; getEntryPoints(params?: EntryPointSearchParams): Promise; + getConsultTransferQueues(params?: ConsultTransferQueueSearchParams): Promise; + getConsultTransferEntryPoints(params?: ConsultTransferEntryPointSearchParams): Promise; addressBook: AddressBook; agentConfig?: { regexUS: RegExp | string; @@ -71,6 +77,7 @@ interface IContactCenter { setAgentState(data: StateChange): Promise; getOutdialAniEntries(params: OutdialAniParams): Promise; getAccessToken(): Promise; + startOutdial(destination: string, origin?: string): Promise; acceptPreviewContact(payload: PreviewContactPayload): Promise; skipPreviewContact(payload: PreviewContactPayload): Promise; removePreviewContact(payload: PreviewContactPayload): Promise; @@ -196,6 +203,9 @@ interface IStore { callControlAudio: MediaStream | null; isEndConsultEnabled: boolean; allowConsultToQueue: boolean; + accessQueue?: string; + accessEntryPoint?: string; + accessBuddyTeam?: string; agentProfile: AgentLoginProfile; isMuted: boolean; isAddressBookEnabled: boolean; @@ -213,9 +223,9 @@ interface IStoreWrapper extends IStore { onErrorCallback?: (widgetName: string, error: Error) => void; setCurrentTask(task: ITask): void; refreshTaskList(): void; - getBuddyAgents(mediaType?: string): Promise; - getQueues(mediaType?: string, params?: ContactServiceQueueSearchParams): Promise; - getEntryPoints(params?: EntryPointSearchParams): Promise; + getBuddyAgents(action?: ConsultTransferAction): Promise; + getQueues(params?: ConsultTransferListSearchParams): Promise; + getEntryPoints(params?: ConsultTransferListSearchParams): Promise; getAddressBookEntries(params?: AddressBookEntrySearchParams): Promise; setDeviceType(option: string): void; setDialNumber(input: string): void; @@ -356,6 +366,10 @@ export type { EntryPointRecord, EntryPointListResponse, EntryPointSearchParams, + ConsultTransferAction, + ConsultTransferListSearchParams, + ConsultTransferQueueSearchParams, + ConsultTransferEntryPointSearchParams, AddressBookEntry, AddressBookEntriesResponse, AddressBookEntrySearchParams, diff --git a/packages/contact-center/store/src/storeEventsWrapper.ts b/packages/contact-center/store/src/storeEventsWrapper.ts index efa3cf9ce..7aad5d2a6 100644 --- a/packages/contact-center/store/src/storeEventsWrapper.ts +++ b/packages/contact-center/store/src/storeEventsWrapper.ts @@ -15,10 +15,10 @@ import { ENGAGED_USERNAME, RESERVED_LABEL, RESERVED_USERNAME, - ContactServiceQueue, - ContactServiceQueueSearchParams, + ContactServiceQueuesResponse, EntryPointListResponse, - EntryPointSearchParams, + ConsultTransferAction, + ConsultTransferListSearchParams, AddressBookEntriesResponse, AddressBookEntrySearchParams, Profile, @@ -31,8 +31,6 @@ import Store from './store'; import { DEVICE_TYPE_BROWSER, MEDIA_TYPE_TELEPHONY_LOWER, - MEDIA_TYPE_TELEPHONY_UPPER, - AGENT_STATE_AVAILABLE, CAMPAIGN_PREVIEW_OUTBOUND_TYPES, CAMPAIGN_PREVIEW_CAMPAIGN_TYPES, } from './store.types'; @@ -188,6 +186,18 @@ class StoreWrapper implements IStoreWrapper { return this.store.allowConsultToQueue; } + get accessQueue() { + return this.store.accessQueue; + } + + get accessEntryPoint() { + return this.store.accessEntryPoint; + } + + get accessBuddyTeam() { + return this.store.accessBuddyTeam; + } + get agentProfile() { return this.store.agentProfile; } @@ -1070,14 +1080,12 @@ class StoreWrapper implements IStoreWrapper { }); }; - getBuddyAgents = async ( - mediaType: string = this.currentTask.data.interaction.mediaType - ): Promise> => { + getBuddyAgents = async (action: ConsultTransferAction = 'Consult'): Promise> => { try { + const mediaType = this.currentTask?.data?.interaction?.mediaType; const response = await this.store.cc.getBuddyAgents({ - //@ts-expect-error To be fixed in SDK - https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6762 - mediaType: mediaType ?? MEDIA_TYPE_TELEPHONY_LOWER, - state: AGENT_STATE_AVAILABLE, + action, + ...(mediaType ? {mediaType} : {}), }); return 'data' in response ? response.data.agentList : []; } catch (error) { @@ -1086,35 +1094,23 @@ class StoreWrapper implements IStoreWrapper { } }; - getQueues = async ( - mediaType: string = this.currentTask.data.interaction.mediaType ?? MEDIA_TYPE_TELEPHONY_UPPER, - params?: ContactServiceQueueSearchParams - ): Promise<{ - data: ContactServiceQueue[]; - meta: {page: number; pageSize: number; total: number; totalPages: number}; - }> => { + getQueues = async (params?: ConsultTransferListSearchParams): Promise => { try { - const upperMediaType = mediaType.toUpperCase(); - const response = await this.store.cc.getQueues(params); - const data = Array.isArray(response) ? response : response.data; - const filtered = data.filter((queue) => queue.channelType === upperMediaType); - const page = Array.isArray(response) ? 0 : (response.meta?.page ?? 0); - const totalPages = Array.isArray(response) ? 1 : (response.meta?.totalPages ?? 1); - const pageSize = Array.isArray(response) ? filtered.length : (response.meta?.pageSize ?? filtered.length); - const total = Array.isArray(response) - ? filtered.length - : ((response as {meta?: {total?: number}}).meta?.total ?? filtered.length); - return {data: filtered, meta: {page, pageSize, total, totalPages}}; + const mediaType = this.currentTask?.data?.interaction?.mediaType; + + return await this.store.cc.getConsultTransferQueues({ + ...(params ?? {}), + ...(mediaType ? {mediaType} : {}), + }); } catch (error) { this.store.logger.error('Error fetching queues:', error); throw error; } }; - getEntryPoints = async (params?: EntryPointSearchParams): Promise => { + getEntryPoints = async (params?: ConsultTransferListSearchParams): Promise => { try { - const response: EntryPointListResponse = await this.store.cc.getEntryPoints(params); - return response; + return await this.store.cc.getConsultTransferEntryPoints(params); } catch (error) { this.store.logger.error('Error fetching entry points:', error); throw error; @@ -1126,8 +1122,7 @@ class StoreWrapper implements IStoreWrapper { if (!this.store.isAddressBookEnabled) { return {data: [], meta: {page: 0, totalPages: 0}}; } - const response: AddressBookEntriesResponse = await this.store.cc.addressBook.getEntries(params ?? {}); - return response; + return await this.store.cc.addressBook.getEntries(params ?? {}); } catch (error) { this.store.logger.error('Error fetching address book entries:', error); throw error; diff --git a/packages/contact-center/store/tests/storeEventsWrapper.ts b/packages/contact-center/store/tests/storeEventsWrapper.ts index 62d624aab..39e6d87ed 100644 --- a/packages/contact-center/store/tests/storeEventsWrapper.ts +++ b/packages/contact-center/store/tests/storeEventsWrapper.ts @@ -998,38 +998,78 @@ describe('storeEventsWrapper', () => { }); it('should return buddy agents list', async () => { - const buddyAgents = [{name: 'agent1'}, {name: 'agent2'}]; + const buddyAgents = [ + {agentName: 'Zeta Agent', agentId: '3'}, + {agentName: 'Alpha Agent', agentId: '1'}, + {agentName: 'Beta Agent', agentId: '2'}, + ]; + storeWrapper['store'].currentTask = {data: {interaction: {mediaType: 'telephony'}}} as ITask; storeWrapper['store'].cc.getBuddyAgents = jest.fn().mockResolvedValue({data: {agentList: buddyAgents}}); - const result = await storeWrapper.getBuddyAgents('telephony'); + const result = await storeWrapper.getBuddyAgents('Consult'); expect(result).toEqual(buddyAgents); + expect(storeWrapper['store'].cc.getBuddyAgents).toHaveBeenCalledWith({ + action: 'Consult', + mediaType: 'telephony', + }); + }); + + it('should pass the transfer intent to the SDK', async () => { + storeWrapper['store'].currentTask = {data: {interaction: {mediaType: 'telephony'}}} as ITask; + storeWrapper['store'].cc.getBuddyAgents = jest.fn().mockResolvedValue({data: {agentList: []}}); + + await storeWrapper.getBuddyAgents('Transfer'); + + expect(storeWrapper['store'].cc.getBuddyAgents).toHaveBeenCalledWith({ + action: 'Transfer', + mediaType: 'telephony', + }); }); it('should handle error in getBuddyAgents and throw error', async () => { + storeWrapper['store'].currentTask = null; storeWrapper['store'].cc.getBuddyAgents = jest.fn().mockRejectedValue(new Error('error')); - await expect(storeWrapper.getBuddyAgents('telephony')).rejects.toThrow('error'); + await expect(storeWrapper.getBuddyAgents('Consult')).rejects.toThrow('error'); }); it('should return contact service queues list', async () => { const queueList = [ {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, {id: 'queue2', name: 'Queue 2', channelType: 'TELEPHONY'}, - {id: 'queue3', name: 'Queue 3', channelType: 'CHAT'}, // This one should be filtered out + {id: 'queue3', name: 'Queue 3', channelType: 'CHAT'}, ]; - storeWrapper['store'].cc.getQueues = jest.fn().mockResolvedValue(queueList); + const response = {data: queueList, meta: {page: 0, totalPages: 1}}; + storeWrapper['store'].currentTask = {data: {interaction: {mediaType: 'telephony'}}} as ITask; + storeWrapper['store'].cc.getConsultTransferQueues = jest.fn().mockResolvedValue(response); - const result = await storeWrapper.getQueues('telephony'); + const result = await storeWrapper.getQueues(); - expect(result.data).toEqual([ - {id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}, - {id: 'queue2', name: 'Queue 2', channelType: 'TELEPHONY'}, - ]); - expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); + expect(result.data).toEqual(queueList); + expect(storeWrapper['store'].cc.getConsultTransferQueues).toHaveBeenCalledWith({ + mediaType: 'telephony', + }); + }); + + it('should pass only runtime context and list inputs when getQueues is called with params', async () => { + const queueList = [{id: 'queue1', name: 'Queue 1', channelType: 'TELEPHONY'}]; + storeWrapper['store'].currentTask = {data: {interaction: {mediaType: 'telephony'}}} as ITask; + storeWrapper['store'].cc.getConsultTransferQueues = jest + .fn() + .mockResolvedValue({data: queueList, meta: {page: 1, totalPages: 1}}); + + await storeWrapper.getQueues({page: 1, pageSize: 25}); + + expect(storeWrapper['store'].cc.getConsultTransferQueues).toHaveBeenCalledWith({ + page: 1, + pageSize: 25, + mediaType: 'telephony', + }); }); it('should handle error in getQueues and throw error', async () => { - storeWrapper['store'].cc.getQueues = jest.fn().mockRejectedValue(new Error('queue error')); + storeWrapper['store'].currentTask = null; + storeWrapper['store'].cc.getConsultTransferQueues = jest.fn().mockRejectedValue(new Error('queue error')); - await expect(storeWrapper.getQueues('telephony')).rejects.toThrow('queue error'); + await expect(storeWrapper.getQueues()).rejects.toThrow('queue error'); }); it('should return contact service queues list when SDK returns paginated response', async () => { @@ -1037,14 +1077,14 @@ describe('storeEventsWrapper', () => { {...mockQueueDetails[0], channelType: 'TELEPHONY'}, {...mockQueueDetails[1], channelType: 'CHAT'}, ]; - storeWrapper['store'].cc.getQueues = jest - .fn() - .mockResolvedValue({data: queueList, meta: {page: 1, pageSize: 50, total: 2, totalPages: 1}}); + const response = {data: queueList, meta: {page: 1, pageSize: 50, totalRecords: 2, totalPages: 1}}; + storeWrapper['store'].currentTask = null; + storeWrapper['store'].cc.getConsultTransferQueues = jest.fn().mockResolvedValue(response); - const result = await storeWrapper.getQueues('telephony'); + const result = await storeWrapper.getQueues(); - expect(result.data).toEqual([{...mockQueueDetails[0], channelType: 'TELEPHONY'}]); - expect(storeWrapper['store'].cc.getQueues).toHaveBeenCalled(); + expect(result).toEqual(response); + expect(storeWrapper['store'].cc.getConsultTransferQueues).toHaveBeenCalledWith({}); }); it('should handle consultQueueCancelled event', () => { @@ -1059,30 +1099,37 @@ describe('storeEventsWrapper', () => { }); it('should fetch entry points successfully', async () => { - storeWrapper['store'].cc.getEntryPoints = jest.fn().mockResolvedValue(mockEntryPointsResponse); + storeWrapper['store'].cc.getConsultTransferEntryPoints = jest.fn().mockResolvedValue(mockEntryPointsResponse); const result = await storeWrapper.getEntryPoints({page: 0, pageSize: 25}); - expect(storeWrapper['store'].cc.getEntryPoints).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.getConsultTransferEntryPoints).toHaveBeenCalledWith({ + page: 0, + pageSize: 25, + }); expect(result).toEqual(mockEntryPointsResponse); }); it('should handle error while fetching entry points', async () => { - storeWrapper['store'].cc.getEntryPoints = jest.fn().mockRejectedValue(new Error('ep error')); + storeWrapper['store'].currentTask = null; + storeWrapper['store'].cc.getConsultTransferEntryPoints = jest.fn().mockRejectedValue(new Error('ep error')); await expect(storeWrapper.getEntryPoints({page: 0, pageSize: 25})).rejects.toThrow('ep error'); }); it('should fetch address book entries successfully', async () => { storeWrapper['store'].isAddressBookEnabled = true; - jest.spyOn(storeWrapper['store'].cc.addressBook, 'getEntries').mockResolvedValue(mockAddressBookEntriesResponse); + storeWrapper['store'].cc.addressBook.getEntries = jest.fn().mockResolvedValue(mockAddressBookEntriesResponse); const result = await storeWrapper.getAddressBookEntries({page: 0, pageSize: 25}); - expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({page: 0, pageSize: 25}); + expect(storeWrapper['store'].cc.addressBook.getEntries).toHaveBeenCalledWith({ + page: 0, + pageSize: 25, + }); expect(result).toEqual(mockAddressBookEntriesResponse); }); it('should handle error while fetching address book entries', async () => { storeWrapper['store'].isAddressBookEnabled = true; - jest.spyOn(storeWrapper['store'].cc.addressBook, 'getEntries').mockRejectedValue(new Error('ab error')); + storeWrapper['store'].cc.addressBook.getEntries = jest.fn().mockRejectedValue(new Error('ab error')); await expect(storeWrapper.getAddressBookEntries({page: 0, pageSize: 25})).rejects.toThrow('ab error'); }); diff --git a/packages/contact-center/task/ai-docs/task-spec.md b/packages/contact-center/task/ai-docs/task-spec.md index 060f6e128..f70160edb 100644 --- a/packages/contact-center/task/ai-docs/task-spec.md +++ b/packages/contact-center/task/ai-docs/task-spec.md @@ -392,7 +392,7 @@ The six widget shells are siblings that each bind to exactly one hook and one pr - **UC-3 Hold / resume active call (CallControl):** Agent clicks Hold → `toggleHold(true)` → `currentTask.hold()` → `TASK_HOLD` → hold timer starts via `useHoldTimer`, `onHoldResume({isHeld:true})`. Evidence: `src/helper.ts`, `src/Utils/useHoldTimer.ts`, `tests/helper.ts` (hold/resume cases). UI flow: Hold button toggles to Resume; "Hold" elapsed timer shown. - **UC-4 Toggle recording (CallControl):** Agent clicks record toggle → `toggleRecording()` → `pauseRecording()`/`resumeRecording()` → `TASK_RECORDING_PAUSED/RESUMED` → `isRecording` flips, `onRecordingToggle`. Evidence: `src/helper.ts`, `tests/helper.ts` (recording cases). - **UC-5 Mute / unmute (CallControl):** Agent clicks mute → `toggleMute()` (gated by `controlVisibility.muteUnmute`) → `currentTask.toggleMute()` → `store.setIsMuted`, `onToggleMute`. Evidence: `src/helper.ts`, `tests/helper.ts` ("should successfully toggle mute…", "rapid toggleMute"). UI flow: shows error-safe revert on failure. -- **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Evidence: `src/helper.ts`, `tests/helper.ts` (consult/transfer/conference cases), `tests/utils/task-util.ts` (EP-DN end-button cases). UI flow: consult controls (switch/merge/end) appear; consult timer label `Consult Requested`/`Consulting`/`Consult on Hold`. +- **UC-6 Consult an agent/queue/EP-DN then transfer or conference (CallControl):** Agent opens consult modal → `consultCall(dest,type,allow)` → on completion `consultTransfer()` (or `transferConference` in conference) or `endConsultCall()`. Consult/Transfer popover tab visibility follows Agent Desktop: `accessQueue`/`accessEntryPoint`/`accessBuddyTeam` from store plus interaction direction (`contactDirection`, `outdialTransferToQueueEnabled`) for queue tab gating. Evidence: `src/helper.ts`, `packages/contact-center/cc-components/.../consult-transfer-tab.utils.ts`, `task/src/CallControl/index.tsx`. - **UC-7 Blind transfer (CallControl):** Agent picks destination → `transferCall(to,type)` → `currentTask.transfer(...)`; failure re-thrown to the modal. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call transferCall successfully"). - **UC-8 Wrap up a call (CallControl):** Call ends → wrap-up codes shown → Agent selects code → `wrapupCall(reason, auxCodeId)` → `currentTask.wrapup` → next task promoted to `currentTask`, agent → ENGAGED. Evidence: `src/helper.ts`, `tests/helper.ts` ("should call wrapupCall"). Auto-wrap-up: countdown shown with Cancel. UI flow: wrap-up dropdown + optional auto-wrap-up countdown. - **UC-9 Place an outbound call (OutdialCall):** Agent enters number, selects ANI → dial → `startOutdial(destination, origin)`; empty number alerts and aborts; disabled while a telephony task is active. Evidence: `src/helper.ts` (`useOutdialCall`), `tests/OutdialCall/index.tsx`. UI flow: dialpad with validation, ANI dropdown, address book toggled by `isAddressBookEnabled`. diff --git a/packages/contact-center/task/src/CallControl/index.tsx b/packages/contact-center/task/src/CallControl/index.tsx index 443072076..c9ef6870a 100644 --- a/packages/contact-center/task/src/CallControl/index.tsx +++ b/packages/contact-center/task/src/CallControl/index.tsx @@ -7,6 +7,29 @@ import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlComponent} from '@webex/cc-components'; import {isUnacceptedCampaignPreview} from '../Utils/task-util'; +import {ITask} from '@webex/contact-center'; + +type ConsultTransferInteractionContext = { + contactDirectionType?: string; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; +}; + +const buildConsultTransferInteractionContext = (currentTask?: ITask): ConsultTransferInteractionContext => { + const interaction = currentTask?.data?.interaction as + | { + contactDirection?: {type?: string}; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; + } + | undefined; + + return { + contactDirectionType: interaction?.contactDirection?.type, + outdialTransferToQueueEnabled: interaction?.outdialTransferToQueueEnabled, + mediaType: interaction?.mediaType, + }; +}; const CallControlInternal: React.FunctionComponent = observer( ({onHoldResume, onEnd, onWrapUp, onRecordingToggle, onToggleMute, consultTransferOptions, conferenceEnabled}) => { @@ -17,6 +40,9 @@ const CallControlInternal: React.FunctionComponent = observer( consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, isMuted, agentId, acceptedCampaignIds, @@ -48,6 +74,10 @@ const CallControlInternal: React.FunctionComponent = observer( consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, + interactionContext: buildConsultTransferInteractionContext(currentTask), logger, consultTransferOptions, }; diff --git a/packages/contact-center/task/src/CallControlCAD/index.tsx b/packages/contact-center/task/src/CallControlCAD/index.tsx index 1426d19da..ab27c18a4 100644 --- a/packages/contact-center/task/src/CallControlCAD/index.tsx +++ b/packages/contact-center/task/src/CallControlCAD/index.tsx @@ -7,6 +7,29 @@ import {useCallControl} from '../helper'; import {CallControlProps} from '../task.types'; import {CallControlCADComponent} from '@webex/cc-components'; import {isUnacceptedCampaignPreview} from '../Utils/task-util'; +import {ITask} from '@webex/contact-center'; + +type ConsultTransferInteractionContext = { + contactDirectionType?: string; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; +}; + +const buildConsultTransferInteractionContext = (currentTask?: ITask): ConsultTransferInteractionContext => { + const interaction = currentTask?.data?.interaction as + | { + contactDirection?: {type?: string}; + outdialTransferToQueueEnabled?: boolean; + mediaType?: string; + } + | undefined; + + return { + contactDirectionType: interaction?.contactDirection?.type, + outdialTransferToQueueEnabled: interaction?.outdialTransferToQueueEnabled, + mediaType: interaction?.mediaType, + }; +}; const CallControlCADInternal: React.FunctionComponent = observer( ({ @@ -27,6 +50,9 @@ const CallControlCADInternal: React.FunctionComponent = observ consultStartTimeStamp, callControlAudio, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, isMuted, agentId, acceptedCampaignIds, @@ -57,6 +83,10 @@ const CallControlCADInternal: React.FunctionComponent = observ callControlClassName, callControlConsultClassName, allowConsultToQueue, + accessQueue, + accessEntryPoint, + accessBuddyTeam, + interactionContext: buildConsultTransferInteractionContext(currentTask), logger, consultTransferOptions, }; diff --git a/packages/contact-center/task/src/helper.ts b/packages/contact-center/task/src/helper.ts index 2405aa814..43ff6057b 100644 --- a/packages/contact-center/task/src/helper.ts +++ b/packages/contact-center/task/src/helper.ts @@ -575,22 +575,25 @@ export const useCallControl = (props: useCallControlProps) => { } }, [currentTask, agentId, extractConsultingAgent]); - const loadBuddyAgents = useCallback(async () => { - try { - setLoadingBuddyAgents(true); - const agents = await store.getBuddyAgents(); - logger.info(`Loaded ${agents.length} buddy agents`, {module: 'helper.ts', method: 'loadBuddyAgents'}); - setBuddyAgents(agents); - } catch (error) { - logger?.error(`CC-Widgets: Task: Error loading buddy agents - ${error.message || error}`, { - module: 'useCallControl', - method: 'loadBuddyAgents', - }); - setBuddyAgents([]); - } finally { - setLoadingBuddyAgents(false); - } - }, [logger]); + const loadBuddyAgents = useCallback( + async (action: 'Consult' | 'Transfer' = 'Consult') => { + try { + setLoadingBuddyAgents(true); + const agents = await store.getBuddyAgents(action); + logger.info(`Loaded ${agents.length} buddy agents`, {module: 'helper.ts', method: 'loadBuddyAgents'}); + setBuddyAgents(agents); + } catch (error) { + logger?.error(`CC-Widgets: Task: Error loading buddy agents - ${error.message || error}`, { + module: 'useCallControl', + method: 'loadBuddyAgents', + }); + setBuddyAgents([]); + } finally { + setLoadingBuddyAgents(false); + } + }, + [logger] + ); const getAddressBookEntries = useCallback( async ({page, pageSize, search}: PaginatedListParams) => { @@ -625,8 +628,7 @@ export const useCallControl = (props: useCallControlProps) => { const getQueuesFetcher = useCallback( async ({page, pageSize, search}: PaginatedListParams) => { try { - const mediaType = currentTask?.data?.interaction?.mediaType; - return await store.getQueues(mediaType, {page, pageSize, search}); + return await store.getQueues({page, pageSize, search}); } catch (error) { logger?.error(`CC-Widgets: Task: Error fetching queues (paginated) - ${error.message || error}`, { module: 'useCallControl', @@ -635,7 +637,7 @@ export const useCallControl = (props: useCallControlProps) => { return {data: [], meta: {page: 0, totalPages: 0}}; } }, - [logger, currentTask] + [logger] ); const holdCallback = () => { @@ -1304,12 +1306,14 @@ export const useOutdialCall = (props: useOutdialCallProps) => { return; } - // Only pass origin if it's defined and not empty - const outdialArgs = origin ? [destination, origin] : [destination]; + const outdialPromise = origin ? cc.startOutdial(destination, origin) : cc.startOutdial(destination); - cc.startOutdial(...outdialArgs) - .then((response) => { - logger.info('Outdial call started', response); + outdialPromise + .then(() => { + logger.info('Outdial call started', { + module: 'widget-OutdialCall#helper.ts', + method: 'startOutdial', + }); }) .catch((error: Error) => { logger.error(`${error}`, { diff --git a/packages/contact-center/task/tests/call-control-recording.tsx b/packages/contact-center/task/tests/call-control-recording.tsx index 9b5ea11ee..f27df0810 100644 --- a/packages/contact-center/task/tests/call-control-recording.tsx +++ b/packages/contact-center/task/tests/call-control-recording.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {render, screen, act} from '@testing-library/react'; import '@testing-library/jest-dom'; import {EventEmitter} from 'events'; -import store, {TASK_EVENTS, IContactCenter} from '@webex/cc-store'; +import store, {TASK_EVENTS, IContactCenter, ITask} from '@webex/cc-store'; import {mockTask, mockCC, createEnabledMainTaskUIControls} from '@webex/test-fixtures'; import {CallControl} from '../src/CallControl'; @@ -76,8 +76,8 @@ const promoteTask = (task: FakeTask) => { store.store.agentId = AGENT_ID; // Registers the store's own task listeners (refreshTaskList on recording // pause/resume, etc.) exactly as production does. - store.handleIncomingTask(task); - store.setCurrentTask(task); + store.handleIncomingTask(task as unknown as ITask); + store.setCurrentTask(task as unknown as ITask); }; /** What the SDK does when the ContactRecordingPaused websocket event arrives. */ @@ -104,7 +104,6 @@ describe('CallControl recording pause/resume state', () => { info: jest.fn(), warn: jest.fn(), error: jest.fn(), - debug: jest.fn(), trace: jest.fn(), }; }); diff --git a/packages/contact-center/task/tests/helper.ts b/packages/contact-center/task/tests/helper.ts index 85c82eefc..423c02737 100644 --- a/packages/contact-center/task/tests/helper.ts +++ b/packages/contact-center/task/tests/helper.ts @@ -2400,9 +2400,10 @@ describe('useCallControl', () => { }) ); await act(async () => { - await result.current.loadBuddyAgents(); + await result.current.loadBuddyAgents('Transfer'); }); expect(result.current.buddyAgents).toEqual(mockAgents); + expect(getBuddyAgentsSpy).toHaveBeenCalledWith('Transfer'); getBuddyAgentsSpy.mockRestore(); }); @@ -3538,7 +3539,7 @@ describe('useCallControl', () => { it('should get queues via getQueuesFetcher', async () => { const getQueuesResponse: Awaited> = { data: mockQueueDetails, - meta: {page: 0, pageSize: mockQueueDetails.length, total: mockQueueDetails.length, totalPages: 1}, + meta: {page: 0, pageSize: mockQueueDetails.length, totalRecords: mockQueueDetails.length, totalPages: 1}, }; const getQueuesSpy = jest.spyOn(store, 'getQueues').mockResolvedValue(getQueuesResponse); @@ -3598,7 +3599,7 @@ describe('useCallControl', () => { it('should get queues via getQueuesFetcher (paginated)', async () => { const mockResponse: Awaited> = { data: [mockQueueDetails[0]], - meta: {page: 0, pageSize: 25, total: 1, totalPages: 1}, + meta: {page: 0, pageSize: 25, totalRecords: 1, totalPages: 1}, }; jest.spyOn(store, 'getQueues').mockResolvedValue(mockResponse); @@ -5696,7 +5697,10 @@ describe('useOutdialCall', () => { }); expect(mockOutdialCallProps.startOutdial).toHaveBeenCalledWith(destination); - expect(logger.info).toHaveBeenCalledWith('Outdial call started', 'Success'); + expect(logger.info).toHaveBeenCalledWith('Outdial call started', { + module: 'widget-OutdialCall#helper.ts', + method: 'startOutdial', + }); }); it('should successfully start an outdial call with origin', async () => { @@ -5713,7 +5717,10 @@ describe('useOutdialCall', () => { }); expect(mockOutdialCallProps.startOutdial).toHaveBeenCalledWith(destination, origin); - expect(logger.info).toHaveBeenCalledWith('Outdial call started', 'Success'); + expect(logger.info).toHaveBeenCalledWith('Outdial call started', { + module: 'widget-OutdialCall#helper.ts', + method: 'startOutdial', + }); }); it('should show alert when destination is empty or only contains spaces', async () => { diff --git a/packages/contact-center/test-fixtures/src/fixtures.ts b/packages/contact-center/test-fixtures/src/fixtures.ts index e095af87f..8453b0e2a 100644 --- a/packages/contact-center/test-fixtures/src/fixtures.ts +++ b/packages/contact-center/test-fixtures/src/fixtures.ts @@ -619,10 +619,13 @@ const mockCC: IContactCenter = { getBuddyAgents: jest.fn().mockResolvedValue(mockAgents), getQueues: jest.fn().mockResolvedValue(mockQueuesResponse), getEntryPoints: jest.fn().mockResolvedValue(mockEntryPointsResponse), + getConsultTransferQueues: jest.fn().mockResolvedValue(mockQueuesResponse), + getConsultTransferEntryPoints: jest.fn().mockResolvedValue(mockEntryPointsResponse), addressBook: mockAddressBook, setAgentState: jest.fn().mockResolvedValue({}), getOutdialAniEntries: jest.fn().mockResolvedValue({entries: []}), getAccessToken: jest.fn().mockResolvedValue('mock-access-token'), + startOutdial: jest.fn().mockResolvedValue({}), acceptPreviewContact: jest.fn().mockResolvedValue({}), skipPreviewContact: jest.fn().mockResolvedValue({}), removePreviewContact: jest.fn().mockResolvedValue({}),