feat(Omnichannel): Queued Status option Current Chats (#32800)

Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com>
pull/32671/head^2
Martin Schoeler 1 year ago committed by GitHub
parent be1f1a21b3
commit 264d7d5496
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/weak-tigers-suffer.md
  2. 3
      apps/meteor/app/livechat/imports/server/rest/rooms.ts
  3. 3
      apps/meteor/app/livechat/server/api/lib/rooms.ts
  4. 9
      apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx
  5. 1
      apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx
  6. 12
      apps/meteor/server/models/raw/LivechatRooms.ts
  7. 72
      apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts
  8. 2
      packages/model-typings/src/models/ILivechatRoomsModel.ts
  9. 7
      packages/rest-typings/src/v1/omnichannel.ts

@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---
Added the ability to filter chats by `queued` on the Current Chats Omnichannel page

@ -30,7 +30,7 @@ API.v1.addRoute(
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort, fields } = await this.parseJsonQuery();
const { agents, departmentId, open, tags, roomName, onhold } = this.queryParams;
const { agents, departmentId, open, tags, roomName, onhold, queued } = this.queryParams;
const { createdAt, customFields, closedAt } = this.queryParams;
const createdAtParam = validateDateParams('createdAt', createdAt);
@ -69,6 +69,7 @@ API.v1.addRoute(
tags,
customFields: parsedCf,
onhold,
queued,
options: { offset, count, sort, fields },
}),
);

