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 24 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
1 change: 1 addition & 0 deletions docs/docs/feature-library/00_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ The **Flex Project Template** comes with a set of features enabled by default wi
| [Omni Channel Management](omni-channel-capacity-management) | _method for mixing chat and voice channels_ |
| [Queues Stats Metrics](queues-stats-metrics) | _add custom metrics columns to the Queues View_ |
| [SIP Support](sip-support) | _adds call control functionality when using a non-WebRTC phone_ |
| [Worker Details](worker-details) | _view or update Worker Attributes_ |

</TabItem>
<TabItem value="experimental" label="Experimental features">
Expand Down
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 Tab 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.
25 changes: 22 additions & 3 deletions flex-config/ui_attributes.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,20 @@
"enabled": true,
"enable_notes": true,
"require_disposition": false,
"global_dispositions": ["Resolved", "Not Resolved", "Follow-up Required", "Escalation", "Wrong Department"],
"global_dispositions": [
"Resolved",
"Not Resolved",
"Follow-up Required",
"Escalation",
"Wrong Department"
],
"per_queue": {
"exampleQueueSid": {
"require_disposition": true,
"dispositions": ["Promotional Sale", "Renewal"]
"dispositions": [
"Promotional Sale",
"Renewal"
]
}
}
},
Expand Down Expand Up @@ -343,6 +352,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 All @@ -352,4 +371,4 @@
}
}
}
}
}
4 changes: 2 additions & 2 deletions plugin-flex-ts-template-v2/package.json
dremin marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "plugin-flex-ts-template-v2",
"version": "0.0.0",
"version": "0.0.3",
"private": true,
"scripts": {
"test:watch": "jest --watch",
Expand Down Expand Up @@ -76,4 +76,4 @@
"jest-junit": {
"outputDirectory": "./test-results"
}
}
}
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.ChannelCapacity,
dremin marked this conversation as resolved.
Show resolved Hide resolved
} as TabbedContentFragmentProps);
};
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="horizontal tabs">
<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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as Flex from '@twilio/flex-ui';
import { Box } from '@twilio-paste/core/Box';
import React from 'react';

import { FlexComponent } from '../../../../types/feature-loader';
import WorkerCanvasTabs from '../../custom-components/WorkerCanvasTabs/WorkerCanvasTabs';

export const componentName = FlexComponent.WorkerCanvas;
export const componentHook = function addWorkerCanvasTabs(flex: typeof Flex, _manager: Flex.Manager) {
// Remove Agent Details header
flex.WorkerCanvas.Content.remove('profile-title');
// Remove Skills Caption and Workerskills
flex.WorkerCanvas.Content.remove('skills-title');
flex.WorkerCanvas.Content.remove('skills');

flex.WorkerCanvas.Content.addWrapper((OriginalComponent) => (originalProps) => {
// preserve the fragments from the WorkerCanvas
const fragments = flex.WorkerCanvas.Content.fragments
.concat([])
.toSorted((a, b) => (a.props.sortOrder || 10000) - (b.props.sortOrder || 9999));

// remove the fragments from the WorkerCanvas to prevent vertical rendering
fragments.forEach((fragment) => {
if (fragment?.props?.children && (fragment.props.children as React.ReactElement).key) {
const key = (fragment.props.children as React.ReactElement).key as string;
flex.WorkerCanvas.Content.remove(key);
}
});

return (
<>
<Box>
<OriginalComponent {...originalProps} />
<WorkerCanvasTabs key="worker-canvas-tabs" worker={originalProps.worker} fragments={fragments} />
</Box>
</>
);
});
};
dremin marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const pasteElementHook = {
WORKER_CANVAS_TAB: {
fontSize: '12px',
},
} as { [key: string]: any };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"PSWorkerCanvasTabSkills": "Habilidades"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import esMX from './es-mx.json';

// Export the template names as an enum for better maintainability when accessing them elsewhere
export enum StringTemplates {
WorkerCanvasTabSkills = 'PSWorkerCanvasTabSkills',
}

export const stringHook = () => ({
'en-US': {
[StringTemplates.WorkerCanvasTabSkills]: 'Skills',
},
'es-MX': esMX,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FeatureDefinition } from '../../types/feature-loader';
import { isFeatureEnabled } from './config';
// @ts-ignore
import hooks from './flex-hooks/**/*.*';

export const register = (): FeatureDefinition => {
if (!isFeatureEnabled()) return {};
return { name: 'worker-canvas-tabs', hooks: typeof hooks === 'undefined' ? [] : hooks };
};
Loading
Loading