Skip to content

chore: apply useQuery on InviteUsers #35861

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 29, 2025
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
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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(
() => [
Expand Down Expand Up @@ -44,25 +46,29 @@ const EditInviteLink = ({ daysAndMaxUses, onClickNewLink }: EditInviteLinkProps)
return (
<>
<Field>
<FieldLabel flexGrow={0}>{t('Expiration_(Days)')}</FieldLabel>
<FieldLabel htmlFor={expirationId} flexGrow={0}>
{t('Expiration_(Days)')}
</FieldLabel>
<FieldRow>
<Controller
name='days'
control={control}
render={({ field: { onChange, value, name } }): ReactElement => (
<Select name={name} value={value} onChange={onChange} options={daysOptions} />
<Select id={expirationId} name={name} value={value} onChange={onChange} options={daysOptions} />
)}
/>
</FieldRow>
</Field>
<Field>
<FieldLabel flexGrow={0}>{t('Max_number_of_uses')}</FieldLabel>
<FieldLabel htmlFor={maxUsesId} flexGrow={0}>
{t('Max_number_of_uses')}
</FieldLabel>
<FieldRow>
<Controller
name='maxUses'
control={control}
render={({ field: { onChange, value, name } }): ReactElement => (
<Select name={name} value={value} onChange={onChange} options={maxUsesOptions} />
<Select id={maxUsesId} name={name} value={value} onChange={onChange} options={maxUsesOptions} />
)}
/>
</FieldRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,14 +13,19 @@ type InviteLinkProps = {
const InviteLink = ({ linkText, captionText, onClickEdit }: InviteLinkProps): ReactElement => {
const { t } = useTranslation();
const { copy } = useClipboardWithToast(linkText);
const inviteLinkId = useId();

return (
<>
<Field>
<FieldLabel flexGrow={0}>{t('Invite_Link')}</FieldLabel>
<FieldLabel htmlFor={inviteLinkId} flexGrow={0}>
{t('Invite_Link')}
</FieldLabel>
<FieldRow>
{!linkText && <InputBox.Skeleton />}
{linkText && <UrlInput value={linkText} addon={<Icon onClick={(): Promise<void> => copy()} name='copy' size='x16' />} />}
{linkText && (
<UrlInput id={inviteLinkId} value={linkText} addon={<Icon onClick={(): Promise<void> => copy()} name='copy' size='x16' />} />
)}
</FieldRow>
{captionText && (
<Box pb={8} color='annotation' fontScale='c2'>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(<Story />);
expect(view.baseElement).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />);

/**
** 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();
});
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -10,12 +14,31 @@ export default {
layout: 'fullscreen',
actions: { argTypesRegex: '^on.*' },
},
decorators: [(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>],
decorators: [
(fn) => <Contextualbar height='100vh'>{fn()}</Contextualbar>,
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<typeof InviteUsers>;

export const Default: StoryFn<typeof InviteUsers> = (args) => <InviteUsers {...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<typeof InviteUsersEdit> = (args) => (
<InviteUsersEdit {...args} daysAndMaxUses={{ days: '1', maxUses: '5' }} />
);

export const InviteLoading: StoryFn<typeof InviteUsersLoading> = (args) => <InviteUsersLoading {...args} />;

export const InviteError: StoryFn<typeof InviteUsersError> = (args) => <InviteUsersError {...args} error={new Error('Error message')} />;
Original file line number Diff line number Diff line change
@@ -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 (
<>
<ContextualbarHeader>
{(onClickBackMembers || onClickBackLink) && <ContextualbarBack onClick={isEditing ? onClickBackLink : onClickBackMembers} />}
<ContextualbarTitle>{t('Invite_Users')}</ContextualbarTitle>
{onClose && <ContextualbarClose onClick={onClose} />}
</ContextualbarHeader>
<ContextualbarScrollableContent>
{error && <Callout type='danger'>{error.toString()}</Callout>}
{isEditing && !error && <EditInviteLink onClickNewLink={onClickNewLink} daysAndMaxUses={daysAndMaxUses} />}
{!isEditing && !error && <InviteLink captionText={captionText} onClickEdit={onClickEdit} linkText={linkText} />}
</ContextualbarScrollableContent>
</>
);
};
const InviteUsers = ({ onClickBackMembers, onClose, onClickEdit, captionText, linkText }: InviteUsersProps): ReactElement => (
<InviteUsersWrapper onClickBack={onClickBackMembers} onClose={onClose}>
<InviteLink captionText={captionText} onClickEdit={onClickEdit} linkText={linkText} />
</InviteUsersWrapper>
);

export default InviteUsers;
Original file line number Diff line number Diff line change
@@ -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 (
<InviteUsersWrapper onClickBack={onClickBackLink} onClose={onClose}>
<EditInviteLink onClickNewLink={onClickNewLink} daysAndMaxUses={daysAndMaxUses} />
</InviteUsersWrapper>
);
};

export default InviteUsersEdit;
Original file line number Diff line number Diff line change
@@ -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 => (
<InviteUsersWrapper onClose={onClose} onClickBack={onClickBack}>
<Callout type='danger'>{(error || '').toString()}</Callout>
</InviteUsersWrapper>
);

export default InviteUsersError;
Original file line number Diff line number Diff line change
@@ -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 => (
<InviteUsersWrapper onClose={onClose} onClickBack={onClickBackMembers}>
<Skeleton w='full' />
</InviteUsersWrapper>
);

export default InviteUsersLoading;
Loading
Loading