diff --git a/.changeset/afraid-guests-jog.md b/.changeset/afraid-guests-jog.md new file mode 100644 index 000000000000..420b9bb5d329 --- /dev/null +++ b/.changeset/afraid-guests-jog.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/livechat": minor +--- + +Created a `transferChat` Livechat API endpoint for transferring chats programmatically, the endpoint has all the limitations & permissions required that transferring via UI has diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts index 278df11f16cb..3fc71b4e0090 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-livechat-api.spec.ts @@ -40,6 +40,7 @@ declare const window: Window & { setParentUrl: (url: string) => void; setTheme: (theme: { color?: string; fontColor?: string; iconColor?: string; title?: string; offlineTitle?: string }) => void; setLanguage: (language: string) => void; + transferChat: (department: string) => void; onChatMaximized: (callback: () => void) => void; onChatMinimized: (callback: () => void) => void; onChatStarted: (callback: () => void) => void; @@ -406,6 +407,60 @@ test.describe('OC - Livechat API', () => { }); }); + test.describe('OC - Livechat API - transferChat', () => { + let poAuxContext2: { page: Page; poHomeOmnichannel: HomeOmnichannel }; + + test.beforeEach(async ({ browser }) => { + const { page: pageCtx2 } = await createAuxContext(browser, Users.user2); + poAuxContext2 = { page: pageCtx2, poHomeOmnichannel: new HomeOmnichannel(pageCtx2) }; + }); + + test.afterEach(async () => { + await poAuxContext2.page.close(); + }); + + test('transferChat - Called during ongoing conversation', async () => { + const [departmentA, departmentB] = departments.map(({ data }) => data); + const registerGuestVisitor = { + name: faker.person.firstName(), + email: faker.internet.email(), + token: faker.string.uuid(), + department: departmentA._id, + }; + + // Start Chat + await poLiveChat.page.evaluate(() => window.RocketChat.livechat.maximizeWidget()); + await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).toBeVisible(); + + await poLiveChat.page.evaluate( + (registerGuestVisitor) => window.RocketChat.livechat.registerGuest(registerGuestVisitor), + registerGuestVisitor, + ); + + await expect(page.frameLocator('#rocketchat-iframe').getByText('Start Chat')).not.toBeVisible(); + + await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.btnSendMessageToOnlineAgent.click(); + + await test.step('Expect registered guest to be in dep1', async () => { + await poAuxContext.poHomeOmnichannel.sidenav.openChat(registerGuestVisitor.name); + await expect(poAuxContext.poHomeOmnichannel.content.channelHeader).toContainText(registerGuestVisitor.name); + }); + + const depId = departmentB._id; + + await test.step('Expect chat to be transferred', async () => { + await poLiveChat.page.evaluate((depId) => window.RocketChat.livechat.transferChat(depId), depId); + + await poAuxContext2.page.locator('role=navigation >> role=button[name=Search]').click(); + await poAuxContext2.page.locator('role=search >> role=searchbox').fill(registerGuestVisitor.name); + await expect( + poAuxContext2.page.locator(`role=search >> role=listbox >> role=link >> text="${registerGuestVisitor.name}"`), + ).toBeVisible(); + }); + }); + }); + test('OC - Livechat API - registerGuest', async ({ browser }) => { const registerGuestVisitor = { name: faker.person.firstName(), diff --git a/packages/livechat/src/lib/hooks.ts b/packages/livechat/src/lib/hooks.ts index 480253a49b1e..e1a980fe65e3 100644 --- a/packages/livechat/src/lib/hooks.ts +++ b/packages/livechat/src/lib/hooks.ts @@ -247,6 +247,27 @@ const api = { }); }, + transferChat: async (department: string) => { + const { + config: { departments = [] }, + room, + } = store.state; + + const dep = departments.find((dep) => dep._id === department || dep.name === department)?._id || ''; + + if (!dep) { + throw new Error( + 'The selected department is invalid. Check departments configuration to ensure the department exists, is enabled and has at least 1 agent', + ); + } + if (!room) { + throw new Error("Conversation has not been started yet, can't transfer"); + } + + const { _id: rid } = room; + await Livechat.transferChat({ rid, department: dep }); + }, + setLanguage: async (language: StoreState['iframe']['language']) => { const { iframe } = store.state; await store.setState({ iframe: { ...iframe, language } }); diff --git a/packages/livechat/src/widget.ts b/packages/livechat/src/widget.ts index 530d5ac8be4e..47f073aaff0a 100644 --- a/packages/livechat/src/widget.ts +++ b/packages/livechat/src/widget.ts @@ -368,6 +368,10 @@ function setParentUrl(url: string) { callHook('setParentUrl', url); } +function transferChat(department: string) { + callHook('transferChat', department); +} + function setGuestMetadata(metadata: StoreState['iframe']['guestMetadata']) { if (typeof metadata !== 'object') { throw new Error('Invalid metadata'); @@ -550,6 +554,7 @@ const livechatWidgetAPI = { setGuestMetadata, clearAllCallbacks, setHiddenSystemMessages, + transferChat, // callbacks onChatMaximized(fn: () => void) {