diff --git a/.changeset/weak-tigers-suffer.md b/.changeset/weak-tigers-suffer.md new file mode 100644 index 000000000000..91748a43c677 --- /dev/null +++ b/.changeset/weak-tigers-suffer.md @@ -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 diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts index f7d5ddb314c9..f80ed61a131e 100644 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts @@ -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 }, }), ); diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index b130e5c2c73a..26449dce3963 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -14,6 +14,7 @@ export async function findRooms({ tags, customFields, onhold, + queued, options: { offset, count, fields, sort }, }: { agents?: Array; @@ -31,6 +32,7 @@ export async function findRooms({ tags?: Array; customFields?: Record; onhold?: string | boolean; + queued?: string | boolean; options: { offset: number; count: number; fields: Record; sort: Record }; }): Promise }>> { 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, diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index 95fb9a54c3ce..c439cc838874 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -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')} - {getStatusText(open, onHold)} + {getStatusText(open, onHold, !!servedBy?.username)} {canRemoveClosedChats && !open && } diff --git a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx index 131aa06f0a70..cda579387ea1 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/FilterByText.tsx @@ -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', ''); diff --git a/apps/meteor/server/models/raw/LivechatRooms.ts b/apps/meteor/server/models/raw/LivechatRooms.ts index d1b704e024b9..648af95ed180 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/server/models/raw/LivechatRooms.ts @@ -1211,6 +1211,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive visitorId, roomIds, onhold, + queued, options = {}, extraQuery = {}, }: { @@ -1226,6 +1227,7 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive visitorId?: string; roomIds?: string[]; onhold?: boolean; + queued?: boolean; options?: { offset?: number; count?: number; sort?: { [k: string]: SortDirection } }; extraQuery?: Filter; }) { @@ -1242,6 +1244,10 @@ export class LivechatRoomsRaw extends BaseRaw 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 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, diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 5c881b530d08..23f6d35d2acd 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -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(); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 6384b325f991..d4da1d7d8159 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -28,6 +28,7 @@ type WithOptions = { options?: any; }; +// TODO: Fix types of model export interface ILivechatRoomsModel extends IBaseModel { getQueueMetrics(params: { departmentId: any; agentId: any; includeOfflineAgents: any; options?: any }): any; @@ -96,6 +97,7 @@ export interface ILivechatRoomsModel extends IBaseModel { visitorId?: any; roomIds?: any; onhold: any; + queued: any; options?: any; extraQuery?: any; }): FindPaginated>; diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index 3dad53cbe8d2..ec53304605fc 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -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: {