From 481e43cb9600fe11f4c6e89c23f585fb58586523 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 27 Aug 2025 16:24:58 -0300 Subject: [PATCH 1/3] fix: added back sla notifier --- .../ee/app/livechat-enterprise/server/lib/SlaHelper.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts index ac1a0cddca0fb..2fda0cabcee6f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts @@ -2,7 +2,11 @@ import { Message } from '@rocket.chat/core-services'; import type { IOmnichannelServiceLevelAgreements, IUser } from '@rocket.chat/core-typings'; import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; -import { notifyOnRoomChangedById, notifyOnLivechatInquiryChangedByRoom } from '../../../../../app/lib/server/lib/notifyListener'; +import { + notifyOnRoomChangedById, + notifyOnLivechatInquiryChangedByRoom, + notifyOnLivechatInquiryChanged, +} from '../../../../../app/lib/server/lib/notifyListener'; import { callbacks } from '../../../../../lib/callbacks'; export const removeSLAFromRooms = async (slaId: string, userId: string) => { @@ -19,7 +23,7 @@ export const removeSLAFromRooms = async (slaId: string, userId: string) => { }; export const updateInquiryQueueSla = async (roomId: string, sla: Pick) => { - const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { rid: 1, ts: 1 } }); + const inquiry = await LivechatInquiry.findOneByRoomId(roomId); if (!inquiry) { return; } @@ -32,6 +36,8 @@ export const updateInquiryQueueSla = async (roomId: string, sla: Pick) => { From ea8d7877844c5a1e631db6a5e6bc6b4be4aad3b6 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 28 Aug 2025 14:22:14 -0300 Subject: [PATCH 2/3] test: implemented e2e tests --- .../additionalForms/SlaPoliciesSelect.tsx | 7 +- .../directory/components/SlaField.tsx | 6 +- .../omnichannel-sla-policies-sidebar.spec.ts | 151 ++++++++++++++++++ .../page-objects/fragments/home-sidenav.ts | 4 + .../e2e/page-objects/omnichannel-room-info.ts | 13 ++ .../meteor/tests/e2e/utils/omnichannel/sla.ts | 7 +- 6 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts diff --git a/apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx b/apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx index b764b75084251..a8e6d0faacd6f 100644 --- a/apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/SlaPoliciesSelect.tsx @@ -1,7 +1,7 @@ import type { IOmnichannelServiceLevelAgreements, Serialized } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; import { Field, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage'; -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; @@ -15,6 +15,7 @@ type SlaPoliciesSelectProps = { export const SlaPoliciesSelect = ({ value, label, options, onChange }: SlaPoliciesSelectProps) => { const hasLicense = useHasLicenseModule('livechat-enterprise'); const optionsSelect = useMemo(() => options?.map((option) => [option._id, option.name]), [options]); + const fieldId = useId(); if (!hasLicense) { return null; @@ -22,9 +23,9 @@ export const SlaPoliciesSelect = ({ value, label, options, onChange }: SlaPolici return ( - {label} + {label} - onChange(String(value))} /> ); diff --git a/apps/meteor/client/views/omnichannel/directory/components/SlaField.tsx b/apps/meteor/client/views/omnichannel/directory/components/SlaField.tsx index 0272c2dcd501b..a54787e334a23 100644 --- a/apps/meteor/client/views/omnichannel/directory/components/SlaField.tsx +++ b/apps/meteor/client/views/omnichannel/directory/components/SlaField.tsx @@ -1,4 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; +import { useId } from 'react'; import { useTranslation } from 'react-i18next'; import { FormSkeleton } from './FormSkeleton'; @@ -14,6 +15,7 @@ type SlaFieldProps = { const SlaField = ({ id }: SlaFieldProps) => { const { t } = useTranslation(); const { data, isLoading, isError } = useSlaInfo(id); + const slaFieldId = useId(); if (isLoading) { return ; @@ -26,8 +28,8 @@ const SlaField = ({ id }: SlaFieldProps) => { const { name } = data; return ( - - {name} + + {name} ); }; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts new file mode 100644 index 0000000000000..52ff99728fd59 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-sla-policies-sidebar.spec.ts @@ -0,0 +1,151 @@ +import { + OmnichannelSortingMechanismSettingType, + type IOmnichannelServiceLevelAgreements, + type Serialized, +} from '@rocket.chat/core-typings'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { OmnichannelRoomInfo } from '../page-objects/omnichannel-room-info'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { createSLA } from '../utils/omnichannel/sla'; +import { test, expect } from '../utils/test'; + +const visitorA = createFakeVisitor(); +const visitorB = createFakeVisitor(); +const visitorC = createFakeVisitor(); + +test.skip(!IS_EE, 'Omnichannel SLAs > Enterprise Only'); + +test.use({ storageState: Users.user1.state }); + +test.describe('OC - SLA Policies [Sidebar]', () => { + let poHomeChannel: HomeOmnichannel; + let poRoomInfo: OmnichannelRoomInfo; + let conversations: Awaited>[] = []; + let slas: Serialized>[] = []; + + test.beforeAll('create SLAs', async ({ api }) => { + slas = await Promise.all([ + createSLA(api, { name: 'Very Urgent', dueTimeInMinutes: 1 }), + createSLA(api, { name: 'Urgent', dueTimeInMinutes: 10 }), + createSLA(api, { name: 'Not Urgent', dueTimeInMinutes: 30 }), + ]); + }); + + test.beforeAll(async ({ api }) => { + ( + await Promise.all([ + // Create agent and manager + api.post('/livechat/users/agent', { username: 'user1' }), + api.post('/livechat/users/manager', { username: 'user1' }), + // Settings + api.post('/settings/Livechat_Routing_Method', { value: 'Manual_Selection' }), + api.post('/settings/Omnichannel_sorting_mechanism', { value: OmnichannelSortingMechanismSettingType.SLAs }), + ]) + ).every((res) => expect(res.status()).toBe(200)); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeOmnichannel(page); + poRoomInfo = new OmnichannelRoomInfo(page); + }); + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.locator('#main-content').waitFor(); + }); + + test.beforeEach(async ({ api }) => { + conversations = await Promise.all([ + createConversation(api, { visitorName: visitorA.name, agentId: 'user1' }), + createConversation(api, { visitorName: visitorB.name, agentId: 'user1' }), + createConversation(api, { visitorName: visitorC.name, agentId: 'user1' }), + ]); + }); + + test.afterAll('delete SLAs', async ({ api }) => { + const responses = await Promise.all(slas.map((sla) => api.delete(`/livechat/sla/${sla._id}`))); + responses.every((res) => expect(res.status()).toBe(200)); + }); + + test.afterAll(async ({ api }) => { + // Delete conversations + await Promise.all(conversations.map((conversation) => conversation.delete())); + + ( + await Promise.all([ + // Delete agent and manager + api.delete('/livechat/users/agent/user1'), + api.delete('/livechat/users/manager/user1'), + // Reset settings + api.post('/settings/Livechat_Routing_Method', { value: 'Auto_Selection' }), + api.post('/settings/Omnichannel_sorting_mechanism', { value: OmnichannelSortingMechanismSettingType.Timestamp }), + ]) + ).every((res) => expect(res.status()).toBe(200)); + }); + + test('OC - SLA Policies [Sidebar] - Update conversation SLA Policy', async () => { + await test.step('expect to change room SLA policy to "Not urgent"', async () => { + await test.step('expect to open room and room info to be visible', async () => { + await poHomeChannel.sidenav.getSidebarItemByName(visitorA.name).click(); + await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + }); + + await test.step('expect to update room SLA policy', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poRoomInfo.btnEditRoomInfo.click(); + await poRoomInfo.selectSLA('Not Urgent'); + await poRoomInfo.btnSaveEditRoom.click(); + }); + + await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Not Urgent'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '1'); + }); + }); + + await test.step('expect to change room SLA policy to "Urgent"', async () => { + await test.step('expect to open room and room info to be visible', async () => { + await poHomeChannel.sidenav.getSidebarItemByName(visitorB.name).click(); + await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + }); + + await test.step('expect to update room SLA policy', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poRoomInfo.btnEditRoomInfo.click(); + await poRoomInfo.selectSLA('Urgent'); + await poRoomInfo.btnSaveEditRoom.click(); + }); + + await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Urgent'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorB.name)).toHaveAttribute('data-index', '1'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '2'); + }); + }); + + await test.step('expect to change room SLA policy to "Very Urgent"', async () => { + await test.step('expect to open room and room info to be visible', async () => { + await poHomeChannel.sidenav.getSidebarItemByName(visitorC.name).click(); + await expect(poRoomInfo.dialogRoomInfo).toBeVisible(); + }); + + await test.step('expect to update room SLA policy', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).not.toBeVisible(); + await poRoomInfo.btnEditRoomInfo.click(); + await poRoomInfo.selectSLA('Very Urgent'); + await poRoomInfo.btnSaveEditRoom.click(); + }); + + await test.step('expect SLA to have been updated in the room info and queue order to be correct', async () => { + await expect(poRoomInfo.getInfoByLabel('SLA Policy')).toHaveText('Very Urgent'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorC.name)).toHaveAttribute('data-index', '1'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorB.name)).toHaveAttribute('data-index', '2'); + await expect(poHomeChannel.sidenav.getSidebarListItemByName(visitorA.name)).toHaveAttribute('data-index', '3'); + }); + }); + }); +}); 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 0318752e96885..2bd4cbac0ef5d 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -109,6 +109,10 @@ export class HomeSidenav { return this.page.getByRole('link').filter({ has: this.page.getByText(name, { exact: true }) }); } + getSidebarListItemByName(name: string): Locator { + return this.sidebarChannelsList.getByRole('listitem').filter({ has: this.getSidebarItemByName(name) }); + } + getSearchItemByName(name: string): Locator { return this.searchList.getByRole('link').filter({ has: this.page.getByText(name, { exact: true }) }); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts index 29c9b7cdf0c89..84a49c01c13b2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts @@ -40,6 +40,19 @@ export class OmnichannelRoomInfo { return this.page.locator(`div >> text="${label}"`); } + getInfoByLabel(label: string): Locator { + return this.dialogRoomInfo.getByLabel(label); + } + + get inputSLAPolicy(): Locator { + return this.dialogEditRoom.getByRole('button', { name: 'SLA Policy' }); + } + + async selectSLA(name: string): Promise { + await this.inputSLAPolicy.click(); + return this.page.getByRole('option', { name, exact: true }).click(); + } + getBadgeIndicator(name: string, title: string): Locator { return this.homeSidenav.getSidebarItemByName(name).getByTitle(title); } diff --git a/apps/meteor/tests/e2e/utils/omnichannel/sla.ts b/apps/meteor/tests/e2e/utils/omnichannel/sla.ts index 908787cc8890e..fe59dcd6b877f 100644 --- a/apps/meteor/tests/e2e/utils/omnichannel/sla.ts +++ b/apps/meteor/tests/e2e/utils/omnichannel/sla.ts @@ -10,8 +10,11 @@ export const generateRandomSLAData = (): Omit> => { - const response = await api.post('/livechat/sla', generateRandomSLAData()); +export const createSLA = async ( + api: BaseTest['api'], + slaData?: Omit, +): Promise> => { + const response = await api.post('/livechat/sla', slaData ?? generateRandomSLAData()); expect(response.status()).toBe(200); const { sla } = (await response.json()) as { sla: Omit }; From a15297aaf34052e6ddd42da981e5c0be8ecffb93 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Thu, 28 Aug 2025 15:11:43 -0300 Subject: [PATCH 3/3] chore: changeset --- .changeset/nice-experts-joke.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nice-experts-joke.md diff --git a/.changeset/nice-experts-joke.md b/.changeset/nice-experts-joke.md new file mode 100644 index 0000000000000..e237385be8e1e --- /dev/null +++ b/.changeset/nice-experts-joke.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes queued conversations not being sorted in real time based on the room's SLA policy