From cf96021d069bba175d10057baa381c25421542df Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Tue, 11 Oct 2022 17:53:55 +0530 Subject: [PATCH 1/3] Chore: Add tests for omni-jobs & add more context in system messages for jobs --- apps/meteor/app/livechat/lib/messageTypes.ts | 3 +- .../app/livechat/server/lib/Livechat.js | 4 +- .../app/models/server/models/LivechatRooms.js | 39 ------- .../actions/OmnichannelLivechatToggle.tsx | 1 + .../server/hooks/afterOnHold.ts | 14 +-- .../server/lib/AutoCloseOnHoldScheduler.ts | 22 +++- .../server/lib/AutoTransferChatScheduler.ts | 16 ++- .../rocketchat-i18n/i18n/en.i18n.json | 3 + .../meteor/server/models/raw/LivechatRooms.js | 41 ++++++- ...nichannel-auto-onhold-chat-closing.spec.ts | 104 ++++++++++++++++++ ...nnel-auto-transfer-unanswered-chat.spec.ts | 87 +++++++++++++++ .../page-objects/fragments/home-content.ts | 12 ++ .../page-objects/fragments/home-sidenav.ts | 22 ++++ .../src/models/ILivechatRoomsModel.ts | 9 +- 14 files changed, 317 insertions(+), 60 deletions(-) create mode 100644 apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts create mode 100644 apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index 65872433a25ff..e1c0f18f42a23 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -48,8 +48,9 @@ MessageTypes.registerType({ ...(comment && { comment }), }), queue: (): string => - TAPi18n.__('Livechat_transfer_return_to_the_queue', { + TAPi18n.__(`Livechat_transfer_return_to_the_queue${commentLabel}`, { from, + ...(comment && { comment }), }), }; return { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index f07d4610f494a..c993c7235ac25 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -750,7 +750,7 @@ export const Livechat = { return RoutingManager.transferRoom(room, guest, transferData); }, - returnRoomAsInquiry(rid, departmentId) { + returnRoomAsInquiry(rid, departmentId, comment = '') { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = LivechatRooms.findOneById(rid); if (!room) { @@ -790,7 +790,7 @@ export const Livechat = { const transferredBy = normalizeTransferredByData(user, room); Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy }; + const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...(comment && { comment }) }; try { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index a2c5239daf36b..94dabb5d4d3af 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -832,45 +832,6 @@ export class LivechatRooms extends Base { return this.update(query, update); } - setAutoTransferredAtById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferredAt: new Date(), - }, - }; - - return this.update(query, update); - } - - setAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $set: { - autoTransferOngoing: true, - }, - }; - - return this.update(query, update); - } - - unsetAutoTransferOngoingById(roomId) { - const query = { - _id: roomId, - }; - const update = { - $unset: { - autoTransferOngoing: 1, - }, - }; - - return this.update(query, update); - } - changeVisitorByRoomId(roomId, { _id, username, token }) { const query = { _id: roomId, diff --git a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx index 6a1aee2149eb8..e68676f07cb84 100644 --- a/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx +++ b/apps/meteor/client/sidebar/sections/actions/OmnichannelLivechatToggle.tsx @@ -21,6 +21,7 @@ export const OmnichannelLivechatToggle = (): ReactElement => { return ( => { const { _id: rid } = room; @@ -23,7 +20,12 @@ const handleAfterOnHold = async (room: any = {}): Promise => { } cbLogger.debug(`Scheduling room ${rid} to be closed in ${autoCloseOnHoldChatTimeout} seconds`); - await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, customCloseMessage); + const closeComment = + settings.get('Livechat_auto_close_on_hold_chats_custom_message') || + TAPi18n.__('Closed_automatically_because_chat_was_onhold_for_seconds', { + onHoldTime: autoCloseOnHoldChatTimeout, + }); + await AutoCloseOnHoldScheduler.scheduleRoom(room._id, autoCloseOnHoldChatTimeout, closeComment); }; settings.watch('Livechat_auto_close_on_hold_chats_timeout', (value) => { @@ -33,7 +35,3 @@ settings.watch('Livechat_auto_close_on_hold_chats_timeout', (value) => { } callbacks.add('livechat:afterOnHold', handleAfterOnHold, callbacks.priority.HIGH, 'livechat-auto-close-on-hold'); }); - -settings.watch('Livechat_auto_close_on_hold_chats_custom_message', (value) => { - customCloseMessage = (value as string) || DEFAULT_CLOSED_MESSAGE; -}); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index 3d782764a3a77..e0b71d2d8005e 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -2,16 +2,18 @@ import { Agenda } from '@rocket.chat/agenda'; import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import moment from 'moment'; +import { LivechatRooms, Users } from '@rocket.chat/models'; +import type { IUser } from '@rocket.chat/core-typings'; import { Livechat } from '../../../../../app/livechat/server'; -import { LivechatRooms, Users } from '../../../../../app/models/server'; -const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler'; class AutoCloseOnHoldSchedulerClass { scheduler: Agenda; + schedulerUser: IUser; + running: boolean; public init(): void { @@ -48,8 +50,8 @@ class AutoCloseOnHoldSchedulerClass { const { roomId, comment } = data; const payload = { - user: schedulerUser, - room: LivechatRooms.findOneById(roomId), + user: await this.getSchedulerUser(), + room: await LivechatRooms.findOneById(roomId), comment, options: {}, visitor: undefined, @@ -57,6 +59,18 @@ class AutoCloseOnHoldSchedulerClass { Livechat.closeRoom(payload); } + + private async getSchedulerUser(): Promise { + if (!this.schedulerUser) { + const schedulerUser = await Users.findOneById('rocket.cat'); + if (!schedulerUser) { + throw new Error('Scheduler user not found'); + } + this.schedulerUser = schedulerUser; + } + + return this.schedulerUser; + } } export const AutoCloseOnHoldScheduler = new AutoCloseOnHoldSchedulerClass(); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index dca87efc6465e..cbbaf5c5245f9 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -1,12 +1,15 @@ import { Agenda } from '@rocket.chat/agenda'; import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; +import { LivechatRooms } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { LivechatRooms, Users } from '../../../../../app/models/server'; +import { Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server'; import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper'; +import { settings } from '../../../../../app/settings/server'; const schedulerUser = Users.findOneById('rocket.cat'); const SCHEDULER_NAME = 'omnichannel_scheduler'; @@ -53,7 +56,7 @@ class AutoTransferChatSchedulerClass { } private async transferRoom(roomId: string): Promise { - const room = LivechatRooms.findOneById(roomId, { + const room = await LivechatRooms.findOneById(roomId, { _id: 1, v: 1, servedBy: 1, @@ -69,8 +72,12 @@ class AutoTransferChatSchedulerClass { servedBy: { _id: ignoreAgentId }, } = room; + const comment = TAPi18n.__('Livechat_auto_transfer_unanswered_chats_comment', { + duration: settings.get('Livechat_auto_transfer_chat_timeout'), + }); + if (!RoutingManager.getConfig().autoAssignAgent) { - return Livechat.returnRoomAsInquiry(room._id, departmentId); + return Livechat.returnRoomAsInquiry(room._id, departmentId, comment); } const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); @@ -79,6 +86,7 @@ class AutoTransferChatSchedulerClass { userId: agent.agentId, transferredBy: schedulerUser, transferredTo: agent, + comment, }); } @@ -89,7 +97,7 @@ class AutoTransferChatSchedulerClass { const { roomId } = data; if (await this.transferRoom(roomId)) { - LivechatRooms.setAutoTransferredAtById(roomId); + await LivechatRooms.setAutoTransferredAtById(roomId); } await this.unscheduleRoom(roomId); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index d6fba1afc913e..1810e32b4cb8a 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -983,6 +983,7 @@ "Closed": "Closed", "Closed_At": "Closed at", "Closed_automatically": "Closed automatically by the system", + "Closed_automatically_because_chat_was_onhold_for_seconds": "Closed automatically because chat was On Hold for __onHoldTime__ seconds", "Closed_automatically_chat_queued_too_long": "Closed automatically by the system (queue maximum time exceeded)", "Closed_by_visitor": "Closed by visitor", "Closing_chat": "Closing chat", @@ -2919,6 +2920,7 @@ "Omnichannel_quick_actions": "Omnichannel Quick Actions", "Livechat_online": "Omnichannel on-line", "Omnichannel_placed_chat_on_hold": "Chat On Hold: __comment__", + "Livechat_auto_transfer_unanswered_chats_comment": "Chat auto transferred since it was unanswered for __duration__ seconds", "Livechat_Queue": "Omnichannel Queue", "Livechat_registration_form": "Registration Form", "Livechat_registration_form_message": "Registration Form Message", @@ -2933,6 +2935,7 @@ "Livechat_transcript_request_has_been_canceled": "The chat transcription request has been canceled.", "Livechat_transcript_sent": "Omnichannel transcript sent", "Livechat_transfer_return_to_the_queue": "__from__ returned the chat to the queue", + "Livechat_transfer_return_to_the_queue_with_a_comment": "__from__ returned the chat to the queue with a comment: __comment__", "Livechat_transfer_to_agent": "__from__ transferred the chat to __to__", "Livechat_transfer_to_agent_with_a_comment": "__from__ transferred the chat to __to__ with a comment: __comment__", "Livechat_transfer_to_department": "__from__ transferred the chat to the department __to__", diff --git a/apps/meteor/server/models/raw/LivechatRooms.js b/apps/meteor/server/models/raw/LivechatRooms.js index a5ecf7e7d1695..112bbc57f11d4 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.js +++ b/apps/meteor/server/models/raw/LivechatRooms.js @@ -1190,10 +1190,49 @@ export class LivechatRoomsRaw extends BaseRaw { } setDepartmentByRoomId(roomId, departmentId) { - return this.update({ _id: roomId }, { $set: { departmentId } }); + return this.updateOne({ _id: roomId }, { $set: { departmentId } }); } findOpen() { return this.find({ t: 'l', open: true }); } + + setAutoTransferOngoingById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $set: { + autoTransferOngoing: true, + }, + }; + + return this.updateOne(query, update); + } + + unsetAutoTransferOngoingById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $unset: { + autoTransferOngoing: 1, + }, + }; + + return this.updateOne(query, update); + } + + setAutoTransferredAtById(roomId) { + const query = { + _id: roomId, + }; + const update = { + $set: { + autoTransferredAt: new Date(), + }, + }; + + return this.updateOne(query, update); + } } diff --git a/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts b/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts new file mode 100644 index 0000000000000..202d186c1e216 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-auto-onhold-chat-closing.spec.ts @@ -0,0 +1,104 @@ +import { faker } from '@faker-js/faker'; +import type { Browser, Page } from '@playwright/test'; + +import { test, expect } from './utils/test'; +import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { IS_EE } from './config/constants'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + await page.locator('.main-content').waitFor(); + + return { page, poHomeChannel }; +}; + +test.describe('omnichannel-auto-onhold-chat-closing', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + // make "user-1" an agent and manager + let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); + expect(statusCode).toBe(200); + + // turn on auto selection routing + statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status(); + expect(statusCode).toBe(200); + + // make auto close on-hold chats timeout to be 5 seconds + statusCode = (await api.post('/settings/Livechat_auto_close_on_hold_chats_timeout', { value: 5 })).status(); + expect(statusCode).toBe(200); + + // allow agents to manually place chats on-hold + statusCode = (await api.post('/settings/Livechat_allow_manual_on_hold', { value: true })).status(); + expect(statusCode).toBe(200); + + agent = await createAuxContext(browser, 'user1-session.json'); + }); + + test.beforeEach(async ({ page }) => { + // make "user-1" online + await agent.poHomeChannel.sidenav.switchStatus('online'); + + // start a new chat for each test + newVisitor = { + name: faker.name.firstName(), + email: faker.internet.email(), + }; + poLiveChat = new OmnichannelLiveChat(page); + await page.goto('/livechat'); + await poLiveChat.btnOpenLiveChat('R').click(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test('expect on-hold chat to be closed automatically in 5 seconds', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + await agent.poHomeChannel.content.sendMessage('this_is_a_test_message_from_agent'); + + expect(agent.poHomeChannel.content.btnOnHold).toBeVisible(); + await agent.poHomeChannel.content.btnOnHold.click(); + + // expect to see a confirmation modal + expect(agent.poHomeChannel.content.btnModalConfirm).toBeVisible(); + await agent.poHomeChannel.content.btnModalConfirm.click(); + + // expect to see a system message saying the chat was on-hold + expect(agent.poHomeChannel.content.lastSystemMessageBody).toHaveText(`Chat On Hold: The chat was manually placed On Hold by user1`); + expect(agent.poHomeChannel.content.inputMessage).not.toBeVisible(); + expect(agent.poHomeChannel.content.resumeOnHoldOmnichannelChatButton).toBeVisible(); + + // current url + const chatRoomUrl = agent.page.url(); + + // wait for the chat to be closed automatically for 5 seconds + await agent.page.waitForTimeout(7000); + + // expect to see a system message saying the chat was closed automatically in the closed chat room + await agent.page.goto(chatRoomUrl); + expect(await agent.poHomeChannel.content.lastSystemMessageBody.innerText()).toBe( + 'Conversation closed: Closed automatically because chat was On Hold for 5 seconds.', + ); + }); + + test.afterAll(async ({ api }) => { + // delete "user-1" from agents + let statusCode = (await api.delete('/livechat/users/agent/user1')).status(); + expect(statusCode).toBe(200); + + // reset auto close on-hold chats timeout + statusCode = (await api.post('/settings/Livechat_auto_close_on_hold_chats_timeout', { value: 3600 })).status(); + expect(statusCode).toBe(200); + + // reset setting which allows agents to manually place chats on-hold + statusCode = (await api.post('/settings/Livechat_allow_manual_on_hold', { value: false })).status(); + expect(statusCode).toBe(200); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts b/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts new file mode 100644 index 0000000000000..983407776bc62 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel-auto-transfer-unanswered-chat.spec.ts @@ -0,0 +1,87 @@ +import { faker } from '@faker-js/faker'; +import type { Browser, Page } from '@playwright/test'; + +import { test, expect } from './utils/test'; +import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { IS_EE } from './config/constants'; + +const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { + const page = await browser.newPage({ storageState }); + const poHomeChannel = new HomeChannel(page); + await page.goto('/'); + await page.locator('.main-content').waitFor(); + + return { page, poHomeChannel }; +}; + +test.describe('omnichannel-auto-transfer-unanswered-chat', () => { + test.skip(!IS_EE, 'Enterprise Only'); + + let poLiveChat: OmnichannelLiveChat; + let newVisitor: { email: string; name: string }; + + let agent1: { page: Page; poHomeChannel: HomeChannel }; + let agent2: { page: Page; poHomeChannel: HomeChannel }; + + test.beforeAll(async ({ api, browser }) => { + // make "user-1" & "user-2" an agent + let statusCode = (await api.post('/livechat/users/agent', { username: 'user1' })).status(); + expect(statusCode).toBe(200); + statusCode = (await api.post('/livechat/users/agent', { username: 'user2' })).status(); + expect(statusCode).toBe(200); + + // turn on auto selection routing + statusCode = (await api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' })).status(); + expect(statusCode).toBe(200); + + // make auto close on-hold chats timeout to be 5 seconds + statusCode = (await api.post('/settings/Livechat_auto_transfer_chat_timeout', { value: 5 })).status(); + expect(statusCode).toBe(200); + + agent1 = await createAuxContext(browser, 'user1-session.json'); + agent2 = await createAuxContext(browser, 'user2-session.json'); + }); + + test.beforeEach(async ({ page }) => { + // make "user-1" online + await agent1.poHomeChannel.sidenav.switchOmnichannelStatus('online'); + await agent2.poHomeChannel.sidenav.switchOmnichannelStatus('offline'); + + // start a new chat for each test + newVisitor = { + name: faker.name.firstName(), + email: faker.internet.email(), + }; + poLiveChat = new OmnichannelLiveChat(page); + await page.goto('/livechat'); + await poLiveChat.btnOpenLiveChat('R').click(); + await poLiveChat.sendMessage(newVisitor, false); + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_user'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + }); + + test('expect chat to be auto transferred to next agent within 5 seconds of no reply from first agent', async () => { + await agent1.poHomeChannel.sidenav.openChat(newVisitor.name); + + await agent2.poHomeChannel.sidenav.switchOmnichannelStatus('online'); + + // wait for the chat to be closed automatically for 5 seconds + await agent1.page.waitForTimeout(7000); + + await agent2.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + test.afterAll(async ({ api }) => { + // delete "user-1" from agents + let statusCode = (await api.delete('/livechat/users/agent/user1')).status(); + expect(statusCode).toBe(200); + + // delete "user-2" from agents + statusCode = (await api.delete('/livechat/users/agent/user2')).status(); + expect(statusCode).toBe(200); + + // reset auto close on-hold chats timeout + statusCode = (await api.post('/settings/Livechat_auto_transfer_chat_timeout', { value: 0 })).status(); + expect(statusCode).toBe(200); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 4072e9a6e6e06..05d4cf5dc0507 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -170,4 +170,16 @@ export class HomeContent { get takeOmnichannelChatButton(): Locator { return this.page.locator('role=button[name="Take it!"]'); } + + get lastSystemMessageBody(): Locator { + return this.page.locator('[data-qa-type="system-message-body"]').last(); + } + + get resumeOnHoldOmnichannelChatButton(): Locator { + return this.page.locator('button.rcx-button--primary >> text=Resume'); + } + + get btnOnHold(): Locator { + return this.page.locator('[data-qa-id="ToolBoxAction-pause-unfilled"]'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 0722895e6224c..9467930497d7f 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -1,5 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { expect } from '../../utils/test'; + export class HomeSidenav { private readonly page: Page; @@ -47,6 +49,26 @@ export class HomeSidenav { await this.page.locator(`[data-qa="sidebar-item-title"] >> text="${name}"`).first().click(); } + async switchOmnichannelStatus(status: 'offline' | 'online') { + // button has a id of "omnichannel-status-toggle" + const toggleButton = this.page.locator('#omnichannel-status-toggle'); + expect(toggleButton).toBeVisible(); + + const currentStatus: 'Available' | 'Not Available' = (await toggleButton.getAttribute('title')) as any; + if (status === 'offline') { + if (currentStatus === 'Available') { + await toggleButton.click(); + } + } else if (currentStatus === 'Not Available') { + await toggleButton.click(); + } + + await this.page.waitForTimeout(500); + + const newStatus: 'Available' | 'Not Available' = (await this.page.locator('#omnichannel-status-toggle').getAttribute('title')) as any; + expect(newStatus).toBe(status === 'offline' ? 'Not Available' : 'Available'); + } + // Note: this is a workaround for now since queued omnichannel chats are not searchable yet so we can't use openChat() :( async openQueuedOmnichannelChat(name: string): Promise { await this.page.locator('[data-qa="sidebar-item-title"]', { hasText: name }).first().click(); diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 1e1a8d105e1e2..2d4b405dfee99 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,5 @@ import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { FindCursor, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -97,5 +98,11 @@ export interface ILivechatRoomsModel extends IBaseModel { setDepartmentByRoomId(roomId: any, departmentId: any): any; - findOpen(): any; + findOpen(): FindCursor; + + setAutoTransferOngoingById(roomId: string): Promise; + + unsetAutoTransferOngoingById(roomId: string): Promise; + + setAutoTransferredAtById(roomId: string): Promise; } From c25a4d3ba30db61a74a7a647bc549102cb2b0484 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Wed, 12 Oct 2022 17:32:29 +0530 Subject: [PATCH 2/3] Fix tests --- .../e2e/page-objects/fragments/home-sidenav.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 9467930497d7f..5df5843c42846 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -54,19 +54,24 @@ export class HomeSidenav { const toggleButton = this.page.locator('#omnichannel-status-toggle'); expect(toggleButton).toBeVisible(); - const currentStatus: 'Available' | 'Not Available' = (await toggleButton.getAttribute('title')) as any; + enum StatusTitleMap { + offline = 'Turn on answer chats', + online = 'Turn off answer chats', + } + + const currentStatus: StatusTitleMap = (await toggleButton.getAttribute('data-tooltip')) as any; if (status === 'offline') { - if (currentStatus === 'Available') { + if (currentStatus === StatusTitleMap.online) { await toggleButton.click(); } - } else if (currentStatus === 'Not Available') { + } else if (currentStatus === StatusTitleMap.offline) { await toggleButton.click(); } await this.page.waitForTimeout(500); - const newStatus: 'Available' | 'Not Available' = (await this.page.locator('#omnichannel-status-toggle').getAttribute('title')) as any; - expect(newStatus).toBe(status === 'offline' ? 'Not Available' : 'Available'); + const newStatus: StatusTitleMap = (await this.page.locator('#omnichannel-status-toggle').getAttribute('data-tooltip')) as any; + expect(newStatus).toBe(status === 'offline' ? StatusTitleMap.offline : StatusTitleMap.online); } // Note: this is a workaround for now since queued omnichannel chats are not searchable yet so we can't use openChat() :( From 5ea33b2d4e94b8570e7a87056fced4644478f813 Mon Sep 17 00:00:00 2001 From: murtaza98 Date: Thu, 20 Oct 2022 16:37:02 +0530 Subject: [PATCH 3/3] Improve transfer chat msg based on feedback --- apps/meteor/app/livechat/lib/messageTypes.ts | 11 +++++++++++ apps/meteor/app/livechat/server/lib/Livechat.js | 4 ++-- .../server/lib/AutoTransferChatScheduler.ts | 14 ++++++++------ .../packages/rocketchat-i18n/i18n/en.i18n.json | 5 +++-- packages/livechat/src/components/helpers.js | 4 +++- packages/livechat/src/i18n/en.json | 2 ++ 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/livechat/lib/messageTypes.ts b/apps/meteor/app/livechat/lib/messageTypes.ts index e1c0f18f42a23..c30916d4b4c9b 100644 --- a/apps/meteor/app/livechat/lib/messageTypes.ts +++ b/apps/meteor/app/livechat/lib/messageTypes.ts @@ -52,6 +52,17 @@ MessageTypes.registerType({ from, ...(comment && { comment }), }), + autoTransferUnansweredChatsToAgent: (): string => + TAPi18n.__(`Livechat_transfer_to_agent_auto_transfer_unanswered_chat`, { + from, + to: message?.transferData?.transferredTo?.name || message?.transferData?.transferredTo?.username || '', + duration: comment, + }), + autoTransferUnansweredChatsToQueue: (): string => + TAPi18n.__(`Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat`, { + from, + duration: comment, + }), }; return { transfer: transferTypes[message.transferData.scope](), diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 9809247a34e7f..3f95038eecc68 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -751,7 +751,7 @@ export const Livechat = { return RoutingManager.transferRoom(room, guest, transferData); }, - returnRoomAsInquiry(rid, departmentId, comment = '') { + returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) { Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`); const room = LivechatRooms.findOneById(rid); if (!room) { @@ -791,7 +791,7 @@ export const Livechat = { const transferredBy = normalizeTransferredByData(user, room); Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`); - const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...(comment && { comment }) }; + const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...overrideTransferData }; try { this.saveTransferHistory(room, transferData); RoutingManager.unassignAgent(inquiry, departmentId); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts index cbbaf5c5245f9..83670508ea46c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts @@ -3,7 +3,6 @@ import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { LivechatRooms } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Users } from '../../../../../app/models/server'; import { Livechat } from '../../../../../app/livechat/server'; @@ -72,12 +71,14 @@ class AutoTransferChatSchedulerClass { servedBy: { _id: ignoreAgentId }, } = room; - const comment = TAPi18n.__('Livechat_auto_transfer_unanswered_chats_comment', { - duration: settings.get('Livechat_auto_transfer_chat_timeout'), - }); + const timeoutDuration = settings.get('Livechat_auto_transfer_chat_timeout').toString(); if (!RoutingManager.getConfig().autoAssignAgent) { - return Livechat.returnRoomAsInquiry(room._id, departmentId, comment); + return Livechat.returnRoomAsInquiry(room._id, departmentId, { + scope: 'autoTransferUnansweredChatsToQueue', + comment: timeoutDuration, + transferredBy: schedulerUser, + }); } const agent = await RoutingManager.getNextAgent(departmentId, ignoreAgentId); @@ -86,7 +87,8 @@ class AutoTransferChatSchedulerClass { userId: agent.agentId, transferredBy: schedulerUser, transferredTo: agent, - comment, + scope: 'autoTransferUnansweredChatsToAgent', + comment: timeoutDuration, }); } diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 797e6b351160f..349e5d47a567f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2923,7 +2923,6 @@ "Omnichannel_quick_actions": "Omnichannel Quick Actions", "Livechat_online": "Omnichannel on-line", "Omnichannel_placed_chat_on_hold": "Chat On Hold: __comment__", - "Livechat_auto_transfer_unanswered_chats_comment": "Chat auto transferred since it was unanswered for __duration__ seconds", "Livechat_Queue": "Omnichannel Queue", "Livechat_registration_form": "Registration Form", "Livechat_registration_form_message": "Registration Form Message", @@ -2939,8 +2938,10 @@ "Livechat_transcript_sent": "Omnichannel transcript sent", "Livechat_transfer_return_to_the_queue": "__from__ returned the chat to the queue", "Livechat_transfer_return_to_the_queue_with_a_comment": "__from__ returned the chat to the queue with a comment: __comment__", + "Livechat_transfer_return_to_the_queue_auto_transfer_unanswered_chat": "__from__ returned the chat to the queue since it was unanswered for __duration__ seconds", "Livechat_transfer_to_agent": "__from__ transferred the chat to __to__", "Livechat_transfer_to_agent_with_a_comment": "__from__ transferred the chat to __to__ with a comment: __comment__", + "Livechat_transfer_to_agent_auto_transfer_unanswered_chat": "__from__ transferred the chat to __to__ since it was unanswered for __duration__ seconds", "Livechat_transfer_to_department": "__from__ transferred the chat to the department __to__", "Livechat_transfer_to_department_with_a_comment": "__from__ transferred the chat to the department __to__ with a comment: __comment__", "Livechat_transfer_failed_fallback": "The original department ( __from__ ) doesn't have online agents. Chat succesfully transferred to __to__", @@ -5468,4 +5469,4 @@ "onboarding.form.standaloneServerForm.manuallyIntegrate": "Need to manually integrate with external services", "Something_Went_Wrong": "Something went wrong", "Toolbox_room_actions": "Primary Room actions" -} \ No newline at end of file +} diff --git a/packages/livechat/src/components/helpers.js b/packages/livechat/src/components/helpers.js index 42a94fc162d39..0223813ae326f 100644 --- a/packages/livechat/src/components/helpers.js +++ b/packages/livechat/src/components/helpers.js @@ -144,7 +144,7 @@ export const normalizeTransferHistoryMessage = (transferData, sender, t) => { return; } - const { transferredBy, transferredTo, nextDepartment, scope } = transferData; + const { transferredBy, transferredTo, nextDepartment, scope, comment } = transferData; const from = transferredBy && (transferredBy.name || transferredBy.username); const transferTypes = { @@ -170,6 +170,8 @@ export const normalizeTransferHistoryMessage = (transferData, sender, t) => { } return t('from_returned_the_chat_to_the_queue', { from }); }, + autoTransferUnansweredChatsToAgent: () => t('the_chat_was_transferred_to_another_agent_due_to_unanswered', { duration: comment }), + autoTransferUnansweredChatsToQueue: () => t('the_chat_was_moved_back_to_queue_due_to_unanswered', { duration: comment }), }; return transferTypes[scope](); diff --git a/packages/livechat/src/i18n/en.json b/packages/livechat/src/i18n/en.json index 9155dd70dea91..791e2262351f8 100644 --- a/packages/livechat/src/i18n/en.json +++ b/packages/livechat/src/i18n/en.json @@ -78,7 +78,9 @@ "start_chat": "Start chat", "thanks_for_talking_with_us": "Thanks for talking with us", "the_chat_was_moved_back_to_queue": "The chat was moved back to queue", + "the_chat_was_moved_back_to_queue_due_to_unanswered": "The chat was moved back to queue since it was unanswered for {{duration}} seconds", "the_chat_was_transferred_to_another_agent": "The chat was transferred to another agent", + "the_chat_was_transferred_to_another_agent_due_to_unanswered": "The chat was transferred to another agent since it was unanswered for {{duration}} seconds", "the_controller_of_your_personal_data_is_company_na": "The controller of your personal data is [Company Name], with registered office at [Company Address]. To start the chat you agree that your personal data shall be processed and trasmitted in accordance with the General Data Protection Regulation (GDPR).", "transcript_success": "Transcript sent", "type_your_message_here": "Type your message here",