Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Worker Canvas Tabs for Skills, Channel Capacity & Worker Attributes #381

Merged
merged 29 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4249e2c
Initial code for tabs
ldvlgr Oct 9, 2023
1b464df
Add Skills tab, strings
ldvlgr Oct 10, 2023
3427a16
Merge branch 'main' into worker-canvas-tabs
ldvlgr Oct 10, 2023
10fc448
Update Worker attributes feature
ldvlgr Oct 11, 2023
2b5760e
Adding update-worker-attributes operation
ldvlgr Oct 12, 2023
48dfc17
UI and config updates
ldvlgr Oct 12, 2023
dc942bd
Merge branch 'main' into worker-canvas-tabs
ldvlgr Oct 12, 2023
bbd03fe
Refactor imports, add strings
ldvlgr Oct 12, 2023
1f78332
Merge main
ldvlgr Oct 16, 2023
816e63f
Refactor
ldvlgr Oct 16, 2023
7c979ac
Additional attributes
ldvlgr Oct 17, 2023
b5e30a9
Update styling
ldvlgr Oct 17, 2023
453c3f8
Add more options
ldvlgr Oct 18, 2023
7940d8c
Update components
ldvlgr Oct 18, 2023
8b4e9bc
Add template strings
ldvlgr Oct 19, 2023
37267ba
Simplified
ldvlgr Oct 19, 2023
28596c9
docs
ldvlgr Oct 19, 2023
12f4640
Configurable attributes
ldvlgr Oct 20, 2023
ff1f7aa
Add boolean attributes
ldvlgr Oct 22, 2023
ebeaee0
Update docs and config
ldvlgr Oct 23, 2023
b4e2470
Doc update
ldvlgr Oct 23, 2023
157fd09
Remove worker_canvas_tabs config. Enable always
ldvlgr Oct 25, 2023
172dca3
Merging latest main
ldvlgr Oct 25, 2023
bf7e728
Revising implementation for worker canvas tabs (#443)
jared-hunter Nov 30, 2023
e84cf0a
Merge branch 'main' into worker-canvas-tabs
dremin Jan 23, 2024
57f6251
Address review comments
dremin Jan 23, 2024
d1b9fb8
Clean up boolean option styles, add readme
dremin Jan 23, 2024
741a3c6
Update docs
dremin Jan 23, 2024
81b13e9
Fix up defaults, fix skills overflow
dremin Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/feature-library/00_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ The **Flex Project Template** comes with a set of features enabled by default wi
| [Supervisor Complete Reservation](supervisor-complete-reservation) | _allows supervisor to remotely complete agent tasks_ |
| [Teams View Enhancements](teams-view-enhancements) | _adds optional columns (Team, Dept, Location, Skills) to the Workers Table. <br/> enable task highlighting based on task age_ |
| [Teams View Filters](teams-view-filters) | _adds additional filtering options to the supervisor teams view_ |
| [Worker Canvas Tabs](worker-canvas-tabs) | _consolidates the worker canvas sections into tabs_ |
| [Worker Details](worker-details) | _view or update worker attributes_ |

---

Expand Down
31 changes: 31 additions & 0 deletions docs/docs/feature-library/worker-canvas-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
sidebar_label: worker-canvas-tabs
title: worker-canvas-tabs
---

## Overview

This feature converts the sections within the worker canvas (such as the skills section) into selectable tabs. This improves the user experience by reducing the amount of scrolling required, especially with multiple sections present.

## How does it work?

This feature takes advantage of the `addWrapper` API in Flex UI, which allows wrapping a component (in this case, `WorkerCanvas`) with something custom. To add individual tabs within this wrapper, each `WorkerCanvas` component fragment is also wrapped with a Paste `Tab` component.

To specify the tab title to use for each component, when adding a component to `WorkerCanvas`, the `tabTitle` option can be specified with the desired string to display, for example:

```ts
Flex.WorkerCanvas.Content.add(<CapacityContainer key="worker-capacity-container" />, {
tabTitle: "Capacity",
});
```

If no title is provided, the component's unique `key` is used instead.

## Setup

This feature can be enabled via the `flex-config` attributes by setting the `worker_canvas_tabs` `enabled` flag to `true`.

## Flex User Experience

![WorkerDetails](/img/features/worker-canvas-tabs/worker-canvas-tabs.png)

48 changes: 48 additions & 0 deletions docs/docs/feature-library/worker-details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
sidebar_label: worker-details
title: worker-details
---

## Overview
This feature adds a section to the the Worker Canvas (Agent Activity & Skills) in the Teams View to allow Supervisors to edit worker attributes like `team`, `department`, other custom text attributes and boolean (true/false) attributes or worker settings. These boolean attributes can be used to configure settings (or permissions) for the worker which could be leveraged to enable or disable certain custom features in the PS Template.

## How does it work?

The Supervisor can select a worker to view or edit from the Teams view. The Worker Canvas side panel displays the Worker Skills and possibly one or more other features related to the worker (for example Supervisor Capacity) in different tabs. The Details tab shows the agent name and the attributes. The list of available **Teams** and **Departments** is read from the common configuration in the Admin UI.

As noted in the [Flex insights docs](https://www.twilio.com/docs/flex/developer/insights/enhance-integration#enhance-agent-data)

- The `team_id` attribute is required to display `team_name`.
- The `department_id` attribute is required to display `department_name`.

Due to these dependencies, this feature will simply set both the `team_id` and `team_name` to the same value.
Similarly it will set `department_id` and `department_name` to the same value.

The `text_attributes` property can be used to specify an array of worker attributes that should be updated using a Text Input box. For example, `text_attributes: ['location', 'manager']` provides two Text Input fields to allow supervisors to update these two worker attributes.

Use the `boolean_attributes` property to provide an array of boolean (true/false) attibutes which will be displayed as [Paste Switch components](https://paste.twilio.design/components/switch). These settings can then be enabled or disabled for each worker. At this time, none of the other features in the PS Template leverage any custom worker settings yet, but with this new capability and a setting like `boolean_attributes: ['auto_accept]` you could enhance the [Agent Automation](agent-automation.md) feature to enable auto accept per worker.

Saving changes to the worker details updates the Worker Attributes using a Twilio serverless function.

## Setup

This feature can be enabled via the `flex-config` attributes. Just set the `worker_details` `enabled` flag to `true` and set up the desired configuration.

Use the `text_attributes` property to specify an array of editable text input fields.

Use the `boolean_attrbutes` property to provide an array of boolean (true/false) attributes that can be enabled (or disabled) per worker.

```json
"worker_details": {
"enabled": false,
"edit_team": true,
"edit_department": true,
"text_attributes": [],
"boolean_attributes": []
}
```

## Flex User Experience

![WorkerDetails](/img/features/worker-details/WorkerDetails.png)

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 11 additions & 1 deletion flex-config/ui_attributes.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@
"columns": {
"calls": true,
"other_tasks": true,
"team": true,
"team": false,
"department": false,
"location": false,
"agent_skills": true,
Expand Down Expand Up @@ -360,6 +360,16 @@
"sip_support": {
"enabled": false
},
"worker_details": {
"enabled": true,
"edit_team": true,
"edit_department": true,
"text_attributes": [],
"boolean_attributes": []
},
"worker_canvas_tabs": {
"enabled": true
},
"datadog_log_integration": {
"enabled": false,
"log_level": "info",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ import AttributeViewerConfig from './types/ServiceConfiguration';
const { enabled = false, enabled_for_agents = false } =
(getFeatureFlags()?.features?.attribute_viewer as AttributeViewerConfig) || {};

const { enabled: workerCanvasTabsEnabled = false } = getFeatureFlags()?.features?.worker_canvas_tabs || {};

export const isFeatureEnabled = () => {
return enabled;
};

export const isEnabledForAgents = () => {
return enabled_for_agents;
};

export const isWorkerCanvasTabsEnabled = () => {
return workerCanvasTabsEnabled;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CodeBlock } from '@twilio-paste/core/code-block';

import { SectionHeader } from './WorkerAttributes.Styles';
import { StringTemplates } from '../../flex-hooks/strings';
import { isWorkerCanvasTabsEnabled } from '../../config';

interface Props {
worker?: IWorker;
Expand All @@ -11,9 +12,11 @@ interface Props {
const WorkerAttributes = ({ worker }: Props) => {
return (
<>
<SectionHeader>
<Template source={templates[StringTemplates.WorkerAttributesHeader]} />
</SectionHeader>
{isWorkerCanvasTabsEnabled() ? null : (
<SectionHeader>
<Template source={templates[StringTemplates.WorkerAttributesHeader]} />
</SectionHeader>
)}
<CodeBlock variant="multi-line" language="json" code={JSON.stringify(worker?.attributes, null, 2)} />
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import * as Flex from '@twilio/flex-ui';
import { ContentFragmentProps } from '@twilio/flex-ui';

import WorkerAttributes from '../../custom-components/WorkerAttributes';
import { FlexComponent } from '../../../../types/feature-loader';
import { StringTemplates } from '../strings';

interface TabbedContentFragmentProps extends ContentFragmentProps {
tabTitle: string;
}

export const componentName = FlexComponent.WorkerCanvas;
export const componentHook = function addAttributesToWorkerCanvas(flex: typeof Flex, _manager: Flex.Manager) {
flex.WorkerCanvas.Content.add(<WorkerAttributes key="worker-attributes" />, {
export const componentHook = function addAttributesToWorkerCanvas() {
Flex.WorkerCanvas.Content.add(<WorkerAttributes key="worker-attributes" />, {
tabTitle: StringTemplates.WorkerAttributesHeader,
sortOrder: 1000,
});
} as TabbedContentFragmentProps);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import SupervisorCapacityConfig from './types/ServiceConfiguration';

const { enabled = false, rules } = (getFeatureFlags()?.features?.supervisor_capacity as SupervisorCapacityConfig) || {};

const { enabled: workerCanvasTabsEnabled = false } = getFeatureFlags()?.features?.worker_canvas_tabs || {};

export const isFeatureEnabled = () => {
return enabled;
};

export const getRules = () => {
return rules;
};

export const isWorkerCanvasTabsEnabled = () => {
return workerCanvasTabsEnabled;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SectionHeader } from './CapacityContainerStyles';
import TaskRouterService, {
WorkerChannelCapacityResponse,
} from '../../../../utils/serverless/TaskRouter/TaskRouterService';
import { getRules } from '../../config';
import { getRules, isWorkerCanvasTabsEnabled } from '../../config';
import CapacityChannel from '../CapacityChannel';
import { StringTemplates } from '../../flex-hooks/strings';

Expand Down Expand Up @@ -131,9 +131,11 @@ export default function CapacityContainer(props: OwnProps) {

return (
<Stack orientation="vertical" spacing="space0">
<SectionHeader>
<Template source={templates[StringTemplates.ChannelCapacity]} />
</SectionHeader>
{isWorkerCanvasTabsEnabled() ? null : (
<SectionHeader>
<Template source={templates[StringTemplates.ChannelCapacity]} />
</SectionHeader>
)}
{workerChannels.length > 0 &&
workerChannels.map((workerChannel) => (
<CapacityChannel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import * as Flex from '@twilio/flex-ui';
import { ContentFragmentProps } from '@twilio/flex-ui';

import CapacityContainer from '../../custom-components/CapacityContainer';
import { FlexComponent } from '../../../../types/feature-loader';
import { StringTemplates } from '../strings';

interface TabbedContentFragmentProps extends ContentFragmentProps {
tabTitle: string;
}

export const componentName = FlexComponent.WorkerCanvas;
export const componentHook = function addCapacityToWorkerCanvas(flex: typeof Flex, _manager: Flex.Manager) {
flex.WorkerCanvas.Content.add(<CapacityContainer key="worker-capacity-container" />);
export const componentHook = function addCapacityToWorkerCanvas() {
Flex.WorkerCanvas.Content.add(<CapacityContainer key="worker-capacity-container" />, {
tabTitle: StringTemplates.Capacity,
} as TabbedContentFragmentProps);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"PSSupervisorCapacity": "Capacidad",
"PSSupervisorCapacityChannelCapacity": "Capacidad del canal",
"PSSupervisorCapacityMissingConfiguration": "Configuración faltante. Por favor notifique a su administrador del sistema.",
"PSSupervisorCapacityNoChannels": "No hay canales de trabajadores disponibles.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"PSSupervisorCapacity": "Capacidad",
"PSSupervisorCapacityChannelCapacity": "Capacidad de Canal",
"PSSupervisorCapacityMissingConfiguration": "Configuración faltante. Por favor, notifique a su administrador del sistema.",
"PSSupervisorCapacityNoChannels": "No hay canales de trabajadores disponibles.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import zhHans from './zh-hans.json';

// Export the template names as an enum for better maintainability when accessing them elsewhere
export enum StringTemplates {
Capacity = 'PSSupervisorCapacity',
ChannelCapacity = 'PSSupervisorCapacityChannelCapacity',
MissingConfiguration = 'PSSupervisorCapacityMissingConfiguration',
NoChannels = 'PSSupervisorCapacityNoChannels',
Expand All @@ -15,6 +16,7 @@ export enum StringTemplates {

export const stringHook = () => ({
'en-US': {
[StringTemplates.Capacity]: 'Capacity',
[StringTemplates.ChannelCapacity]: 'Channel Capacity',
[StringTemplates.MissingConfiguration]: 'Missing configuration. Please notify your system administrator.',
[StringTemplates.NoChannels]: 'No worker channels available.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"PSSupervisorCapacity": "Capacidade",
"PSSupervisorCapacityChannelCapacity": "Capacidade do canal",
"PSSupervisorCapacityMissingConfiguration": "Configuração ausente. Por favor, notifique o administrador do sistema.",
"PSSupervisorCapacityNoChannels": "Nenhum canal disponível.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"PSSupervisorCapacity": "ความจุ",
"PSSupervisorCapacityChannelCapacity": "ความจุของช่องสื่อสาร",
"PSSupervisorCapacityMissingConfiguration": "การกำหนดค่าหายไป โปรดแจ้งผู้ดูแลระบบของคุณ",
"PSSupervisorCapacityNoChannels": "ไม่มีช่องทำงานว่างใช้งานได้",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"PSSupervisorCapacity": "容量",
"PSSupervisorCapacityChannelCapacity": "通道容量",
"PSSupervisorCapacityMissingConfiguration": "缺少配置。请通知您的系统管理员。",
"PSSupervisorCapacityNoChannels": "没有可用的工作人员渠道。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ interface WorkerItem {
}
const activityConfig = getAgentActivityConfig();
const getSkills = (item: WorkerItem) => {
return item.worker.attributes.routing ? item.worker?.attributes?.routing?.skills?.join(', ') : '-';
if (!item.worker?.attributes?.routing?.skills) {
return '-';
}
const skillsStr = item.worker.attributes.routing.skills.join(', ');
return skillsStr.length > 100 ? `${skillsStr.substring(0, 100)}...` : skillsStr;
};

export const componentName = FlexComponent.TaskCanvasHeader;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getFeatureFlags } from '../../utils/configuration';

const { enabled = false } = getFeatureFlags()?.features?.worker_canvas_tabs || {};

export const isFeatureEnabled = () => {
return enabled;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Box } from '@twilio-paste/core/Box';
import { Tabs, Tab, TabList, TabPanel, TabPanels, useTabState } from '@twilio-paste/core/tabs';
import { useUID } from '@twilio-paste/core/uid-library';
import { IWorker, Template, templates, ContentFragmentProps, WorkerSkills, Actions } from '@twilio/flex-ui';
import React from 'react';

import { StringTemplates } from '../../flex-hooks/strings';

interface Props {
worker?: IWorker;
fragments: React.ReactElement<
ContentFragmentProps & {
children?: React.ReactNode;
},
string | React.JSXElementConstructor<any>
>[];
}

interface TabbedContentFragmentProps extends ContentFragmentProps {
tabTitle: string;
}

const hasTabTitle = (
fragment: React.ReactElement<
ContentFragmentProps & {
children?: React.ReactNode;
},
string | React.JSXElementConstructor<any>
>,
): boolean => {
const title = (fragment.props as TabbedContentFragmentProps)?.tabTitle;
return !(title === '' || title === undefined || title === null);
};

const handleClose = () => {
Actions.invokeAction('SelectWorkerInSupervisor', { worker: undefined });
Actions.invokeAction('SetComponentState', { name: 'SupervisorCanvasTabs', state: { selectedTabName: undefined } });
};

const WorkerCanvasTabs = ({ worker, fragments }: Props) => {
const randomId = useUID();
const { ...tabState } = useTabState();

return (
<Box height="100%" borderLeftStyle="solid" borderColor="colorBorderWeak" borderWidth="borderWidth10">
<Tabs selectedId={randomId} baseId="options" state={tabState}>
<TabList aria-label="Worker tabs" element="WORKER_CANVAS_TAB_LIST">
<Tab id={randomId} element="WORKER_CANVAS_TAB">
<Template source={templates[StringTemplates.WorkerCanvasTabSkills]} />
</Tab>
{fragments.map((fragment) => (
<Tab key={(fragment.props.children as React.ReactElement).key} element="WORKER_CANVAS_TAB">
{hasTabTitle(fragment) ? (
<Template source={templates[(fragment.props as TabbedContentFragmentProps).tabTitle]} />
) : (
(fragment.props.children as React.ReactElement).key
)}
</Tab>
))}
</TabList>
<TabPanels>
<TabPanel>{worker && <WorkerSkills key="skills" worker={worker} onClose={handleClose} />}</TabPanel>
{fragments.map((fragment) => (
<TabPanel key={(fragment.props.children as React.ReactElement).key}>
{React.createElement((fragment.props.children as any)?.type, { worker })}
</TabPanel>
))}
</TabPanels>
</Tabs>
</Box>
);
};

export default WorkerCanvasTabs;
Loading
Loading