Skip to content

Commit

Permalink
feat: Add pagination & tooltips to agent's dropdown on forwarding mod…
Browse files Browse the repository at this point in the history
…al (#30629)
  • Loading branch information
KevLehman authored Oct 23, 2023
1 parent c4325e8 commit a82d8c2
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 71 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-dryers-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Add pagination & tooltips to agent's dropdown on forwarding modal
13 changes: 8 additions & 5 deletions apps/meteor/app/livechat/imports/server/rest/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,15 @@ import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/f
import { findAgents, findManagers } from '../../../server/api/lib/users';
import { Livechat } from '../../../server/lib/Livechat';

const emptyStringArray: string[] = [];

API.v1.addRoute(
'livechat/users/:type',
{
authRequired: true,
permissionsRequired: {
GET: {
permissions: ['manage-livechat-agents'],
operation: 'hasAll',
},
POST: { permissions: ['view-livechat-manager'], operation: 'hasAll' },
'POST': ['view-livechat-manager'],
'*': emptyStringArray,
},
validateParams: {
GET: isLivechatUsersManagerGETProps,
Expand All @@ -39,9 +38,13 @@ API.v1.addRoute(
return API.v1.unauthorized();
}

const { onlyAvailable, excludeId, showIdleAgents } = this.queryParams;
return API.v1.success(
await findAgents({
text,
onlyAvailable,
excludeId,
showIdleAgents,
pagination: {
offset,
count,
Expand Down
39 changes: 35 additions & 4 deletions apps/meteor/app/livechat/server/api/lib/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ILivechatAgent, IRole } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { FilterOperators } from 'mongodb';

/**
* @param {IRole['_id']} role the role id
Expand All @@ -10,18 +11,39 @@ import { escapeRegExp } from '@rocket.chat/string-helpers';
async function findUsers({
role,
text,
onlyAvailable = false,
excludeId,
showIdleAgents = true,
pagination: { offset, count, sort },
}: {
role: IRole['_id'];
text?: string;
onlyAvailable?: boolean;
excludeId?: string;
showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise<{ users: ILivechatAgent[]; count: number; offset: number; total: number }> {
const query = {};
const query: FilterOperators<ILivechatAgent> = {};
const orConditions: FilterOperators<ILivechatAgent>['$or'] = [];
if (text) {
const filterReg = new RegExp(escapeRegExp(text), 'i');
Object.assign(query, {
$or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }],
});
orConditions.push({ $or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }] });
}

if (onlyAvailable) {
query.statusLivechat = 'available';
}

if (excludeId) {
query._id = { $ne: excludeId };
}

if (!showIdleAgents) {
orConditions.push({ $or: [{ status: { $exists: true, $ne: 'offline' }, roles: { $ne: 'bot' } }, { roles: 'bot' }] });
}

if (orConditions.length) {
query.$and = orConditions;
}

const [
Expand Down Expand Up @@ -52,14 +74,23 @@ async function findUsers({
}
export async function findAgents({
text,
onlyAvailable = false,
excludeId,
showIdleAgents = true,
pagination: { offset, count, sort },
}: {
text?: string;
onlyAvailable: boolean;
excludeId?: string;
showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise<ReturnType<typeof findUsers>> {
return findUsers({
role: 'livechat-agent',
text,
onlyAvailable,
excludeId,
showIdleAgents,
pagination: {
offset,
count,
Expand Down
30 changes: 14 additions & 16 deletions apps/meteor/client/components/AutoCompleteAgent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,53 +11,51 @@ type AutoCompleteAgentProps = {
value: string;
error?: string;
placeholder?: string;
onChange: (value: string) => void;
haveAll?: boolean;
haveNoAgentsSelectedOption?: boolean;
excludeId?: string;
showIdleAgents?: boolean;
onlyAvailable?: boolean;
withTitle?: boolean;
onChange: (value: string) => void;
};

const AutoCompleteAgent = ({
value,
error,
placeholder,
onChange,
haveAll = false,
haveNoAgentsSelectedOption = false,
excludeId,
showIdleAgents = false,
onlyAvailable = false,
withTitle = false,
onChange,
}: AutoCompleteAgentProps): ReactElement => {
const [agentsFilter, setAgentsFilter] = useState<string>('');

const debouncedAgentsFilter = useDebouncedValue(agentsFilter, 500);

const { itemsList: AgentsList, loadMoreItems: loadMoreAgents } = useAgentsList(
useMemo(
() => ({ text: debouncedAgentsFilter, haveAll, haveNoAgentsSelectedOption }),
[debouncedAgentsFilter, haveAll, haveNoAgentsSelectedOption],
() => ({ text: debouncedAgentsFilter, onlyAvailable, haveAll, haveNoAgentsSelectedOption, excludeId, showIdleAgents }),
[debouncedAgentsFilter, excludeId, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents],
),
);

const { phase: agentsPhase, itemCount: agentsTotal, items: agentsItems } = useRecordList(AgentsList);

const sortedByName = agentsItems.sort((a, b) => {
if (a.label > b.label) {
return 1;
}
if (a.label < b.label) {
return -1;
}

return 0;
});

return (
<PaginatedSelectFiltered
withTitle={withTitle}
value={value}
error={error}
placeholder={placeholder}
onChange={onChange}
flexShrink={0}
filter={agentsFilter}
setFilter={setAgentsFilter as (value: string | number | undefined) => void}
options={sortedByName}
options={agentsItems}
data-qa='autocomplete-agent'
endReached={
agentsPhase === AsyncStatePhase.LOADING ? (): void => undefined : (start): void => loadMoreAgents(start, Math.min(50, agentsTotal))
Expand Down
17 changes: 12 additions & 5 deletions apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ type AgentsListOptions = {
text: string;
haveAll: boolean;
haveNoAgentsSelectedOption: boolean;
excludeId?: string;
showIdleAgents?: boolean;
onlyAvailable?: boolean;
};

type AgentOption = { value: string; label: string; _updatedAt: Date; _id: string };
Expand All @@ -26,6 +29,7 @@ export const useAgentsList = (
const reload = useCallback(() => setItemsList(new RecordList<AgentOption>()), []);

const getAgents = useEndpoint('GET', '/v1/livechat/users/agent');
const { text, onlyAvailable = false, showIdleAgents = false, excludeId, haveAll, haveNoAgentsSelectedOption } = options;

useComponentDidUpdate(() => {
options && reload();
Expand All @@ -34,7 +38,10 @@ export const useAgentsList = (
const fetchData = useCallback(
async (start, end) => {
const { users: agents, total } = await getAgents({
...(options.text && { text: options.text }),
...(text && { text }),
...(excludeId && { excludeId }),
showIdleAgents,
onlyAvailable,
offset: start,
count: end + start,
sort: `{ "name": 1 }`,
Expand All @@ -43,22 +50,22 @@ export const useAgentsList = (
const items = agents.map<AgentOption>((agent) => {
const agentOption = {
_updatedAt: new Date(agent._updatedAt),
label: agent.username || agent._id,
label: `${agent.name || agent._id} (@${agent.username})`,
value: agent._id,
_id: agent._id,
};
return agentOption;
});

options.haveAll &&
haveAll &&
items.unshift({
label: t('All'),
value: 'all',
_updatedAt: new Date(),
_id: 'all',
});

options.haveNoAgentsSelectedOption &&
haveNoAgentsSelectedOption &&
items.unshift({
label: t('Empty_no_agent_selected'),
value: 'no-agent-selected',
Expand All @@ -71,7 +78,7 @@ export const useAgentsList = (
itemCount: total + 1,
};
},
[getAgents, options.haveAll, options.haveNoAgentsSelectedOption, options.text, t],
[excludeId, getAgents, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents, t, text],
);

const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { useForm } from 'react-hook-form';

import { useRecordList } from '../../../hooks/lists/useRecordList';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import UserAutoComplete from '../../UserAutoComplete';
import AutoCompleteAgent from '../../AutoCompleteAgent';
import { useDepartmentsList } from '../hooks/useDepartmentsList';

const ForwardChatModal = ({
Expand Down Expand Up @@ -53,28 +53,6 @@ const ForwardChatModal = ({
);
const { phase: departmentsPhase, items: departments, itemCount: departmentsTotal } = useRecordList(departmentsList);

const _id = { $ne: room.servedBy?._id };
const conditions = {
_id,
...(!idleAgentsAllowedForForwarding && {
$or: [
{
status: {
$exists: true,
$ne: 'offline',
},
roles: {
$ne: 'bot',
},
},
{
roles: 'bot',
},
],
}),
statusLivechat: 'available',
};

const endReached = useCallback(
(start) => {
if (departmentsPhase !== AsyncStatePhase.LOADING) {
Expand Down Expand Up @@ -134,13 +112,16 @@ const ForwardChatModal = ({
<Field>
<FieldLabel>{t('Forward_to_user')}</FieldLabel>
<FieldRow>
<UserAutoComplete
conditions={conditions}
placeholder={t('Username')}
<AutoCompleteAgent
withTitle
onlyAvailable
value={getValues().username}
excludeId={room.servedBy?._id}
showIdleAgents={idleAgentsAllowedForForwarding}
placeholder={t('Username_name_email')}
onChange={(value) => {
setValue('username', value);
}}
value={getValues().username}
/>
</FieldRow>
</Field>
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5453,6 +5453,7 @@
"Username_title": "Register username",
"Username_has_been_updated": "Username has been updated",
"Username_wants_to_start_otr_Do_you_want_to_accept": "{{username}} wants to start OTR. Do you want to accept?",
"Username_name_email": "Username, name or e-mail",
"Users": "Users",
"Users must use Two Factor Authentication": "Users must use Two Factor Authentication",
"Users_added": "The users have been added",
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -4581,6 +4581,7 @@
"Username_Placeholder": "Digite os nomes de usuário...",
"Username_title": "Cadastre um nome de usuário",
"Username_wants_to_start_otr_Do_you_want_to_accept": "{{username}} quer começar OTR. Você aceita?",
"Username_name_email": "Nome de usuário, nome ou e-mail",
"Users": "Usuários",
"Users must use Two Factor Authentication": "Os usuários devem usar autenticação de dois fatores",
"Users_added": "Os usuários foram adicionados",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ test.describe('omnichannel-transfer-to-another-agent', () => {
await agent2.poHomeOmnichannel.sidenav.switchStatus('offline');

await agent1.poHomeOmnichannel.content.btnForwardChat.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2');
await expect(agent1.page.locator('text=Empty')).toBeVisible();

Expand All @@ -76,8 +77,9 @@ test.describe('omnichannel-transfer-to-another-agent', () => {

await agent1.poHomeOmnichannel.sidenav.getSidebarItemByName(newVisitor.name).click();
await agent1.poHomeOmnichannel.content.btnForwardChat.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2');
await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2"').click();
await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2 (@user2)"').click();
await agent1.poHomeOmnichannel.content.inputModalAgentForwardComment.type('any_comment');
await agent1.poHomeOmnichannel.content.btnModalConfirm.click();
await expect(agent1.poHomeOmnichannel.toastSuccess).toBeVisible();
Expand Down
14 changes: 3 additions & 11 deletions apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class HomeContent {
}

get inputModalAgentUserName(): Locator {
return this.page.locator('#modal-root input:nth-child(1)');
return this.page.locator('#modal-root input[placeholder="Username, name or e-mail"]');
}

get inputModalAgentForwardComment(): Locator {
Expand Down Expand Up @@ -237,16 +237,8 @@ export class HomeContent {

async openLastThreadMessageMenu(): Promise<void> {
await this.page.locator('//main//aside >> [data-qa-type="message"]').last().hover();
await this.page
.locator('//main//aside >> [data-qa-type="message"]')
.last()
.locator('role=button[name="More"]')
.waitFor();
await this.page
.locator('//main//aside >> [data-qa-type="message"]')
.last()
.locator('role=button[name="More"]')
.click();
await this.page.locator('//main//aside >> [data-qa-type="message"]').last().locator('role=button[name="More"]').waitFor();
await this.page.locator('//main//aside >> [data-qa-type="message"]').last().locator('role=button[name="More"]').click();
}

async toggleAlsoSendThreadToChannel(isChecked: boolean): Promise<void> {
Expand Down
20 changes: 20 additions & 0 deletions apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,26 @@ describe('LIVECHAT - Agents', function () {
expect(agentRecentlyCreated?._id).to.be.equal(agent._id);
});
});
it('should return an array of available agents', async () => {
await updatePermission('edit-omnichannel-contact', ['admin']);
await updatePermission('transfer-livechat-guest', ['admin']);
await updatePermission('manage-livechat-agents', ['admin']);

await request
.get(api('livechat/users/agent'))
.set(credentials)
.expect('Content-Type', 'application/json')
.query({ onlyAvailable: true })
.expect(200)
.expect((res: Response) => {
expect(res.body).to.have.property('success', true);
expect(res.body.users).to.be.an('array');
expect(res.body).to.have.property('offset');
expect(res.body).to.have.property('total');
expect(res.body).to.have.property('count');
expect(res.body.users.every((u: { statusLivechat: string }) => u.statusLivechat === 'available')).to.be.true;
});
});
it('should return an array of managers', async () => {
await updatePermission('view-livechat-manager', ['admin']);
await updatePermission('manage-livechat-agents', ['admin']);
Expand Down
Loading

0 comments on commit a82d8c2

Please sign in to comment.