Repository: webex/widgets
Pull request #445: fix(user-state): resolved-ui-gaps-in-user-state-dropdown-component
Author: login: akulakum
Labels: name: released on @ccwidgets
name: validated
description: Indicates that the PR is ready for actions
Created: 2025-05-07T06:37:31Z | Merged: 2025-05-08T12:16:16Z

**Description:** This PR addresses UI gaps in the User State dropdown component, aligning it with the expected design and behavior. The following enhancements and fixes have been implemented:

Jira Link: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6272

Vidcast Link: https://app.vidcast.io/share/177a960c-c028-4078-be6b-0c90467b5df3

**Fixes & Enhancements**

**Selected icon styling:**
Updated styles so that the icon for the selected state appears bolder than icons in the dropdown list.

**Timer formatting improvement:**
Timer now hides the hour section unless the elapsed time crosses the 1-hour mark (e.g., 00:59 → 01:00:00).

**Dropdown sorting logic:**
Ensured that "Available" always appears at the top, with all other states sorted alphabetically.

**RONA and ENGAGED behavior:**
When the current state is RONA or ENGAGED, the dropdown highlights the previously selected user-selectable state to maintain visual clarity and selection consistency.

**Screenshots:**

**Timer formatting improvement:**

<img width="676" alt="LessThanAnHour" src="https://github.com/user-attachments/assets/044530be-eed6-408e-8cea-b0e46af3c6f3" />

<img width="677" alt="exeedsOneHour" src="https://github.com/user-attachments/assets/12d7b6c5-c8e7-41c7-9861-86cc1fe9114e" />


**Dropdown sorting logic:**

<img width="677" alt="exeedsOneHour" src="https://github.com/user-attachments/assets/14db8b3b-d11d-44cc-84e4-d19f7b26936d" />


**Selected icon styling:**

<img width="656" alt="Screenshot 2025-05-07 at 11 29 26 AM" src="https://github.com/user-attachments/assets/b1f2363b-000a-40e6-9993-3d9e3e71951f" />

<img width="656" alt="Screenshot 2025-05-07 at 11 31 01 AM" src="https://github.com/user-attachments/assets/0f31e568-5181-43f8-960a-8b4f5928e20a" />

<img width="656" alt="Screenshot 2025-05-07 at 11 30 10 AM" src="https://github.com/user-attachments/assets/a7d79193-ee37-41c7-ae6f-de1c0d19cc3f" />

<img width="656" alt="Screenshot 2025-05-07 at 11 29 35 AM" src="https://github.com/user-attachments/assets/fd67fdf1-dad5-4396-947d-03d6eca52e7b" />





Diff:
diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.scss b/packages/contact-center/cc-components/src/components/UserState/user-state.scss
index 4517a3ade..3a7986366 100644
--- a/packages/contact-center/cc-components/src/components/UserState/user-state.scss
+++ b/packages/contact-center/cc-components/src/components/UserState/user-state.scss
@@ -27,6 +27,13 @@
 
 .state-name {
   color: var(--mds-color-theme-text-primary-normal);
+  &.rona {
+    color: var(--mds-color-theme-text-error-normal);
+  }
+
+  &.custom {
+    color: var(--mds-color-theme-text-warning-normal);
+  }
 }
 
 .elapsedTime-disabled {
@@ -96,11 +103,45 @@
     }
   }
 
+  &.available {
+    width: 100%;
+    --mds-color-theme-background-primary-active: var(--mds-color-theme-background-alert-success-normal);
+
+    .md-select-dropdown-input {
+      background-color: var(--mds-color-theme-background-alert-success-normal);
+      border: 1px solid var(--mds-color-theme-outline-join-normal);
+    }
+  }
+
   .md-select-popover ul li[data-key^="hide-"] {
     display: none;
   }
+
+.item-container.selected {
+  &.available {
+    .state-icon {
+      --mdc-icon-fill-color: var(--mds-color-theme-outline-join-normal);
+    }
+
+    .state-name {
+      color: var(--mds-color-theme-outline-join-normal);
+    }
+  }
+
+  &.idle {
+    .state-icon {
+      --mdc-icon-fill-color: var(--mds-color-theme-text-primary-normal);
+    }
+
+    .state-name {
+      color: var(--mds-color-theme-text-primary-normal);
+    }
+  }
 }
 
+}
+
+
 .select-arrow-icon {
   position: absolute;
   right: 1rem; /* Adjusted to leave space for the arrow icon */
diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx
index c64e72b4d..2095d2393 100644
--- a/packages/contact-center/cc-components/src/components/UserState/user-state.tsx
+++ b/packages/contact-center/cc-components/src/components/UserState/user-state.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
+import React, {useMemo} from 'react';
 
-import {IUserState} from './user-state.types';
+import {IUserState, AgentUserState} from './user-state.types';
 import {formatTime} from '../../utils';
 
 import './user-state.scss';
@@ -19,6 +19,10 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
     customState,
   } = props;
 
