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();