diff --git a/.changeset/khaki-spoons-teach.md b/.changeset/khaki-spoons-teach.md new file mode 100644 index 0000000000..b75cfc91e6 --- /dev/null +++ b/.changeset/khaki-spoons-teach.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Require role to be selected before sending organization invite, affects `` and `. diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/InviteMembersForm.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/InviteMembersForm.tsx index dfd49f3fae..52a50ec204 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/InviteMembersForm.tsx @@ -30,10 +30,6 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { const { t, locale } = useLocalizations(); const [isValidUnsubmittedEmail, setIsValidUnsubmittedEmail] = useState(false); - if (!organization) { - return null; - } - const validateUnsubmittedEmail = (value: string) => setIsValidUnsubmittedEmail(isEmail(value)); const emailAddressField = useFormControl('emailAddress', '', { @@ -41,6 +37,14 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { label: localizationKeys('formFieldLabel__emailAddresses'), }); + const roleField = useFormControl('role', '', { + label: localizationKeys('formFieldLabel__role'), + }); + + if (!organization) { + return null; + } + const { props: { setError, @@ -58,7 +62,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { }, } = emailAddressField; - const canSubmit = !!emailAddressField.value.length || isValidUnsubmittedEmail; + const canSubmit = (!!emailAddressField.value.length || isValidUnsubmittedEmail) && !!roleField.value; const onSubmit = (e: FormEvent) => { e.preventDefault(); @@ -122,7 +126,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { /> - + { ); }; -const AsyncRoleSelect = () => { +const AsyncRoleSelect = (field: ReturnType>) => { const { options, isLoading } = useFetchRoles(); - const roleField = useFormControl('role', '', { - label: localizationKeys('formFieldLabel__role'), - }); return ( - + - + roleField.setValue(value)} + onChange={value => field.setValue(value)} triggerSx={t => ({ width: t.sizes.$48, justifyContent: 'space-between', display: 'flex' })} optionListSx={t => ({ minWidth: t.sizes.$48 })} /> diff --git a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx index 1784f139d3..9a1b97ce9e 100644 --- a/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx +++ b/packages/clerk-js/src/ui.retheme/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx @@ -24,7 +24,7 @@ describe('InviteMembersPage', () => { }); describe('Submitting', () => { - it('enables the Send button when one or more email has been entered', async () => { + it('keeps the Send button disabled until a role is selected and one or more email has been entered', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ @@ -34,10 +34,14 @@ describe('InviteMembersPage', () => { }); fixtures.clerk.organization?.getRoles.mockRejectedValue(null); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByText, getByRole, userEvent, getByTestId } = render(, { wrapper }); expect(getByRole('button', { name: 'Send invitations' })).toBeDisabled(); await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,'); + expect(getByRole('button', { name: 'Send invitations' })).toBeDisabled(); + + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); expect(getByRole('button', { name: 'Send invitations' })).not.toBeDisabled(); }); @@ -172,6 +176,9 @@ describe('InviteMembersPage', () => { ); const { getByRole, userEvent, getByText, getByTestId } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); await waitFor(() => expect( @@ -206,8 +213,11 @@ describe('InviteMembersPage', () => { status: 400, }), ); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByRole, userEvent, getByTestId, getByText } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'invalid@clerk.dev'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); expect(getByTestId('tag-input')).not.toHaveValue(); @@ -236,8 +246,11 @@ describe('InviteMembersPage', () => { status: 403, }), ); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByRole, getByText, userEvent, getByTestId } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'blocked@clerk.dev'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); expect(getByTestId('tag-input')).not.toHaveValue(); diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx index dfd49f3fae..52a50ec204 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx @@ -30,10 +30,6 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { const { t, locale } = useLocalizations(); const [isValidUnsubmittedEmail, setIsValidUnsubmittedEmail] = useState(false); - if (!organization) { - return null; - } - const validateUnsubmittedEmail = (value: string) => setIsValidUnsubmittedEmail(isEmail(value)); const emailAddressField = useFormControl('emailAddress', '', { @@ -41,6 +37,14 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { label: localizationKeys('formFieldLabel__emailAddresses'), }); + const roleField = useFormControl('role', '', { + label: localizationKeys('formFieldLabel__role'), + }); + + if (!organization) { + return null; + } + const { props: { setError, @@ -58,7 +62,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { }, } = emailAddressField; - const canSubmit = !!emailAddressField.value.length || isValidUnsubmittedEmail; + const canSubmit = (!!emailAddressField.value.length || isValidUnsubmittedEmail) && !!roleField.value; const onSubmit = (e: FormEvent) => { e.preventDefault(); @@ -122,7 +126,7 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { /> - + { ); }; -const AsyncRoleSelect = () => { +const AsyncRoleSelect = (field: ReturnType>) => { const { options, isLoading } = useFetchRoles(); - const roleField = useFormControl('role', '', { - label: localizationKeys('formFieldLabel__role'), - }); return ( - + - + roleField.setValue(value)} + onChange={value => field.setValue(value)} triggerSx={t => ({ width: t.sizes.$48, justifyContent: 'space-between', display: 'flex' })} optionListSx={t => ({ minWidth: t.sizes.$48 })} /> diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx index 1784f139d3..9a1b97ce9e 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/InviteMembersPage.test.tsx @@ -24,7 +24,7 @@ describe('InviteMembersPage', () => { }); describe('Submitting', () => { - it('enables the Send button when one or more email has been entered', async () => { + it('keeps the Send button disabled until a role is selected and one or more email has been entered', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.withOrganizations(); f.withUser({ @@ -34,10 +34,14 @@ describe('InviteMembersPage', () => { }); fixtures.clerk.organization?.getRoles.mockRejectedValue(null); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByText, getByRole, userEvent, getByTestId } = render(, { wrapper }); expect(getByRole('button', { name: 'Send invitations' })).toBeDisabled(); await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,'); + expect(getByRole('button', { name: 'Send invitations' })).toBeDisabled(); + + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); expect(getByRole('button', { name: 'Send invitations' })).not.toBeDisabled(); }); @@ -172,6 +176,9 @@ describe('InviteMembersPage', () => { ); const { getByRole, userEvent, getByText, getByTestId } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'test+1@clerk.com,'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); await waitFor(() => expect( @@ -206,8 +213,11 @@ describe('InviteMembersPage', () => { status: 400, }), ); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByRole, userEvent, getByTestId, getByText } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'invalid@clerk.dev'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); expect(getByTestId('tag-input')).not.toHaveValue(); @@ -236,8 +246,11 @@ describe('InviteMembersPage', () => { status: 403, }), ); - const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + const { getByRole, getByText, userEvent, getByTestId } = render(, { wrapper }); await userEvent.type(getByTestId('tag-input'), 'blocked@clerk.dev'); + await waitFor(() => expect(getByRole('button', { name: /select an option/i })).not.toBeDisabled()); + await userEvent.click(getByRole('button', { name: /select an option/i })); + await userEvent.click(getByText(/^member$/i)); await userEvent.click(getByRole('button', { name: 'Send invitations' })); expect(getByTestId('tag-input')).not.toHaveValue();