+  const previousSelectableState = useMemo(() => {
+    return idleCodes.find((code) => code.id !== AgentUserState.RONA && code.id !== AgentUserState.Engaged)?.id ?? '0';
+  }, [idleCodes]);
+
   let selectedKey;
   if (customState) {
     selectedKey = `hide-${customState.developerName}`;
@@ -31,15 +35,15 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
     : [];
 
   for (const item of idleCodes) {
-    if (item.name === 'RONA' && item.id === currentState) {
+    if (item.name === AgentUserState.RONA && item.id === currentState) {
       selectedKey = `hide-${item.id}`;
     }
-    if (item.name === 'RONA' && item.id !== currentState) {
+    if (item.name === AgentUserState.RONA && item.id !== currentState) {
       continue; // Skip RONA unless it matches the current state
     }
     items.push({
       ...item,
-      id: item.name === 'RONA' ? `hide-${item.id}` : item.id,
+      id: item.name === AgentUserState.RONA ? `hide-${item.id}` : item.id,
     });
   }
 
@@ -51,7 +55,7 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
       return '';
     }
     for (const item of idleCodes) {
-      if (item.id === currentState && item.name === 'RONA') {
+      if (item.id === currentState && item.name === AgentUserState.RONA) {
         return 'rona';
       }
     }
@@ -64,14 +68,20 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
     }
     switch (item.id) {
       case '0':
-        return {class: '', iconName: 'active-presence-small-filled'};
-      case item.name === 'RONA' && item.id:
+        return {class: 'available', iconName: 'active-presence-small-filled'};
+      case item.name === AgentUserState.RONA && item.id:
         return {class: 'rona', iconName: 'dnd-presence-filled'};
       default:
         return {class: 'idle', iconName: 'recents-presence-filled'};
     }
   };
 
+  // Sorts the dropdown items by keeping 'Available' at the top and sorting the rest alphabetically by name
+  const sortedItems = [
+    ...items.filter((item) => item.name === AgentUserState.Available),
+    ...items.filter((item) => item.name !== AgentUserState.Available).sort((a, b) => a.name.localeCompare(b.name)),
+  ];
+
   return (
     <div className="user-state-container">
       <SelectNext
@@ -84,19 +94,24 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
         }}
         showBorder
         selectedKey={selectedKey}