@ -14,6 +14,7 @@ export async function findRooms({
tags,
customFields,
onhold,
queued,
options: { offset, count, fields, sort },
}: {
agents?: Array<string>;
@ -31,6 +32,7 @@ export async function findRooms({
tags?: Array<string>;
customFields?: Record<string, string>;
onhold?: string | boolean;
queued?: string | boolean;
options: { offset: number; count: number; fields: Record<string, number>; sort: Record<string, number> };
}): Promise<PaginatedResult<{ rooms: Array<IOmnichannelRoom> }>> {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
@ -44,6 +46,7 @@ export async function findRooms({
tags,
customFields,
onhold: ['t', 'true', '1'].includes(`${onhold}`),
queued: ['t', 'true', '1'].includes(`${queued}`),
options: {
sort: sort || { ts: -1 },
offset,

@ -54,6 +54,7 @@ type CurrentChatQuery = {
customFields?: string;
sort: string;
count?: number;
queued?: boolean;
};
type useQueryType = (
@ -95,8 +96,9 @@ const currentChatQuery: useQueryType = (
}
if (status !== 'all') {
query.open = status === 'opened' || status === 'onhold';
query.open = status === 'opened' || status === 'onhold' || status === 'queued';
query.onhold = status === 'onhold';
query.queued = status === 'queued';
}
if (servedBy && servedBy !== 'all') {
query.agents = [servedBy];
@ -170,8 +172,9 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s
const renderRow = useCallback(
(room) => {
const { _id, fname, servedBy, ts, lm, department, open, onHold, priorityWeight } = room;
const getStatusText = (open: boolean, onHold: boolean): string => {
const getStatusText = (open: boolean, onHold: boolean, servedBy: boolean): string => {
if (!open) return t('Closed');
if (open && !servedBy) return t('Queued');
return onHold ? t('On_Hold_Chats') : t('Room_Status_Open');
};
@ -198,7 +201,7 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s
{moment(lm).format('L LTS')}
</GenericTableCell>
<GenericTableCell withTruncatedText data-qa='current-chats-cell-status'>
<RoomActivityIcon room={room} /> {getStatusText(open, onHold)}
<RoomActivityIcon room={room} /> {getStatusText(open, onHold, !!servedBy?.username)}
</GenericTableCell>
{canRemoveClosedChats && !open && <RemoveChatButton _id={_id} />}
</GenericTableRow>

@ -30,6 +30,7 @@ const FilterByText = ({ setFilter, reload, customFields, setCustomFields, hasCus
['closed', t('Closed')],
['opened', t('Room_Status_Open')],
['onhold', t('On_Hold_Chats')],
['queued', t('Queued')],
];
const [guest, setGuest] = useLocalStorage('guest', '');

@ -1211,6 +1211,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
visitorId,
roomIds,
onhold,
queued,
options = {},
extraQuery = {},
}: {
@ -1226,6 +1227,7 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
visitorId?: string;
roomIds?: string[];
onhold?: boolean;
queued?: boolean;
options?: { offset?: number; count?: number; sort?: { [k: string]: SortDirection } };
extraQuery?: Filter<IOmnichannelRoom>;
}) {
@ -1242,6 +1244,10 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
...(visitorId && visitorId !== 'undefined' && { 'v._id': visitorId }),
};
if (open) {
query.servedBy = { $exists: true };
}
if (createdAt) {
query.ts = {};
if (createdAt.start) {
@ -1280,6 +1286,12 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
};
}
if (queued) {
query.servedBy = { $exists: false };
query.open = true;
query.onHold = { $ne: true };
}
return this.findPaginated(query, {
sort: options.sort || { name: 1 },
skip: options.offset,

@ -35,6 +35,7 @@ import {
fetchMessages,
deleteVisitor,
makeAgentUnavailable,
sendAgentMessage,
} from '../../../data/livechat/rooms';
import { saveTags } from '../../../data/livechat/tags';
import type { DummyResponse } from '../../../data/livechat/utils';
@ -341,6 +342,77 @@ describe('LIVECHAT - rooms', () => {
expect(body.rooms.some((room: IOmnichannelRoom) => !!room.closedAt)).to.be.true;
expect(body.rooms.some((room: IOmnichannelRoom) => room.open)).to.be.true;
});
it('should return queued rooms when `queued` param is passed', async () => {
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { body } = await request.get(api('livechat/rooms')).query({ queued: true }).set(credentials).expect(200);
expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true;
expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.not.undefined;
});
it('should return queued rooms when `queued` and `open` params are passed', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { body } = await request.get(api('livechat/rooms')).query({ queued: true, open: true }).set(credentials).expect(200);
expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true;
expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.not.undefined;
});
it('should return open rooms when `open` is param is passed. Open rooms should not include queued conversations', async () => {
const visitor = await createVisitor();
const room = await createLivechatRoom(visitor.token);
const { room: room2 } = await startANewLivechatRoomAndTakeIt();
const { body } = await request.get(api('livechat/rooms')).query({ open: true }).set(credentials).expect(200);
expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room2._id)).to.be.not.undefined;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.undefined;
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
});
(IS_EE ? describe : describe.skip)('Queued and OnHold chats', () => {
before(async () => {
await updateSetting('Livechat_allow_manual_on_hold', true);
await updateSetting('Livechat_Routing_Method', 'Manual_Selection');
});
after(async () => {
await updateSetting('Livechat_Routing_Method', 'Auto_Selection');
await updateSetting('Livechat_allow_manual_on_hold', false);
});
it('should not return on hold rooms along with queued rooms when `queued` is true and `onHold` is true', async () => {
const { room } = await startANewLivechatRoomAndTakeIt();
await sendAgentMessage(room._id);
const response = await request
.post(api('livechat/room.onHold'))
.set(credentials)
.send({
roomId: room._id,
})
.expect(200);
expect(response.body.success).to.be.true;
const visitor = await createVisitor();
const room2 = await createLivechatRoom(visitor.token);
const { body } = await request.get(api('livechat/rooms')).query({ queued: true, onhold: true }).set(credentials).expect(200);
expect(body.rooms.every((room: IOmnichannelRoom) => room.open)).to.be.true;
expect(body.rooms.every((room: IOmnichannelRoom) => !room.servedBy)).to.be.true;
expect(body.rooms.every((room: IOmnichannelRoom) => !room.onHold)).to.be.true;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room._id)).to.be.undefined;
expect(body.rooms.find((froom: IOmnichannelRoom) => froom._id === room2._id)).to.be.not.undefined;
});
});
(IS_EE ? it : it.skip)('should return only rooms with the given department', async () => {
const { department } = await createDepartmentWithAnOnlineAgent();

@ -28,6 +28,7 @@ type WithOptions = {
options?: any;
};
// TODO: Fix types of model
export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
getQueueMetrics(params: { departmentId: any; agentId: any; includeOfflineAgents: any; options?: any }): any;
@ -96,6 +97,7 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
visitorId?: any;
roomIds?: any;
onhold: any;
queued: any;
options?: any;
extraQuery?: any;
}): FindPaginated<FindCursor<IOmnichannelRoom>>;

@ -2552,6 +2552,7 @@ export type GETLivechatRoomsParams = PaginatedRequest<{
departmentId?: string;
open?: string | boolean;
onhold?: string | boolean;
queued?: string | boolean;
tags?: string[];
}>;
@ -2617,6 +2618,12 @@ const GETLivechatRoomsParamsSchema = {
{ type: 'boolean', nullable: true },
],
},
queued: {
anyOf: [
{ type: 'string', nullable: true },
{ type: 'boolean', nullable: true },
],
},
tags: {
type: 'array',
items: {

Loading…
Cancel
Save