diff --git a/.changeset/warm-steaks-fetch.md b/.changeset/warm-steaks-fetch.md new file mode 100644 index 0000000000000..842d7f04b46e6 --- /dev/null +++ b/.changeset/warm-steaks-fetch.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes contact's conflict resolution not working due to invalid parameters diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx index bb5eacfe17d9a..7ca34aa3c4dc7 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ReviewContactModal.tsx @@ -47,7 +47,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { const payload = { name, contactManager, - ...(customFields && { ...customFields }), + ...(customFields && { customFields }), wipeConflicts: true, }; @@ -86,7 +86,7 @@ const ReviewContactModal = ({ contact, onCancel }: ReviewContactModalProps) => { return ( - {t(label as TranslationKey)} + {t(label as TranslationKey)} { rules={{ required: isContactManagerField ? undefined : t('Required_field', { field: t(label as TranslationKey) }), }} - render={({ field: { value, onChange } }) => } + render={({ field: { value, onChange } }) => ( + + )} /> - + {t('different_values_found', { number: values.length })} - {errors?.[name] && {errors?.[name]?.message}} + {errors?.[name] && {errors?.[name]?.message}} ); })} diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts new file mode 100644 index 0000000000000..f2845a1713195 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts @@ -0,0 +1,93 @@ +import { faker } from '@faker-js/faker'; + +import { createFakeVisitor } from '../../mocks/data'; +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { createCustomField } from '../utils/omnichannel/custom-field'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { test, expect } from '../utils/test'; + +const visitor = createFakeVisitor(); + +test.skip(!IS_EE, 'Omnichannel Contact Review > Enterprise Only'); + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('OC - Contact Review', () => { + let poHomeChannel: HomeOmnichannel; + + const customFieldName = faker.string.uuid(); + const visitorToken = faker.string.uuid(); + let conversation: Awaited>; + let customField: Awaited>; + + test.beforeAll(async ({ api }) => { + ( + await Promise.all([ + api.post('/livechat/users/agent', { username: 'user1' }), + api.post('/livechat/users/manager', { username: 'user1' }), + ]) + ).every((res) => expect(res.status()).toBe(200)); + + customField = await createCustomField(api, { field: customFieldName }); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeOmnichannel(page); + }); + + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.locator('.main-content').waitFor(); + }); + + test.beforeEach(async ({ api }) => { + conversation = await createConversation(api, { visitorName: visitor.name, agentId: `user1`, visitorToken }); + }); + + test.beforeEach(async ({ api }) => { + const resCustomFieldA = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value', + overwrite: true, + }); + + expect(resCustomFieldA.status()).toBe(200); + + const resCustomFieldB = await api.post('/livechat/custom.field', { + token: visitorToken, + key: customFieldName, + value: 'custom-field-value-2', + overwrite: false, + }); + + expect(resCustomFieldB.status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + (await Promise.all([api.delete('/livechat/users/agent/user1'), api.delete('/livechat/users/manager/user1')])).every((res) => + expect(res.status()).toBe(200), + ); + + await conversation.delete(); + await customField.delete(); + }); + + test('OC - Contact Review - Update custom field conflicting', async ({ page }) => { + await poHomeChannel.sidenav.getSidebarItemByName(visitor.name).click(); + await poHomeChannel.content.btnContactInformation.click(); + + await poHomeChannel.content.contactReviewModal.btnSeeConflicts.click(); + + await poHomeChannel.content.contactReviewModal.getFieldByName(customFieldName).click(); + await poHomeChannel.content.contactReviewModal.findOption('custom-field-value-2').click(); + await poHomeChannel.content.contactReviewModal.btnSave.click(); + + const response = await page.waitForResponse('**/api/v1/omnichannel/contacts.update'); + await expect(response.status()).toBe(200); + + await expect(poHomeChannel.content.contactReviewModal.btnSeeConflicts).not.toBeVisible(); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index d6c8cf9decf72..335cec483cb19 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -3,16 +3,20 @@ import type { Locator, Page } from '@playwright/test'; import { OmnichannelTransferChatModal } from '../omnichannel-transfer-chat-modal'; import { HomeContent } from './home-content'; import { OmnichannelCloseChatModal } from './omnichannel-close-chat-modal'; +import { OmnichannelContactReviewModal } from '../omnichannel-contact-review-modal'; export class HomeOmnichannelContent extends HomeContent { readonly closeChatModal: OmnichannelCloseChatModal; readonly forwardChatModal: OmnichannelTransferChatModal; + readonly contactReviewModal: OmnichannelContactReviewModal; + constructor(page: Page) { super(page); this.closeChatModal = new OmnichannelCloseChatModal(page); this.forwardChatModal = new OmnichannelTransferChatModal(page); + this.contactReviewModal = new OmnichannelContactReviewModal(page); } get btnReturnToQueue(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts new file mode 100644 index 0000000000000..520faa2891ada --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts @@ -0,0 +1,25 @@ +import type { Locator, Page } from '@playwright/test'; + +export class OmnichannelContactReviewModal { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get btnSeeConflicts(): Locator { + return this.page.getByRole('button', { name: 'See conflicts', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); + } + + getFieldByName(name: string): Locator { + return this.page.getByLabel(name, { exact: true }); + } + + findOption(name: string): Locator { + return this.page.getByRole('option', { name, exact: true }); + } +} diff --git a/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts new file mode 100644 index 0000000000000..bc476aae918c8 --- /dev/null +++ b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts @@ -0,0 +1,54 @@ +import type { ILivechatCustomField } from '@rocket.chat/core-typings'; + +import { parseMeteorResponse } from '../parseMeteorResponse'; +import type { BaseTest } from '../test'; + +type CustomField = Omit & { field: string }; + +export const removeCustomField = (api: BaseTest['api'], id: string) => { + return api.post('/method.call/livechat:deleteCustomField', { + method: 'livechat:saveCustomField', + params: [id], + id: 'id', + msg: 'method', + }); +}; + +export const createCustomField = async (api: BaseTest['api'], overwrides: Partial) => { + const response = await api.post('/method.call/livechat:saveCustomField', { + message: JSON.stringify({ + method: 'livechat:saveCustomField', + params: [ + null, + { + field: overwrides.field, + label: overwrides.label || overwrides.field, + visibility: 'visible', + scope: 'visitor', + searchable: false, + regexp: '', + type: 'input', + required: false, + defaultValue: '', + options: '', + public: false, + ...overwrides, + }, + ], + id: 'id', + msg: 'method', + }), + }); + + if (!response.ok()) { + throw new Error(`Failed to create custom field [http status: ${response.status()}]`); + } + + const customField = await parseMeteorResponse(response); + + return { + response, + customField, + delete: () => removeCustomField(api, customField._id), + }; +};