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/good-bobcats-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes a GUI crash when editing a canned response with tags via room contextual bar.
7 changes: 5 additions & 2 deletions apps/meteor/client/components/Omnichannel/Tags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -66,13 +67,14 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps)

return (
<>
<FieldLabel required={tagRequired} mb={4}>
<FieldLabel htmlFor={tagsFieldId} required={tagRequired} mb={4}>
{t('Tags')}
</FieldLabel>

{tagsResult?.tags && tagsResult?.tags.length ? (
<FieldRow>
<CurrentChatTags
id={tagsFieldId}
value={paginatedTagValue}
handler={(tags: { label: string; value: string }[]): void => {
handler(tags.map((tag) => tag.label));
Expand All @@ -87,6 +89,7 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps)
<TextInput
error={error}
value={tagValue}
id={tagsFieldId}
onChange={({ currentTarget }: ChangeEvent<HTMLInputElement>): void => handleTagValue(currentTarget.value)}
flexGrow={1}
placeholder={t('Enter_a_tag')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -17,6 +18,7 @@ const CurrentChatTags = ({ value, handler, department, viewAll }: CurrentChatTag

return (
<AutoCompleteTagsMultiple
id={id}
onChange={handler as any} // FIXME: any
value={value}
department={department}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,22 @@ import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.ch
import { Box } from '@rocket.chat/fuselage';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { memo, useCallback } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { FormProvider, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';

import GenericError from '../../../../components/GenericError';
import GenericModal from '../../../../components/GenericModal';
import type { CannedResponseEditFormData } from '../../CannedResponseEdit';
import CannedResponseForm from '../../components/CannedResponseForm';

export type CreateCannedResponseModalFormData = {
_id: string;
shortcut: string;
text: string;
tags: {
label: string;
value: string;
}[];
scope: string;
departmentId: string;
};

const getInitialData = (
cannedResponseData: (IOmnichannelCannedResponse & { departmentName: ILivechatDepartment['name'] }) | undefined,
) => ({
_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 || '',
});
Expand All @@ -44,7 +32,7 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi
const { t } = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();

const methods = useForm<CreateCannedResponseModalFormData>({ defaultValues: getInitialData(cannedResponseData) });
const methods = useForm<CannedResponseEditFormData>({ defaultValues: getInitialData(cannedResponseData) });
const {
handleSubmit,
formState: { isDirty },
Expand All @@ -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,
Expand Down Expand Up @@ -83,9 +71,11 @@ const CreateCannedResponseModal = ({ cannedResponseData, onClose, reloadCannedLi
title={cannedResponseData?._id ? t('Edit_Canned_Response') : t('Create_canned_response')}
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(handleCreate)} {...props} />}
>
<FormProvider {...methods}>
<CannedResponseForm />
</FormProvider>
<ErrorBoundary fallbackRender={() => <GenericError icon='circle-exclamation' />}>
<FormProvider {...methods}>
<CannedResponseForm />
</FormProvider>
</ErrorBoundary>
</GenericModal>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,6 +18,7 @@ type AutoCompleteTagsMultipleProps = {
};

const AutoCompleteTagsMultiple = ({
id,
value = [],
onlyMyTags = false,
onChange = () => undefined,
Expand Down Expand Up @@ -44,6 +46,7 @@ const AutoCompleteTagsMultiple = ({

return (
<PaginatedMultiSelectFiltered
id={id}
withTitle
value={value}
onChange={onChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { faker } from '@faker-js/faker';
import type { Page } from '@playwright/test';

import { createFakeVisitor } from '../../mocks/data';
import { IS_EE } from '../config/constants';
import { createAuxContext } from '../fixtures/createAuxContext';
import { Users } from '../fixtures/userStates';
import { OmnichannelLiveChat, HomeChannel } from '../page-objects';
import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects';
import { test } from '../utils/test';

test.describe('Omnichannel Canned Responses Sidebar', () => {
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();
Expand All @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading