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

feat: Add pagination & tooltips to agent's dropdown on forwarding modal #30629

Merged
merged 23 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7a7aa0e
users/agent endpoint now public for forwarding
KevLehman Oct 12, 2023
782180c
Create rotten-dryers-allow.md
KevLehman Oct 12, 2023
589b3df
onlyavailable
KevLehman Oct 12, 2023
aae452c
test
KevLehman Oct 12, 2023
34d8387
good permission
KevLehman Oct 12, 2023
4ccfbe4
new filters
KevLehman Oct 13, 2023
4b27bfc
feat: added new props to AutoCompleteAgent
aleksandernsilva Oct 13, 2023
75e52fd
fix: changed UserAutoComplete to AutoCompleteAgent in ForwardChatModal
aleksandernsilva Oct 13, 2023
2e26338
feat: added tooltip to 'forward to user' field
aleksandernsilva Oct 13, 2023
399ee5d
chore: adjusted e2e tests
aleksandernsilva Oct 16, 2023
19e3452
Update apps/meteor/app/livechat/server/api/lib/users.ts
KevLehman Oct 17, 2023
892efe5
feat: added tooltip to department field on agent edit page
aleksandernsilva Oct 19, 2023
f134553
feat: changed display name formatting
aleksandernsilva Oct 20, 2023
545c091
feat: improved placeholder
aleksandernsilva Oct 20, 2023
3f4db55
chore: adjusting expressions file
aleksandernsilva Oct 20, 2023
e8f30cb
Revert "feat: added tooltip to department field on agent edit page"
aleksandernsilva Oct 20, 2023
64cc881
cr
KevLehman Oct 20, 2023
971fd49
Update rotten-dryers-allow.md
KevLehman Oct 20, 2023
9bb4388
Update rotten-dryers-allow.md
KevLehman Oct 20, 2023
ff7e377
Update .changeset/rotten-dryers-allow.md
sampaiodiego Oct 20, 2023
99d412d
chore: adjusted e2e tests
aleksandernsilva Oct 20, 2023
70983ef
chore: fixed e2e tests
aleksandernsilva Oct 20, 2023
13d9270
Merge branch 'develop' into chore/agents-endpoint-public
casalsgh Oct 23, 2023
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
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,
KevLehman marked this conversation as resolved.
Show resolved Hide resolved
},
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
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 @@ -4575,6 +4575,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,6 +77,7 @@ 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.poHomeOmnichannel.content.inputModalAgentForwardComment.type('any_comment');
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"]');
}

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
Loading