Skip to content

Commit

Permalink
Refactor custom directory components for reusability, override worker…
Browse files Browse the repository at this point in the history
… directory (#430)
  • Loading branch information
dremin authored Nov 17, 2023
1 parent 26d16ae commit dbeba42
Show file tree
Hide file tree
Showing 17 changed files with 561 additions and 382 deletions.
14 changes: 9 additions & 5 deletions docs/docs/feature-library/custom-transfer-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ sidebar_label: custom-transfer-directory
title: custom-transfer-directory
---

This feature enables the replacement of the queue transfer directory enabling the following behavior
This feature enables the replacement of the queue and worker transfer directories enabling the following behavior

- render different transfer icons for chat channel
- enable the use of real time data to
Expand All @@ -13,13 +13,13 @@ This feature enables the replacement of the queue transfer directory enabling th
- provide an improved starting point for augmenting queue transfer list with custom data (imagine the need to filter queues based on skills required to transfer to those queues)
- provide the ability to enforce queue filters by worker
- provide ability to enforce global queue filter to filter out system queues.
- external
- provide the option to filter out unavailable workers

It also enables the addition of an external directory, enabling the following behavior

- present a list of external transfer numbers
- each transfer number can independently be configured for warm or cold transfers
- validtion checks performed on transfer numbers with notifications of any validation failures
- validation checks performed on transfer numbers with notifications of any validation failures

# flex-user-experience

Expand All @@ -38,7 +38,11 @@ Enable the feature in the flex-config asset for your environment.
```javascript
"custom_transfer_directory": {
"enabled": true, // globally enable or disable the feature
"use_paste_search_icon": false, // use new paste icon or old legacy icon (recommended to use old icon if mixing with OOTB tabs for consistant look)
"use_paste_search_icon": false, // use new paste icon or old legacy icon (recommended to use old icon if mixing with OOTB tabs for consistent look)
"worker" : {
"enabled": true, // enable the custom worker tab
"show_only_available_workers": false
},
"queue" : {
"enabled": true, // enable the custom queue tab
"show_only_queues_with_available_workers": true,
Expand Down Expand Up @@ -70,4 +74,4 @@ worker.attributes : {

# how does it work?

The queue tab is replaced with the custom components using the Flex component framework. When the component is rendered, a queues list is loaded from the taskrouter sdk and cached. Then the insights client is used to load the real time stats for all the queues. The real time stats are appended to each queue retrieved in the insights client and then any filters are applied. Various events trigger a re-evaluation of the filtered list including queue updates (update, add or remove) or an entry into the search field
The queue and worker tabs are replaced with custom components using the Flex component framework. When the component is rendered, a list is loaded from the TaskRouter SDK and cached. Then the insights client is used to load the real time stats for all the queues. The real time stats are appended to each queue retrieved in the insights client and then any filters are applied. Various events trigger a re-evaluation of the filtered list including queue updates (update, add or remove) or an entry into the search field.
4 changes: 4 additions & 0 deletions flex-config/ui_attributes.common.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@
"custom_transfer_directory": {
"enabled": true,
"use_paste_search_icon": false,
"worker": {
"enabled": true,
"show_only_available_workers": false
},
"queue": {
"enabled": true,
"show_only_queues_with_available_workers": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Manager } from '@twilio/flex-ui';

import { getFeatureFlags } from '../../utils/configuration';
import { ExternalDirectoryEntry } from './types/ServiceConfiguration';
import { ExternalDirectoryEntry } from './types/DirectoryEntry';
import CustomTransferDirectoryConfig from './types/ServiceConfiguration';

const { enabled = false, use_paste_search_icon = false } = getFeatureFlags()?.features?.custom_transfer_directory || {};
const {
enabled = false,
use_paste_search_icon = false,
queue: queue_config,
worker: worker_config,
external_directory: external_directory_config,
} = (getFeatureFlags()?.features?.custom_transfer_directory as CustomTransferDirectoryConfig) || {};

const {
enabled: queueEnabled = false,
Expand All @@ -12,13 +19,15 @@ const {
enforce_queue_filter_from_worker_object = false,
enforce_global_exclude_filter = false,
global_exclude_filter = '',
} = getFeatureFlags()?.features?.custom_transfer_directory?.queue || {};
} = queue_config || {};

const { enabled: workerEnabled = false, show_only_available_workers = false } = worker_config || {};

const {
enabled: externalDirectoryEnabled = false,
skipPhoneNumberValidation = false,
directory = [] as Array<ExternalDirectoryEntry>,
} = getFeatureFlags()?.features?.custom_transfer_directory?.external_directory || {};
} = external_directory_config || {};

const {
enabled: conversation_transfer_enabled = false,
Expand Down Expand Up @@ -90,3 +99,11 @@ export const isVoiceXWTEnabled = () => {
export const shouldSkipPhoneNumberValidation = () => {
return skipPhoneNumberValidation;
};

export const isCustomWorkerTransferEnabled = (): boolean => {
return isFeatureEnabled() && workerEnabled;
};

export const showOnlyAvailableWorkers = (): boolean => {
return show_only_available_workers;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import * as React from 'react';
import { TaskHelper, ITask, templates } from '@twilio/flex-ui';
import { Box } from '@twilio-paste/core/box';
import { ButtonGroup } from '@twilio-paste/core/button-group';
import { Button } from '@twilio-paste/core/button';
import { Flex } from '@twilio-paste/core/flex';
import { Tooltip } from '@twilio-paste/core/tooltip';
import { Text } from '@twilio-paste/core/text';
import { AgentIcon } from '@twilio-paste/icons/esm/AgentIcon';
import { ProductContactCenterTeamsIcon } from '@twilio-paste/icons/esm/ProductContactCenterTeamsIcon';
import { ProductPhoneNumbersIcon } from '@twilio-paste/icons/esm/ProductPhoneNumbersIcon';
import { CallTransferIcon } from '@twilio-paste/icons/esm/CallTransferIcon';
import { CallOutgoingIcon } from '@twilio-paste/icons/esm/CallOutgoingIcon';
import { SendIcon } from '@twilio-paste/icons/esm/SendIcon';
import { ChatIcon } from '@twilio-paste/icons/esm/ChatIcon';

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

export interface DirectoryItemProps {
entry: DirectoryEntry;
task: ITask;
onTransferClick: (options: any) => void;
}

const DirectoryItem = (props: DirectoryItemProps) => {
const { entry, task, onTransferClick } = props;

const onWarmTransferClick = () => {
onTransferClick({ mode: 'WARM' });
};

const onColdTransferClick = () => {
onTransferClick({ mode: 'COLD' });
};

const renderIcon = (): React.JSX.Element => {
if (entry.icon) {
return entry.icon;
}

switch (entry.type) {
case 'number':
return <ProductPhoneNumbersIcon decorative={true} />;
case 'queue':
return <ProductContactCenterTeamsIcon decorative={true} />;
default:
return <AgentIcon decorative={true} />;
}
};

const renderLabel = (): React.JSX.Element => (
<Box key={`directory-item-label-${entry.type}-${entry.address}`} element="TRANSFER_DIR_COMMON_ROW_LABEL">
{entry.labelComponent || (
<Text as="div" className="Twilio" element="TRANSFER_DIR_COMMON_ROW_NAME">
{entry.label}
</Text>
)}
</Box>
);

return (
<Flex
element="TRANSFER_DIR_COMMON_HORIZONTAL_ROW_CONTAINER"
vertical={false}
vAlignContent="center"
key={`directory-item-container-${entry.type}-${entry.address}`}
>
<Box key={`directory-item-icon-${entry.type}-${entry.address}`} element="TRANSFER_DIR_COMMON_ROW_ICON">
{renderIcon()}
</Box>
{entry.tooltip ? (
<Tooltip
key={`directory-item-label-tooltip-${entry.type}-${entry.address}`}
element="TRANSFER_DIR_COMMON_TOOLTIP"
text={entry.tooltip}
>
{renderLabel()}
</Tooltip>
) : (
renderLabel()
)}

<ButtonGroup
element="TRANSFER_DIR_COMMON_ROW_BUTTONGROUP"
key={`directory-item-buttongroup-${entry.type}-${entry.address}`}
attached
>
{entry.warm_transfer_enabled ? (
<Tooltip
key={`directory-item-buttons-warm-transfer-tooltip-${entry.type}-${entry.address}`}
element="TRANSFER_DIR_COMMON_TOOLTIP"
text={templates[StringTemplates.WarmTransfer]()}
>
<Button
element="TRANSFER_DIR_COMMON_ROW_BUTTON"
key={`directory-item-warm-transfer-button-${entry.type}-${entry.address}`}
variant="secondary_icon"
size="circle"
onClick={onWarmTransferClick}
>
{task && TaskHelper.isChatBasedTask(task) ? (
<ChatIcon
key={`directory-item-warm-transfer-icon-${entry.type}-${entry.address}`}
decorative={false}
title=""
/>
) : (
<CallTransferIcon
key={`directory-item-warm-transfer-icon-${entry.type}-${entry.address}`}
decorative={false}
title=""
/>
)}
</Button>
</Tooltip>
) : (
<div></div>
)}
{entry.cold_transfer_enabled ? (
<Tooltip
key={`directory-item-buttons-cold-transfer-tooltip-${entry.type}-${entry.address}`}
element="TRANSFER_DIR_COMMON_TOOLTIP"
text={templates[StringTemplates.ColdTransfer]()}
>
<Button
element="TRANSFER_DIR_COMMON_ROW_BUTTON"
key={`directory-item-warm-transfer-button-${entry.type}-${entry.address}`}
variant="secondary_icon"
size="circle"
onClick={onColdTransferClick}
>
{task && TaskHelper.isChatBasedTask(task) ? (
<SendIcon
key={`directory-item-cold-transfer-icon-${entry.type}-${entry.address}`}
decorative={false}
title=""
/>
) : (
<CallOutgoingIcon
key={`directory-item-cold-transfer-icon-${entry.type}-${entry.address}`}
decorative={false}
title=""
/>
)}
</Button>
</Tooltip>
) : (
<div></div>
)}
</ButtonGroup>
</Flex>
);
};

export default DirectoryItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Alert } from '@twilio-paste/core/alert';
import { Flex } from '@twilio-paste/core/flex';
import { Spinner } from '@twilio-paste/core/spinner';
import { withTaskContext, ITask, Actions, Template, templates } from '@twilio/flex-ui';
import { useState, useRef, useEffect } from 'react';
import debounce from 'lodash/debounce';

import DirectoryItem from './DirectoryItem';
import SearchBox from './SearchBox';
import { StringTemplates } from '../flex-hooks/strings/CustomTransferDirectory';
import { DirectoryEntry } from '../types/DirectoryEntry';

export interface TransferClickPayload {
mode: 'WARM' | 'COLD';
}

export interface OwnProps {
task: ITask;
entries: Array<DirectoryEntry>;
isLoading: boolean;
noEntriesMessage?: string;
onTransferClick: (entry: DirectoryEntry, transferOptions: TransferClickPayload) => void;
}

const DirectoryTab = (props: OwnProps) => {
const [filteredDirectory, setFilteredDirectory] = useState([] as Array<DirectoryEntry>);

const searchInputRef = useRef<HTMLInputElement>(null);

// takes the input in the search box and applies it to filter the entry list
const onQueueSearchInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// eslint-disable-next-line no-eq-null, eqeqeq
if (event.target != null) {
filterDirectoryDebounce();
}
};

// function to filter the entries and trigger a re-render
const filterDirectory = () => {
const tempDir = props.entries.filter((entry) => {
const searchString = searchInputRef.current?.value.toLocaleLowerCase() || '';
return entry.label.toLocaleLowerCase().includes(searchString);
});

setFilteredDirectory(tempDir);
};

const filterDirectoryDebounce = debounce(filterDirectory, 500, { maxWait: 1000 });

const onTransferEntryClick = (entry: DirectoryEntry) => async (transferOptions: TransferClickPayload) => {
props.onTransferClick(entry, transferOptions);
Actions.invokeAction('HideDirectory');
};

useEffect(() => {
filterDirectory();
}, [props.entries]);

return (
<Flex key="external-directory-tab-list" vertical wrap={false} grow={1} shrink={1}>
<SearchBox key="key-tab-search-box" onInputChange={onQueueSearchInputChange} inputRef={searchInputRef} />
<Flex key="external-tab-results" vertical element="TRANSFER_DIR_COMMON_ROWS_CONTAINER">
{props.isLoading && (
<Flex hAlignContent="center">
<Spinner decorative size="sizeIcon90" />
</Flex>
)}
{filteredDirectory.length === 0 && !props.isLoading ? (
<Alert variant="neutral">
<Template
source={
props.noEntriesMessage && !searchInputRef.current?.value
? props.noEntriesMessage
: templates[StringTemplates.NoItemsFound]
}
/>
</Alert>
) : (
Array.from(filteredDirectory).map((entry: DirectoryEntry) => {
return (
<DirectoryItem
task={props.task}
entry={entry}
key={`dir-item-${entry.type}-${entry.address}`}
onTransferClick={onTransferEntryClick(entry)}
/>
);
})
)}
</Flex>
</Flex>
);
};

export default withTaskContext(DirectoryTab);
Loading

0 comments on commit dbeba42

Please sign in to comment.