diff --git a/.changeset/odd-gorillas-obey.md b/.changeset/odd-gorillas-obey.md new file mode 100644 index 0000000000000..de32602af5c44 --- /dev/null +++ b/.changeset/odd-gorillas-obey.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes issue when trying to create an unencrypted discussion when a parent channel is encrypted diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index 3f1353be8a6c2..14b43c1e83d2d 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -67,6 +67,7 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid name: 1, t: 1, avatarETag: 1, + encrypted: 1, }, limit: 10, sort: { diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.spec.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.spec.tsx new file mode 100644 index 0000000000000..ccee1d2e3ed55 --- /dev/null +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.spec.tsx @@ -0,0 +1,101 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { composeStories } from '@storybook/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { axe } from 'jest-axe'; + +import CreateDiscussion from './CreateDiscussion'; +import * as stories from './CreateDiscussion.stories'; +import { createFakeRoom } from '../../../tests/mocks/data'; + +jest.mock('../../lib/utils/goToRoomById', () => ({ + goToRoomById: jest.fn(), +})); + +jest.mock('../../lib/rooms/roomCoordinator', () => ({ + roomCoordinator: { + getRoomDirectives: () => ({ + getRoomName: () => 'General', + }), + }, +})); + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +const room1 = createFakeRoom({ encrypted: true, fname: 'Encrypted Room 1' }); +const room2 = createFakeRoom({ encrypted: false, fname: 'Unencrypted Room 2' }); + +const appRoot = mockAppRoot() + .withEndpoint('POST', '/v1/rooms.createDiscussion', () => ({ + success: true, + discussion: { + ...createFakeRoom({ + _id: 'discussion-id', + t: 'p' as const, + name: 'discussion-name', + fname: 'Discussion Name', + prid: 'parent-room-id', + }), + rid: 'discussion-id', + } as any, + })) + .withEndpoint( + 'GET', + '/v1/rooms.autocomplete.channelAndPrivate', + () => + ({ + items: [room1, room2], + }) as any, + ) + .withTranslations('en', 'core', { + Encrypted: 'Encrypted', + Discussion_first_message_title: 'Message', + Discussion_target_channel: 'Parent channel or team', + }) + .build(); + +describe('CreateDiscussion', () => { + test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(, { wrapper: appRoot }); + expect(baseElement).toMatchSnapshot(); + }); + + test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(, { wrapper: appRoot }); + + const results = await axe(container); + expect(results).toHaveNoViolations(); + }); + + describe('Encrypted parent room behavior', () => { + it('should disable encrypted toggle and first message field when parent room is encrypted', () => { + render(, { wrapper: appRoot }); + + const encryptedToggle = screen.getByRole('checkbox', { name: 'Encrypted' }); + expect(encryptedToggle).toBeDisabled(); + + const firstMessageField = screen.getByLabelText('Message'); + expect(firstMessageField).toBeDisabled(); + }); + + it('should disable encrypted toggle and first message field when an encrypted parent room is selected', async () => { + render(, { wrapper: appRoot }); + + const parentRoomSelect = screen.getByRole('textbox', { name: 'Parent channel or team' }); + + await userEvent.click(parentRoomSelect); + + await waitFor(() => { + expect(screen.getByText('Encrypted Room 1')).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByText('Encrypted Room 1')); + + const encryptedToggle = screen.getByRole('checkbox', { name: 'Encrypted' }); + expect(encryptedToggle).toBeDisabled(); + + const firstMessageField = screen.getByLabelText('Message'); + expect(firstMessageField).toBeDisabled(); + }); + }); +}); diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx new file mode 100644 index 0000000000000..dfb5822ee9c41 --- /dev/null +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryFn } from '@storybook/react'; + +import CreateDiscussion from './CreateDiscussion'; + +export default { + component: CreateDiscussion, + parameters: { + layout: 'fullscreen', + actions: { argTypesRegex: '^on.*' }, + }, +} satisfies Meta; + +export const Default: StoryFn = (args) => ; diff --git a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx index 7aae6736909b5..fe4438f9243a2 100644 --- a/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx +++ b/apps/meteor/client/components/CreateDiscussion/CreateDiscussion.tsx @@ -20,9 +20,10 @@ import { ModalFooter, ModalFooterControllers, } from '@rocket.chat/fuselage'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useMutation } from '@tanstack/react-query'; -import { useId } from 'react'; +import { useId, useState } from 'react'; import type { ReactElement } from 'react'; import { useForm, Controller } from 'react-hook-form'; @@ -43,32 +44,50 @@ type CreateDiscussionFormValues = { type CreateDiscussionProps = { parentMessageId?: IMessage['_id']; + encryptedParentRoom?: boolean; onClose: () => void; defaultParentRoom?: IRoom['_id']; nameSuggestion?: string; }; // TODO: Replace `Modal` in favor of `GenericModal` -const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSuggestion }: CreateDiscussionProps): ReactElement => { +const CreateDiscussion = ({ + onClose, + defaultParentRoom, + parentMessageId, + nameSuggestion, + encryptedParentRoom = false, +}: CreateDiscussionProps): ReactElement => { const t = useTranslation(); + const [encryptedDisabled, setEncryptedDisabled] = useState(encryptedParentRoom); + const { formState: { errors }, handleSubmit, control, watch, + setValue, } = useForm({ mode: 'onBlur', defaultValues: { name: nameSuggestion || '', parentRoom: '', - encrypted: false, + encrypted: encryptedParentRoom, usernames: [], firstMessage: '', topic: '', }, }); + const onParentRoomChange = useEffectEvent((room: IRoom | undefined) => { + if (!room) { + return; + } + setValue('encrypted', room.encrypted === true); + setEncryptedDisabled(room.encrypted === true); + }); + const { encrypted } = watch(); const createDiscussion = useEndpoint('POST', '/v1/rooms.createDiscussion'); @@ -143,6 +162,8 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug aria-invalid={Boolean(errors.parentRoom)} aria-required='true' aria-describedby={`${parentRoomId}-error`} + setSelectedRoom={onParentRoomChange} + renderRoomIcon={({ encrypted }) => (encrypted ? : null)} /> )} /> @@ -242,7 +263,9 @@ const CreateDiscussion = ({ onClose, defaultParentRoom, parentMessageId, nameSug } + render={({ field: { value, ...field } }) => ( + + )} /> {getEncryptedHint({ isPrivate: true, encrypted })} diff --git a/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap new file mode 100644 index 0000000000000..65553ca945c97 --- /dev/null +++ b/apps/meteor/client/components/CreateDiscussion/__snapshots__/CreateDiscussion.spec.tsx.snap @@ -0,0 +1,328 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`CreateDiscussion renders Default without crashing 1`] = ` + +
+ +
+
+
+

+ Discussion_title +

+ +
+
+
+
+
+ Discussion_description +
+
+
+ + +
+
+ +
+
+ +
+
+
+
+
+ + + + +
+
+ + + + + + + Displayed_next_to_name + + +
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+
+ + +