diff --git a/.changeset/good-bobcats-argue.md b/.changeset/good-bobcats-argue.md new file mode 100644 index 0000000000000..e4603f8e99b0c --- /dev/null +++ b/.changeset/good-bobcats-argue.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes a GUI crash when editing a canned response with tags via room contextual bar. diff --git a/apps/meteor/client/components/Omnichannel/Tags.tsx b/apps/meteor/client/components/Omnichannel/Tags.tsx index 9a2a86500633e..15678390b323f 100644 --- a/apps/meteor/client/components/Omnichannel/Tags.tsx +++ b/apps/meteor/client/components/Omnichannel/Tags.tsx @@ -2,7 +2,7 @@ import { TextInput, Chip, Button, FieldLabel, FieldRow } from '@rocket.chat/fuse import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, ReactElement } from 'react'; -import { useMemo, useState } from 'react'; +import { useId, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { FormSkeleton } from './Skeleton'; @@ -19,6 +19,7 @@ type TagsProps = { const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps): ReactElement => { const { t } = useTranslation(); + const tagsFieldId = useId(); const { data: tagsResult, isLoading } = useLivechatTags({ department, @@ -66,13 +67,14 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) return ( <> - + {t('Tags')} {tagsResult?.tags && tagsResult?.tags.length ? ( { handler(tags.map((tag) => tag.label)); @@ -87,6 +89,7 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) ): void => handleTagValue(currentTarget.value)} flexGrow={1} placeholder={t('Enter_a_tag')} diff --git a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx index 08bb02b8fb0a2..b4c2dde91758a 100644 --- a/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx +++ b/apps/meteor/client/omnichannel/additionalForms/CurrentChatTags.tsx @@ -2,13 +2,14 @@ import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import AutoCompleteTagsMultiple from '../tags/AutoCompleteTagsMultiple'; type CurrentChatTagsProps = { + id?: string; value: Array<{ value: string; label: string }>; handler: (value: { label: string; value: string }[]) => void; department?: string; viewAll?: boolean; }; -const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTagsProps) => { +const CurrentChatTags = ({ id, value, handler, department, viewAll }: CurrentChatTagsProps) => { const hasLicense = useHasLicenseModule('livechat-enterprise'); if (!hasLicense) { @@ -17,6 +18,7 @@ const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTag return ( ({ _id: cannedResponseData?._id || '', shortcut: cannedResponseData?.shortcut || '', text: cannedResponseData?.text || '', - tags: - cannedResponseData?.tags && Array.isArray(cannedResponseData.tags) - ? cannedResponseData.tags.map((tag: string) => ({ label: tag, value: tag })) - : [], + tags: cannedResponseData?.tags || [], scope: cannedResponseData?.scope || 'user', departmentId: cannedResponseData?.departmentId || '', }); @@ -44,7 +32,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); - const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); + const methods = useForm({ defaultValues: getInitialData(cannedResponseData) }); const { handleSubmit, formState: { isDirty }, @@ -53,7 +41,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi const saveCannedResponse = useEndpoint('POST', '/v1/canned-responses'); const handleCreate = useCallback( - async ({ departmentId, ...data }: CreateCannedResponseModalFormData) => { + async ({ departmentId, ...data }: CannedResponseEditFormData) => { try { await saveCannedResponse({ ...data, @@ -83,9 +71,11 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi title={cannedResponseData?._id ? t('Edit_Canned_Response') : t('Create_canned_response')} wrapperFunction={(props) => } > - - - + }> + + + + ); }; diff --git a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx index d80cc2b913fb1..6a980e6de6bfb 100644 --- a/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx +++ b/apps/meteor/client/omnichannel/tags/AutoCompleteTagsMultiple.tsx @@ -9,6 +9,7 @@ import { AsyncStatePhase } from '../../hooks/useAsyncState'; import { useTagsList } from '../../hooks/useTagsList'; type AutoCompleteTagsMultipleProps = { + id?: string; value?: PaginatedMultiSelectOption[]; onlyMyTags?: boolean; onChange?: (value: PaginatedMultiSelectOption[]) => void; @@ -17,6 +18,7 @@ type AutoCompleteTagsMultipleProps = { }; const AutoCompleteTagsMultiple = ({ + id, value = [], onlyMyTags = false, onChange = () => undefined, @@ -44,6 +46,7 @@ const AutoCompleteTagsMultiple = ({ return ( { +test.describe.serial('OC - Canned Responses Sidebar', () => { test.skip(!IS_EE, 'Enterprise Only'); let poLiveChat: OmnichannelLiveChat; let newVisitor: { email: string; name: string }; - let agent: { page: Page; poHomeChannel: HomeChannel }; + let agent: { page: Page; poHomeChannel: HomeOmnichannel }; + + const cannedResponseName = faker.string.uuid(); test.beforeAll(async ({ api, browser }) => { newVisitor = createFakeVisitor(); @@ -23,34 +26,65 @@ test.describe('Omnichannel Canned Responses Sidebar', () => { await api.post('/livechat/users/manager', { username: 'user1' }); const { page } = await createAuxContext(browser, Users.user1); - agent = { page, poHomeChannel: new HomeChannel(page) }; + agent = { page, poHomeChannel: new HomeOmnichannel(page) }; }); + test.beforeEach(async ({ page, api }) => { poLiveChat = new OmnichannelLiveChat(page, api); }); + test.afterAll('close livechat conversation', async () => { + await agent.poHomeChannel.content.closeChat(); + }); + test.afterAll(async ({ api }) => { await api.delete('/livechat/users/agent/user1'); await api.delete('/livechat/users/manager/user1'); + await poLiveChat.page.close(); await agent.page.close(); }); - test('Receiving a message from visitor', async ({ page }) => { - await test.step('Expect send a message as a visitor', async () => { + test('OC - Canned Responses Sidebar - Create', async ({ page }) => { + await test.step('expect send a message as a visitor', async () => { await page.goto('/livechat'); await poLiveChat.openLiveChat(); await poLiveChat.sendMessage(newVisitor, false); - await poLiveChat.onlineAgentMessage.type('this_a_test_message_from_visitor'); + await poLiveChat.onlineAgentMessage.fill('this_a_test_message_from_visitor'); await poLiveChat.btnSendMessageToOnlineAgent.click(); }); - await test.step('Expect to have 1 omnichannel assigned to agent 1', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { await agent.poHomeChannel.sidenav.openChat(newVisitor.name); }); - await test.step('Expect to be able to open canned responses sidebar and creation', async () => { + await test.step('expect to be able to open canned responses sidebar and creation', async () => { await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to create new canned response', async () => { await agent.poHomeChannel.content.btnNewCannedResponse.click(); + await agent.poHomeChannel.cannedResponses.inputShortcut.fill(cannedResponseName); + await agent.poHomeChannel.cannedResponses.inputMessage.fill(faker.lorem.paragraph()); + await agent.poHomeChannel.cannedResponses.addTag(faker.commerce.department()); + await agent.poHomeChannel.cannedResponses.radioPublic.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); + }); + }); + + test('OC - Canned Responses Sidebar - Edit', async () => { + await test.step('expect to have 1 omnichannel assigned to agent 1', async () => { + await agent.poHomeChannel.sidenav.openChat(newVisitor.name); + }); + + await test.step('expect to be able to open canned responses sidebar and creation', async () => { + await agent.poHomeChannel.content.btnCannedResponses.click(); + }); + + await test.step('expect to edit canned response', async () => { + await agent.poHomeChannel.cannedResponses.listItem(cannedResponseName).click(); + await agent.poHomeChannel.cannedResponses.btnEdit.click(); + await agent.poHomeChannel.cannedResponses.radioPrivate.click(); + await agent.poHomeChannel.cannedResponses.btnSave.click(); }); }); }); 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 37f35b8202de8..d6c8cf9decf72 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 @@ -74,4 +74,10 @@ export class HomeOmnichannelContent extends HomeContent { get infoHeaderName(): Locator { return this.page.locator('.rcx-room-header').getByRole('heading'); } + + async closeChat() { + await this.btnCloseChat.click(); + await this.closeChatModal.inputComment.fill('any_comment'); + await this.closeChatModal.btnConfirm.click(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts index 0537090cc0c02..17713dd4b51e2 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-canned-responses.ts @@ -3,8 +3,49 @@ import type { Locator } from '@playwright/test'; import { OmnichannelAdministration } from './omnichannel-administration'; export class OmnichannelCannedResponses extends OmnichannelAdministration { - get radioPublic(): Locator { - return this.page.locator('[data-qa-id="canned-response-public-radio"]').first(); + get inputShortcut() { + return this.page.getByRole('textbox', { name: 'Shortcut', exact: true }); + } + + get inputMessage() { + return this.page.getByRole('textbox', { name: 'Message', exact: true }); + } + + get radioPublic() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Public' }) }); + } + + get radioDepartment() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Department' }) }); + } + + get radioPrivate() { + return this.page.locator('label', { has: this.page.getByRole('radio', { name: 'Private' }) }); + } + + get inputTags() { + return this.page.getByRole('textbox', { name: 'Tags', exact: true }); + } + + get btnAddTag() { + return this.page.getByRole('button', { name: 'Add', exact: true }); + } + + listItem(name: string) { + return this.page.getByText(`!${name}`, { exact: true }); + } + + async addTag(tag: string) { + await this.inputTags.fill(tag); + await this.btnAddTag.click(); + } + + get btnEdit() { + return this.page.getByRole('button', { name: 'Edit', exact: true }); + } + + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); } get btnNew(): Locator {