-        items={items}
+        items={sortedItems}
         className={`state-select ${getDropdownClass()}`}
       >
         {(item) => {
+          const isRonaOrEngaged = [AgentUserState.RONA, AgentUserState.Engaged].includes(
+            idleCodes.find((code) => code.id === currentState)?.name || ''
+          );
+          const shouldHighlight = currentState === item.id || (isRonaOrEngaged && item.id === previousSelectableState);
+
           return (
             <Item key={item.id} textValue={item.name}>
-              <div className="item-container">
+              <div className={`item-container ${shouldHighlight ? `selected ${getIconStyle(item).class}` : ''}`}>
                 <Icon
                   name={getIconStyle(item).iconName}
                   title=""
                   className={`state-icon ${getIconStyle(item).class}`}
                 />
-                <Text className="state-name" tagName={'small'}>
+                <Text className={`state-name ${getIconStyle(item).class}`} tagName={'small'}>
                   {item.name}
                 </Text>
               </div>
diff --git a/packages/contact-center/cc-components/src/components/UserState/user-state.types.ts b/packages/contact-center/cc-components/src/components/UserState/user-state.types.ts
index 68e70261c..07e687f5f 100644
--- a/packages/contact-center/cc-components/src/components/UserState/user-state.types.ts
+++ b/packages/contact-center/cc-components/src/components/UserState/user-state.types.ts
@@ -61,3 +61,9 @@ export interface IUserState {
    */
   onStateChange: (state: string) => void;
 }
+
+export enum AgentUserState {
+  Available = 'Available',
+  RONA = 'RONA',
+  Engaged = 'ENGAGED',
+}
diff --git a/packages/contact-center/cc-components/src/utils/index.ts b/packages/contact-center/cc-components/src/utils/index.ts
index b2d679c38..dedc08d95 100644
--- a/packages/contact-center/cc-components/src/utils/index.ts
+++ b/packages/contact-center/cc-components/src/utils/index.ts
@@ -2,7 +2,13 @@ export const formatTime = (time: number): string => {
   const hours = Math.floor(time / 3600);
   const minutes = Math.floor((time % 3600) / 60);
   const seconds = time % 60;
-  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds
-    .toString()
-    .padStart(2, '0')}`;
+  // Display full format with hours if time is 1 hour or more
+  if (hours > 0) {
+    return `${hours.toString().padStart(2, '0')}:${minutes
+      .toString()
+      .padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
+  }
+
+  // Less than 1 hour: display mm:ss with two-digit padding for minutes
+  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
 };


Reviews:
user: login: rsarika
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T10:59:41Z
user: login: rsarika
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:12:30Z
user: login: rsarika
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:04:00Z
user: login: Shreyas281299
body: Mostly look good, just 1 question
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:01:21Z
user: login: akulakum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:06:46Z
user: login: Shreyas281299
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:29:45Z

Review comments:
diff_hunk: @@ -19,6 +19,10 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
     customState,
   } = props;
 
+  const previousSelectableState = useMemo(() => {
+    return idleCodes.find((code) => code.id !== AgentUserState.RONA && code.id !== AgentUserState.Engaged)?.id ?? '0';
+  }, [idleCodes]);
path: packages/contact-center/cc-components/src/components/UserState/user-state.tsx
user: login: rsarika
body: this will always return available right? cant we hardcode with AgentUserState.Available in shouldHighlight code.
created_at: 2025-05-07T10:55:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -19,6 +19,10 @@ const UserStateComponent: React.FunctionComponent<IUserState> = (props) => {
     customState,
   } = props;
 
+  const previousSelectableState = useMemo(() => {
+    return idleCodes.find((code) => code.id !== AgentUserState.RONA && code.id !== AgentUserState.Engaged)?.id ?? '0';
+  }, [idleCodes]);
path: packages/contact-center/cc-components/src/components/UserState/user-state.tsx
user: login: rsarika
body: got it after discussing 1-1
created_at: 2025-05-07T13:12:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,3 +61,9 @@ export interface IUserState {
    */
   onStateChange: (state: string) => void;
 }
+
+export enum AgentUserState {
+  Available = 'Available',
+  RONA = 'RONA',
+  Engaged = 'ENGAGED',
+}
path: packages/contact-center/cc-components/src/components/UserState/user-state.types.ts
user: login: Shreyas281299
body: Why only these 3? 
created_at: 2025-05-08T11:00:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -61,3 +61,9 @@ export interface IUserState {
    */
   onStateChange: (state: string) => void;
 }
+
+export enum AgentUserState {
+  Available = 'Available',
+  RONA = 'RONA',
+  Engaged = 'ENGAGED',
+}
path: packages/contact-center/cc-components/src/components/UserState/user-state.types.ts
user: login: akulakum
body: These three states are currently hardcoded in the component, so I created an enum for better reusability and maintainability. If additional states are required in the future, I’ll include them in the next PR.
created_at: 2025-05-08T11:06:46Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-07T06:37:37Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=webex/widgets&utm_content=445):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: aws-amplify-us-east-2[bot]
created_at: 2025-05-07T06:42:11Z
body: This pull request is automatically being deployed by Amplify Hosting ([learn more](https://aws.amazon.com/amplify/hosting/)).<br/><br/>Access this pull request here: https://pr-445.d1b38q61t1z947.amplifyapp.com
author_association: NONE
user: login: rsarika
created_at: 2025-05-07T11:00:04Z
body: Are we not writing UT's as part of UI tickets?
author_association: CONTRIBUTOR
user: login: rsarika
created_at: 2025-05-07T11:03:04Z
body: Custom State for Engaged should also show cumulative timer (Feedback from EPIC ADoRE)

this is  missing
author_association: CONTRIBUTOR
user: login: cmullenx
created_at: 2025-05-07T12:37:04Z
body: Could you please update the component to use ```Select``` from ```'@momentum-design/components/dist/react'```, this would resolve an issue we are seeing downstream due to the Icon issue in MRV2
author_association: CONTRIBUTOR
user: login: rsarika
created_at: 2025-05-08T04:48:48Z
body: > Could you please update the component to use `Select` from `'@momentum-design/components/dist/react'`, this would resolve an issue we are seeing downstream due to the Icon issue in MRV2

we have a separate ticket for this https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6468
author_association: CONTRIBUTOR
user: login: akulakum
created_at: 2025-05-08T04:55:21Z
body: > Are we not writing UT's as part of UI tickets?



> Could you please update the component to use `Select` from `'@momentum-design/components/dist/react'`, this would resolve an issue we are seeing downstream due to the Icon issue in MRV2

We have a separate ticket for this. We will take care of that fix in the following JIRA ticket: https://jira-eng-sjc12.cisco.com/jira/browse/CAI-6468
author_association: CONTRIBUTOR
user: login: akulakum
created_at: 2025-05-08T04:56:17Z
body: > Custom State for Engaged should also show cumulative timer (Feedback from EPIC ADoRE)
> 
> this is missing

We won't do this as agreed with UX team
author_association: CONTRIBUTOR
user: login: sreenara
created_at: 2025-05-08T12:22:26Z
body: :tada: This PR is included in version 1.28.0-ccwidgets.52 :tada:

The release is available on [GitHub release](https://github.com/webex/widgets/releases/tag/v1.28.0-ccwidgets.52)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: CONTRIBUTOR