diff --git a/.changeset/nervous-flies-sleep.md b/.changeset/nervous-flies-sleep.md new file mode 100644 index 0000000000..c77a171356 --- /dev/null +++ b/.changeset/nervous-flies-sleep.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Introduce the `skipInvitationScreen` prop on `` component diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationPage.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationPage.tsx index e8d7a510d2..812d66b196 100644 --- a/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationPage.tsx +++ b/packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationPage.tsx @@ -25,7 +25,7 @@ export const CreateOrganizationPage = withCardStateProvider(() => { const [file, setFile] = React.useState(); const { createOrganization, isLoaded } = useCoreOrganizations(); const { setActive, closeCreateOrganization } = useCoreClerk(); - const { mode, navigateAfterCreateOrganization } = useCreateOrganizationContext(); + const { mode, navigateAfterCreateOrganization, skipInvitationScreen } = useCreateOrganizationContext(); const { organization } = useCoreOrganization(); const wizard = useWizard({ onNextStep: () => card.setError(undefined) }); @@ -63,7 +63,7 @@ export const CreateOrganizationPage = withCardStateProvider(() => { await setActive({ organization }); - if (organization.maxAllowedMemberships === 1) { + if (skipInvitationScreen ?? organization.maxAllowedMemberships === 1) { return completeFlow(); } diff --git a/packages/clerk-js/src/ui/components/CreateOrganization/__tests__/CreateOrganization.test.tsx b/packages/clerk-js/src/ui/components/CreateOrganization/__tests__/CreateOrganization.test.tsx new file mode 100644 index 0000000000..061f44be19 --- /dev/null +++ b/packages/clerk-js/src/ui/components/CreateOrganization/__tests__/CreateOrganization.test.tsx @@ -0,0 +1,160 @@ +import type { OrganizationResource } from '@clerk/types'; +import { describe, jest } from '@jest/globals'; +import { waitFor } from '@testing-library/dom'; +import React from 'react'; + +import { render } from '../../../../testUtils'; +import { bindCreateFixtures } from '../../../utils/test/createFixtures'; +import { CreateOrganization } from '../CreateOrganization'; + +const { createFixtures } = bindCreateFixtures('CreateOrganization'); + +type FakeOrganizationParams = { + id: string; + createdAt?: Date; + imageUrl?: string; + logoUrl?: string; + slug: string; + name: string; + membersCount: number; + pendingInvitationsCount: number; + adminDeleteEnabled: boolean; + maxAllowedMemberships: number; +}; + +const createFakeOrganization = (params: FakeOrganizationParams): OrganizationResource => { + return { + logoUrl: null, + pathRoot: '', + id: params.id, + name: params.name, + slug: params.slug, + imageUrl: params.imageUrl || '', + membersCount: params.membersCount, + pendingInvitationsCount: params.pendingInvitationsCount, + publicMetadata: {}, + adminDeleteEnabled: params.adminDeleteEnabled, + maxAllowedMemberships: params?.maxAllowedMemberships, + createdAt: params?.createdAt || new Date(), + updatedAt: new Date(), + update: jest.fn() as any, + getMemberships: jest.fn() as any, + getPendingInvitations: jest.fn() as any, + addMember: jest.fn() as any, + inviteMember: jest.fn() as any, + inviteMembers: jest.fn() as any, + updateMember: jest.fn() as any, + removeMember: jest.fn() as any, + destroy: jest.fn() as any, + setLogo: jest.fn() as any, + reload: jest.fn() as any, + }; +}; + +const getCreatedOrg = (params: Partial) => + createFakeOrganization({ + id: '1', + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + membersCount: 1, + name: 'new org', + pendingInvitationsCount: 0, + slug: 'new-org', + ...params, + }); + +describe('CreateOrganization', () => { + it('renders component', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + }); + }); + const { getByText } = render(, { wrapper }); + expect(getByText('Create Organization')).toBeInTheDocument(); + }); + + it('skips invitation screen', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + }); + }); + + fixtures.clerk.createOrganization.mockReturnValue( + Promise.resolve( + getCreatedOrg({ + maxAllowedMemberships: 3, + }), + ), + ); + + props.setProps({ skipInvitationScreen: true }); + const { getByRole, userEvent, getByLabelText, queryByText } = render(, { + wrapper, + }); + await userEvent.type(getByLabelText(/Organization name/i), 'new org'); + await userEvent.click(getByRole('button', { name: /create organization/i })); + + await waitFor(() => { + expect(queryByText(/Invite members/i)).not.toBeInTheDocument(); + }); + }); + + it('always visit invitation screen', async () => { + const { wrapper, fixtures, props } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + }); + }); + + fixtures.clerk.createOrganization.mockReturnValue( + Promise.resolve( + getCreatedOrg({ + maxAllowedMemberships: 1, + }), + ), + ); + + props.setProps({ skipInvitationScreen: false }); + const { getByRole, userEvent, getByLabelText, queryByText } = render(, { + wrapper, + }); + await userEvent.type(getByLabelText(/Organization name/i), 'new org'); + await userEvent.click(getByRole('button', { name: /create organization/i })); + + await waitFor(() => { + expect(queryByText(/Invite members/i)).toBeInTheDocument(); + }); + }); + + it('auto skip invitation screen', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withUser({ + email_addresses: ['test@clerk.dev'], + }); + }); + + fixtures.clerk.createOrganization.mockReturnValue( + Promise.resolve( + getCreatedOrg({ + maxAllowedMemberships: 1, + }), + ), + ); + + const { getByRole, userEvent, getByLabelText, queryByText } = render(, { + wrapper, + }); + await userEvent.type(getByLabelText(/Organization name/i), 'new org'); + await userEvent.click(getByRole('button', { name: /create organization/i })); + + await waitFor(() => { + expect(queryByText(/Invite members/i)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 86ad31e340..5445475b1b 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -662,6 +662,12 @@ export type CreateOrganizationProps = { * @default undefined */ afterCreateOrganizationUrl?: string; + /** + * Hides the screen for sending invitations after an organization is created. + * @default undefined When left undefined Clerk will automatically hide the screen if + * the number of max allowed members is equal to 1 + */ + skipInvitationScreen?: boolean; /** * Customisation options to fully match the Clerk components to your own brand. * These options serve as overrides and will be merged with the global `appearance`