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 + + + + + Parent channel or team + + * + + + + + + + + + + + + + + + + + + Name + + * + + + + + + + + + + + + + + + + Topic + + + + + + + Displayed_next_to_name + + + + + + Members + + + + + + + + + + + + + + + + + + + Message + + + + + + First_message_hint + + + + + + Encrypted + + + + + + + + Not_available_for_this_workspace + + + + + + + + + + +`; diff --git a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx index ee41f907d7ab3..f36e9f3ea252e 100644 --- a/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx +++ b/apps/meteor/client/components/RoomAutoComplete/RoomAutoComplete.tsx @@ -4,7 +4,7 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { RoomAvatar } from '@rocket.chat/ui-avatar'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import type { ComponentProps, Dispatch, ReactElement, SetStateAction } from 'react'; +import type { ComponentProps, ReactElement } from 'react'; import { memo, useMemo, useState } from 'react'; const generateQuery = ( @@ -16,7 +16,7 @@ const generateQuery = ( type RoomAutoCompleteProps = Omit, 'filter'> & { scope?: 'admin' | 'regular'; renderRoomIcon?: (props: { encrypted: IRoom['encrypted']; type: IRoom['t'] }) => ReactElement | null; - setSelectedRoom?: Dispatch>; + setSelectedRoom?: (room: IRoom | undefined) => void; }; const AVATAR_SIZE = 'x20'; @@ -80,7 +80,16 @@ const RoomAutoComplete = ({ value, onChange, scope = 'regular', renderRoomIcon, > )} renderItem={({ value, label, ...props }) => ( - } /> + + {label?.name} + {renderRoomIcon?.({ ...label })} + > + } + avatar={} + /> )} options={options} /> diff --git a/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx index bfc44de422388..1087c6f29fdb1 100644 --- a/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useNewDiscussionMessageAction.tsx @@ -60,6 +60,7 @@ export const useNewDiscussionMessageAction = ( onClose={() => setModal(undefined)} parentMessageId={message._id} nameSuggestion={message?.msg?.substr(0, 140)} + encryptedParentRoom={room?.encrypted} />, ); }, diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx index 4c10012e7b394..29147dbb2d63c 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useCreateDiscussionAction.tsx @@ -14,7 +14,9 @@ export const useCreateDiscussionAction = (room?: IRoom): GenericMenuItemProps => } const handleCreateDiscussion = () => - setModal( setModal(null)} defaultParentRoom={room?.prid || room?._id} />); + setModal( + setModal(null)} defaultParentRoom={room?.prid || room?._id} encryptedParentRoom={room?.encrypted} />, + ); const discussionEnabled = useSetting('Discussion_enabled', true); const canStartDiscussion = usePermission('start-discussion', room._id);