Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-experts-joke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes queued conversations not being sorted in real time based on the room's SLA policy
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -15,16 +15,17 @@ type SlaPoliciesSelectProps = {
export const SlaPoliciesSelect = ({ value, label, options, onChange }: SlaPoliciesSelectProps) => {
const hasLicense = useHasLicenseModule('livechat-enterprise');
const optionsSelect = useMemo<SelectOption[]>(() => options?.map((option) => [option._id, option.name]), [options]);
const fieldId = useId();

if (!hasLicense) {
return null;
}

return (
<Field>
<FieldLabel>{label}</FieldLabel>
<FieldLabel id={fieldId}>{label}</FieldLabel>
<FieldRow>
<Select value={value} options={optionsSelect} onChange={(value) => onChange(String(value))} />
<Select aria-labelledby={fieldId} value={value} options={optionsSelect} onChange={(value) => onChange(String(value))} />
</FieldRow>
</Field>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box } from '@rocket.chat/fuselage';
import { useId } from 'react';
import { useTranslation } from 'react-i18next';

import { FormSkeleton } from './FormSkeleton';
Expand All @@ -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 <FormSkeleton />;
Expand All @@ -26,8 +28,8 @@ const SlaField = ({ id }: SlaFieldProps) => {
const { name } = data;
return (
<Field>
<Label>{t('SLA_Policy')}</Label>
<Info>{name}</Info>
<Label id={slaFieldId}>{t('SLA_Policy')}</Label>
<Info aria-labelledby={slaFieldId}>{name}</Info>
</Field>
);
};
Expand Down
10 changes: 8 additions & 2 deletions apps/meteor/ee/app/livechat-enterprise/server/lib/SlaHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -19,7 +23,7 @@ export const removeSLAFromRooms = async (slaId: string, userId: string) => {
};

export const updateInquiryQueueSla = async (roomId: string, sla: Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes' | '_id'>) => {
const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { projection: { rid: 1, ts: 1 } });
const inquiry = await LivechatInquiry.findOneByRoomId(roomId);
if (!inquiry) {
return;
}
Expand All @@ -32,6 +36,8 @@ export const updateInquiryQueueSla = async (roomId: string, sla: Pick<IOmnichann
slaId,
estimatedWaitingTimeQueue,
});

void notifyOnLivechatInquiryChanged({ ...inquiry, slaId, estimatedWaitingTimeQueue, _updatedAt: new Date() }, 'updated');
};

export const updateRoomSlaWeights = async (roomId: string, sla: Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes' | '_id'>) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();

Comment thread
aleksandernsilva marked this conversation as resolved.
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<ReturnType<typeof createConversation>>[] = [];
let slas: Serialized<Omit<IOmnichannelServiceLevelAgreements, '_updatedAt'>>[] = [];

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 () => {
Comment thread
aleksandernsilva marked this conversation as resolved.
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');
Comment thread
aleksandernsilva marked this conversation as resolved.
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');
Comment thread
aleksandernsilva marked this conversation as resolved.
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');
Comment thread
aleksandernsilva marked this conversation as resolved.
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');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) });
}
Expand Down
13 changes: 13 additions & 0 deletions apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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);
}
Expand Down
7 changes: 5 additions & 2 deletions apps/meteor/tests/e2e/utils/omnichannel/sla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ export const generateRandomSLAData = (): Omit<IOmnichannelServiceLevelAgreements
dueTimeInMinutes: faker.number.int({ min: 10, max: DEFAULT_SLA_CONFIG.ESTIMATED_WAITING_TIME_QUEUE }),
});

export const createSLA = async (api: BaseTest['api']): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>> => {
const response = await api.post('/livechat/sla', generateRandomSLAData());
export const createSLA = async (
api: BaseTest['api'],
slaData?: Omit<IOmnichannelServiceLevelAgreements, '_updatedAt' | '_id'>,
): Promise<Omit<IOmnichannelServiceLevelAgreements, '_updated'>> => {
const response = await api.post('/livechat/sla', slaData ?? generateRandomSLAData());
expect(response.status()).toBe(200);

const { sla } = (await response.json()) as { sla: Omit<IOmnichannelServiceLevelAgreements, '_updated'> };
Expand Down
Loading