From 7397bedc1e0b917d400551bd64fbb0b1fbde7098 Mon Sep 17 00:00:00 2001 From: Haris Chaniotakis Date: Fri, 13 Oct 2023 22:13:53 +0300 Subject: [PATCH] feat(clerk-js,shared,types): Remove invalid email addresses from input for Organization Invitation In invite members screen of the component, consume any invalid email addresses as they are returned in the API error and remove them from the input automatically. --- .changeset/yellow-pumpkins-buy.md | 7 +++ .../OrganizationProfile/InviteMembersForm.tsx | 17 ++++-- .../__tests__/InviteMembersPage.test.tsx | 59 +++++++++++++++++++ packages/shared/src/errors/Error.ts | 1 + packages/types/src/api.ts | 1 + packages/types/src/json.ts | 1 + 6 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 .changeset/yellow-pumpkins-buy.md diff --git a/.changeset/yellow-pumpkins-buy.md b/.changeset/yellow-pumpkins-buy.md new file mode 100644 index 00000000000..7e3060e8eee --- /dev/null +++ b/.changeset/yellow-pumpkins-buy.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +'@clerk/types': patch +--- + +In invite members screen of the component, consume any invalid email addresses as they are returned in the API error and remove them from the input automatically. diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx index 035ee83edd7..ce58e431cd6 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/InviteMembersForm.tsx @@ -1,5 +1,5 @@ import { isClerkAPIResponseError } from '@clerk/shared'; -import type { MembershipRole, OrganizationResource } from '@clerk/types'; +import type { ClerkAPIError, MembershipRole, OrganizationResource } from '@clerk/types'; import React from 'react'; import { Flex, Text } from '../../customizables'; @@ -89,17 +89,16 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { .inviteMembers({ emailAddresses: emailAddressField.value.split(','), role: roleField.value as MembershipRole }) .then(onSuccess) .catch(err => { + if (isClerkAPIResponseError(err)) { + removeInvalidEmails(err.errors[0]); + } + if (isClerkAPIResponseError(err) && err.errors?.[0]?.code === 'duplicate_record') { const unlocalizedEmailsList = err.errors[0].meta?.emailAddresses || []; // Create a localized list of email addresses const localizedList = createListFormat(unlocalizedEmailsList, locale); setLocalizedEmails(localizedList); - - // Remove any invalid email address - const invalids = new Set(unlocalizedEmailsList); - const emails = emailAddressField.value.split(','); - emailAddressField.setValue(emails.filter(e => !invalids.has(e)).join(',')); } else { setLocalizedEmails(null); handleError(err, [], card.setError); @@ -107,6 +106,12 @@ export const InviteMembersForm = (props: InviteMembersFormProps) => { }); }; + const removeInvalidEmails = (err: ClerkAPIError) => { + const invalidEmails = new Set([...(err.meta?.emailAddresses ?? []), ...(err.meta?.identifiers ?? [])]); + const emails = emailAddressField.value.split(','); + emailAddressField.setValue(emails.filter(e => !invalidEmails.has(e)).join(',')); + }; + return ( <> {localizedEmails && ( 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 d3c73689864..3fc76ba84fb 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 @@ -133,6 +133,65 @@ describe('InviteMembersPage', () => { ).toBeInTheDocument(), ); }); + + it('removes duplicate emails from input after error', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', role: 'admin' }], + }); + }); + + fixtures.clerk.organization?.inviteMembers.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'duplicate_record', + long_message: + 'There are already pending invitations for the following email addresses: invalid@clerk.dev', + message: 'duplicate invitation', + meta: { email_addresses: ['invalid@clerk.dev'] }, + }, + ], + status: 400, + }), + ); + const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + await userEvent.type(getByTestId('tag-input'), 'invalid@clerk.dev'); + await userEvent.click(getByRole('button', { name: 'Send invitations' })); + + expect(getByTestId('tag-input')).not.toHaveValue(); + }); + + it('removes blocked/not allowed emails from input after error', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + organization_memberships: [{ name: 'Org1', role: 'admin' }], + }); + }); + + fixtures.clerk.organization?.inviteMembers.mockRejectedValueOnce( + new ClerkAPIResponseError('Error', { + data: [ + { + code: 'not_allowed_access', + long_message: 'blocked@clerk.dev is not allowed to access this application.', + message: 'Access not allowed.', + meta: { identifiers: ['blocked@clerk.dev'] }, + }, + ], + status: 403, + }), + ); + const { getByRole, userEvent, getByTestId } = render(, { wrapper }); + await userEvent.type(getByTestId('tag-input'), 'blocked@clerk.dev'); + await userEvent.click(getByRole('button', { name: 'Send invitations' })); + + expect(getByTestId('tag-input')).not.toHaveValue(); + }); }); describe('Navigation', () => { diff --git a/packages/shared/src/errors/Error.ts b/packages/shared/src/errors/Error.ts index 066dffa627e..7280057eed8 100644 --- a/packages/shared/src/errors/Error.ts +++ b/packages/shared/src/errors/Error.ts @@ -60,6 +60,7 @@ export function parseError(error: ClerkAPIErrorJSON): ClerkAPIError { paramName: error?.meta?.param_name, sessionId: error?.meta?.session_id, emailAddresses: error?.meta?.email_addresses, + identifiers: error?.meta?.identifiers, zxcvbn: error?.meta?.zxcvbn, }, }; diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 5796a98cbce..8146fe34ec0 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -9,6 +9,7 @@ export interface ClerkAPIError { paramName?: string; sessionId?: string; emailAddresses?: string[]; + identifiers?: string[]; zxcvbn?: { suggestions: { code: string; diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 607ffa43baa..aeb5684239f 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -267,6 +267,7 @@ export interface ClerkAPIErrorJSON { param_name?: string; session_id?: string; email_addresses?: string[]; + identifiers?: string[]; zxcvbn?: { suggestions: { code: string;