diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/EditInviteLink.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/EditInviteLink.tsx index a7698c83fbb81..a666d287d883d 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/EditInviteLink.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/EditInviteLink.tsx @@ -1,7 +1,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, Field, FieldLabel, FieldRow, Select, Button } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import { useMemo } from 'react'; +import { useId, useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -17,6 +17,8 @@ const EditInviteLink = ({ daysAndMaxUses, onClickNewLink }: EditInviteLinkProps) formState: { isDirty, isSubmitting }, control, } = useForm({ defaultValues: { days: daysAndMaxUses.days, maxUses: daysAndMaxUses.maxUses } }); + const expirationId = useId(); + const maxUsesId = useId(); const daysOptions: SelectOption[] = useMemo( () => [ @@ -44,25 +46,29 @@ const EditInviteLink = ({ daysAndMaxUses, onClickNewLink }: EditInviteLinkProps) return ( <> - {t('Expiration_(Days)')} + + {t('Expiration_(Days)')} + ( - )} /> - {t('Max_number_of_uses')} + + {t('Max_number_of_uses')} + ( - )} /> diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteLink.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteLink.tsx index 5535b952417c4..7de3980a9960e 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteLink.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteLink.tsx @@ -1,5 +1,5 @@ import { Box, Field, FieldLabel, FieldRow, UrlInput, Icon, Button, InputBox } from '@rocket.chat/fuselage'; -import type { ReactElement } from 'react'; +import { useId, type ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; import useClipboardWithToast from '../../../../../hooks/useClipboardWithToast'; @@ -13,14 +13,19 @@ type InviteLinkProps = { const InviteLink = ({ linkText, captionText, onClickEdit }: InviteLinkProps): ReactElement => { const { t } = useTranslation(); const { copy } = useClipboardWithToast(linkText); + const inviteLinkId = useId(); return ( <> - {t('Invite_Link')} + + {t('Invite_Link')} + {!linkText && } - {linkText && => copy()} name='copy' size='x16' />} />} + {linkText && ( + => copy()} name='copy' size='x16' />} /> + )} {captionText && ( diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.spec.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.spec.tsx new file mode 100644 index 0000000000000..f61063044669e --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.spec.tsx @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './InviteUsers.stories'; + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); + +beforeEach(() => { + jest.mock('react', () => ({ + ...jest.requireActual('react'), + useId: () => 1, + })); +}); + +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const view = render(); + expect(view.baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + /** + ** Disable 'nested-interactive' rule because our `Select` component is still not a11y compliant + **/ + const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } }); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx index bfa75ca775f94..c7b2c635f5354 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.stories.tsx @@ -1,6 +1,10 @@ +import { mockAppRoot } from '@rocket.chat/mock-providers'; import type { Meta, StoryFn } from '@storybook/react'; import InviteUsers from './InviteUsers'; +import InviteUsersEdit from './InviteUsersEdit'; +import InviteUsersError from './InviteUsersError'; +import InviteUsersLoading from './InviteUsersLoading'; import { Contextualbar } from '../../../../../components/Contextualbar'; export default { @@ -10,12 +14,31 @@ export default { layout: 'fullscreen', actions: { argTypesRegex: '^on.*' }, }, - decorators: [(fn) => {fn()}], + decorators: [ + (fn) => {fn()}, + mockAppRoot() + .withTranslations('en', 'core', { + 'Edit_Invite': 'Edit invite', + 'Invite_Users': 'Invite users', + 'Invite_Link': 'Invite link', + 'Expiration_(Days)': 'Expiration (Days)', + 'Max_number_of_uses': 'Max number of uses', + }) + .buildStoryDecorator(), + ], } satisfies Meta; export const Default: StoryFn = (args) => ; -Default.storyName = 'InviteUsers'; +Default.storyName = 'Invite Link'; Default.args = { linkText: 'https://go.rocket.chat/invite?host=open.rocket.chat&path=invite%2F5sBs3a', captionText: 'Expire on February 4, 2020 4:45 PM.', }; + +export const InviteEdit: StoryFn = (args) => ( + +); + +export const InviteLoading: StoryFn = (args) => ; + +export const InviteError: StoryFn = (args) => ; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx index 2a8648152ec60..d8c13278b58da 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsers.tsx @@ -1,58 +1,20 @@ -import { Callout } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; -import EditInviteLink from './EditInviteLink'; import InviteLink from './InviteLink'; -import { - ContextualbarHeader, - ContextualbarTitle, - ContextualbarBack, - ContextualbarClose, - ContextualbarScrollableContent, -} from '../../../../../components/Contextualbar'; +import InviteUsersWrapper from './InviteUsersWrapper'; type InviteUsersProps = { onClickBackMembers?: () => void; - onClickBackLink?: () => void; - onClickNewLink: (daysAndMaxUses: { days: string; maxUses: string }) => void; onClose: () => void; - isEditing: boolean; - daysAndMaxUses: { days: string; maxUses: string }; onClickEdit: () => void; captionText: string; linkText: string; - error?: Error; }; -const InviteUsers = ({ - onClickBackMembers, - onClickBackLink, - onClickNewLink, - onClose, - isEditing, - onClickEdit, - daysAndMaxUses, - captionText, - linkText, - error, -}: InviteUsersProps): ReactElement => { - const { t } = useTranslation(); - - return ( - <> - - {(onClickBackMembers || onClickBackLink) && } - {t('Invite_Users')} - {onClose && } - - - {error && {error.toString()}} - {isEditing && !error && } - {!isEditing && !error && } - - - ); -}; +const InviteUsers = ({ onClickBackMembers, onClose, onClickEdit, captionText, linkText }: InviteUsersProps): ReactElement => ( + + + +); export default InviteUsers; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersEdit.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersEdit.tsx new file mode 100644 index 0000000000000..0c9a0bb71e80a --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersEdit.tsx @@ -0,0 +1,21 @@ +import type { ReactElement } from 'react'; + +import EditInviteLink from './EditInviteLink'; +import InviteUsersWrapper from './InviteUsersWrapper'; + +type InviteUsersEditProps = { + onClickBackLink?: () => void; + onClickNewLink: (daysAndMaxUses: { days: string; maxUses: string }) => void; + onClose: () => void; + daysAndMaxUses: { days: string; maxUses: string }; +}; + +const InviteUsersEdit = ({ onClickBackLink, onClickNewLink, onClose, daysAndMaxUses }: InviteUsersEditProps): ReactElement => { + return ( + + + + ); +}; + +export default InviteUsersEdit; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersError.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersError.tsx new file mode 100644 index 0000000000000..3ffb5efe1b6e4 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersError.tsx @@ -0,0 +1,18 @@ +import { Callout } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +import InviteUsersWrapper from './InviteUsersWrapper'; + +type InviteUsersProps = { + onClose: () => void; + error: Error; + onClickBack?: (() => void) | undefined; +}; + +const InviteUsersError = ({ onClose, error, onClickBack }: InviteUsersProps): ReactElement => ( + + {(error || '').toString()} + +); + +export default InviteUsersError; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersLoading.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersLoading.tsx new file mode 100644 index 0000000000000..67d80d0c0a87d --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersLoading.tsx @@ -0,0 +1,17 @@ +import { Skeleton } from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; + +import InviteUsersWrapper from './InviteUsersWrapper'; + +type InviteUsersProps = { + onClose: () => void; + onClickBack: (() => void) | undefined; +}; + +const InviteUsersLoading = ({ onClose, onClickBack: onClickBackMembers }: InviteUsersProps): ReactElement => ( + + + +); + +export default InviteUsersLoading; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx index 56b83d1975df4..f514b3b3fc4fd 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWithData.tsx @@ -1,10 +1,14 @@ import type { IRoom } from '@rocket.chat/core-typings'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useTranslation, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import { useState, useEffect } from 'react'; import InviteUsers from './InviteUsers'; +import InviteUsersEdit from './InviteUsersEdit'; +import InviteUsersError from './InviteUsersError'; +import InviteUsersLoading from './InviteUsersLoading'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import { useRoomToolbox } from '../../../contexts/RoomToolboxContext'; @@ -19,18 +23,12 @@ const InviteUsersWithData = ({ rid, onClickBack }: InviteUsersWithDataProps): Re const [ { isEditing, - url, - caption, - error, daysAndMaxUses: { days, maxUses }, }, setInviteState, ] = useState({ isEditing: false, daysAndMaxUses: { days: '1', maxUses: '0' }, - url: '', - caption: '', - error: undefined as Error | undefined, }); const { closeTab } = useRoomToolbox(); @@ -41,7 +39,7 @@ const InviteUsersWithData = ({ rid, onClickBack }: InviteUsersWithDataProps): Re const handleBackToLink = useEffectEvent(() => setInviteState((prevState) => ({ ...prevState, isEditing: false }))); const linkExpirationText = useEffectEvent( - (data: { + (data?: { days: number; maxUses: number; rid: string; @@ -79,36 +77,53 @@ const InviteUsersWithData = ({ rid, onClickBack }: InviteUsersWithDataProps): Re }, ); + const { data, isSuccess, error, isError, isLoading } = useQuery({ + queryKey: ['findOrCreateInvite', days, maxUses], + queryFn: async () => findOrCreateInvite({ rid, days: Number(days), maxUses: Number(maxUses) }), + }); + useEffect(() => { - (async (): Promise => { - try { - const data = await findOrCreateInvite({ rid, days: Number(days), maxUses: Number(maxUses) }); - setInviteState((prevState) => ({ ...prevState, url: data?.url, caption: linkExpirationText(data) })); - dispatchToastMessage({ type: 'success', message: t('Invite_link_generated') }); - } catch (error) { - setInviteState((prevState) => ({ ...prevState, error: error as Error })); - } - })(); - }, [dispatchToastMessage, t, findOrCreateInvite, linkExpirationText, rid, days, maxUses]); + if (isSuccess) { + dispatchToastMessage({ type: 'success', message: t('Invite_link_generated') }); + } + }, [dispatchToastMessage, isSuccess, t]); const handleGenerateLink = useEffectEvent((daysAndMaxUses: { days: string; maxUses: string }) => { setInviteState((prevState) => ({ ...prevState, daysAndMaxUses, isEditing: false })); }); - return ( - - ); + if (isError) { + return ; + } + + if (isLoading) { + return ; + } + + if (isEditing) { + return ( + + ); + } + + if (isSuccess) { + return ( + + ); + } + + return ; }; export default InviteUsersWithData; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWrapper.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWrapper.tsx new file mode 100644 index 0000000000000..ad330f0af83bc --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/InviteUsersWrapper.tsx @@ -0,0 +1,33 @@ +import type { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + ContextualbarHeader, + ContextualbarTitle, + ContextualbarBack, + ContextualbarClose, + ContextualbarScrollableContent, +} from '../../../../../components/Contextualbar'; + +type InviteUsersWrapperProps = { + children: ReactElement; + onClickBack: (() => void) | undefined; + onClose: () => void; +}; + +const InviteUsersWrapper = ({ children, onClickBack, onClose }: InviteUsersWrapperProps): ReactElement => { + const { t } = useTranslation(); + + return ( + <> + + + {t('Invite_Users')} + + + {children} + + ); +}; + +export default InviteUsersWrapper; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/__snapshots__/InviteUsers.spec.tsx.snap b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/__snapshots__/InviteUsers.spec.tsx.snap new file mode 100644 index 0000000000000..1e741aeddd016 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/InviteUsers/__snapshots__/InviteUsers.spec.tsx.snap @@ -0,0 +1,667 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders Default without crashing 1`] = ` + +
+
+
+
+ +
+ Invite users +
+ +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+ Expire on February 4, 2020 4:45 PM. +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`renders InviteEdit without crashing 1`] = ` + +
+
+
+
+ +
+ Invite users +
+ +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+
+ + + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`renders InviteError without crashing 1`] = ` + +
+
+
+
+ +
+ Invite users +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ Error: Error message +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`; + +exports[`renders InviteLoading without crashing 1`] = ` + +
+
+
+
+ +
+ Invite users +
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +`;