diff --git a/.changeset/polite-ways-fetch.md b/.changeset/polite-ways-fetch.md new file mode 100644 index 00000000000..a1e72d98814 --- /dev/null +++ b/.changeset/polite-ways-fetch.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Hide "Create organization" action when user reaches organization membership limit diff --git a/packages/clerk-js/src/ui/common/CreateOrganizationAction.tsx b/packages/clerk-js/src/ui/common/CreateOrganizationAction.tsx new file mode 100644 index 00000000000..253c98b477f --- /dev/null +++ b/packages/clerk-js/src/ui/common/CreateOrganizationAction.tsx @@ -0,0 +1,26 @@ +import { useUser } from '@clerk/shared/react/index'; + +import { useEnvironment } from '../contexts'; +import { Action } from '../elements/Actions'; +import { Add } from '../icons'; + +type CreateOrganizationActionProps = Omit, 'icon'>; + +export const CreateOrganizationAction = (props: CreateOrganizationActionProps) => { + const { user } = useUser(); + const { organizationSettings } = useEnvironment(); + + const currentMembershipCount = (user?.organizationMemberships ?? []).length; + const canCreateAdditionalMembership = currentMembershipCount < organizationSettings.maxAllowedMemberships; + + if (!user?.createOrganizationEnabled || !canCreateAdditionalMembership) { + return null; + } + + return ( + + ); +}; diff --git a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx index 0d7327fe13c..1c14496b3f8 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/OrganizationListPage.tsx @@ -1,8 +1,8 @@ -import { useUser } from '@clerk/shared/react'; import { useState } from 'react'; +import { CreateOrganizationAction } from '@/ui/common/CreateOrganizationAction'; import { OrganizationPreviewSpinner } from '@/ui/common/organizations/OrganizationPreview'; -import { Action, Actions } from '@/ui/elements/Actions'; +import { Actions } from '@/ui/elements/Actions'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; @@ -10,7 +10,6 @@ import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView' import { useEnvironment, useOrganizationListContext } from '../../contexts'; import { Box, Col, descriptors, Flex, localizationKeys, Spinner } from '../../customizables'; -import { Add } from '../../icons'; import { CreateOrganizationForm } from '../CreateOrganization/CreateOrganizationForm'; import { PreviewListItems } from './shared'; import { InvitationPreview } from './UserInvitationList'; @@ -22,16 +21,9 @@ const CreateOrganizationButton = ({ }: { onCreateOrganizationClick: React.MouseEventHandler; }) => { - const { user } = useUser(); - - if (!user?.createOrganizationEnabled) { - return null; - } - return ( - ({ diff --git a/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx b/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx index 09ad363316b..85df1212bb4 100644 --- a/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationList/__tests__/OrganizationList.test.tsx @@ -324,6 +324,27 @@ describe('OrganizationList', () => { expect(queryByRole('button', { name: 'Create organization' })).not.toBeInTheDocument(); }); + it('does not display CreateOrganization action if not allowed to create additional membership', async () => { + const { wrapper } = await createFixtures(f => { + f.withOrganizations(); + f.withMaxAllowedMemberships({ max: 1 }); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + organization_memberships: [{ name: 'Org1', id: '1', role: 'admin' }], + }); + }); + + const { findByRole, queryByRole } = render(, { + wrapper, + }); + + await waitFor(async () => { + expect(await findByRole('heading', { name: /choose an account/i })).toBeInTheDocument(); + }); + expect(queryByRole('menuitem', { name: 'Create organization' })).not.toBeInTheDocument(); + }); + describe('navigation', () => { it('constructs afterSelectPersonalUrl from `:id` ', async () => { const { wrapper, fixtures, props } = await createFixtures(f => { diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx index 209505b7a11..7100e2d9207 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx @@ -1,10 +1,8 @@ -import { useUser } from '@clerk/shared/react'; import React from 'react'; -import { Action } from '@/ui/elements/Actions'; +import { CreateOrganizationAction } from '@/ui/common/CreateOrganizationAction'; import { descriptors, localizationKeys } from '../../customizables'; -import { Add } from '../../icons'; import { UserInvitationSuggestionList } from './UserInvitationSuggestionList'; import type { UserMembershipListProps } from './UserMembershipList'; import { UserMembershipList } from './UserMembershipList'; @@ -16,21 +14,14 @@ export interface OrganizationActionListProps extends UserMembershipListProps { const CreateOrganizationButton = ({ onCreateOrganizationClick, }: Pick) => { - const { user } = useUser(); - - if (!user?.createOrganizationEnabled) { - return null; - } - return ( - ({ diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx index c854f15f702..b8e35367830 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx @@ -200,6 +200,23 @@ describe('OrganizationSwitcher', () => { expect(getByText('Org2')).toBeInTheDocument(); }); + it('does not allow creating organization if not allowed to create additional membership', async () => { + const { wrapper, props } = await createFixtures(f => { + f.withOrganizations(); + f.withMaxAllowedMemberships({ max: 1 }); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + organization_memberships: [{ name: 'Org1', id: '1', role: 'admin' }], + }); + }); + + props.setProps({ hidePersonal: true }); + const { queryByText, getByRole, userEvent } = render(, { wrapper }); + await userEvent.click(getByRole('button', { name: 'Open organization switcher' })); + expect(queryByText('Create organization')).not.toBeInTheDocument(); + }); + it.each([ ['Admin', 'admin'], ['Member', 'basic_member'], diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx index 1b5e1f35d53..fcee5bc26b1 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/ChooseOrganizationScreen.tsx @@ -7,6 +7,7 @@ import type { } from '@clerk/types'; import React, { useState } from 'react'; +import { CreateOrganizationAction } from '@/ui/common/CreateOrganizationAction'; import { OrganizationPreviewButton, OrganizationPreviewListItem, @@ -18,13 +19,12 @@ import { import { organizationListParams, populateCacheUpdateItem } from '@/ui/components/OrganizationSwitcher/utils'; import { useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks'; import { Col, descriptors, localizationKeys, Text, useLocalizations } from '@/ui/customizables'; -import { Action, Actions } from '@/ui/elements/Actions'; +import { Actions } from '@/ui/elements/Actions'; import { Card } from '@/ui/elements/Card'; import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { OrganizationPreview } from '@/ui/elements/OrganizationPreview'; import { useOrganizationListInView } from '@/ui/hooks/useOrganizationListInView'; -import { Add } from '@/ui/icons'; import { useRouter } from '@/ui/router'; import { handleError } from '@/ui/utils/errorHandler'; @@ -258,16 +258,9 @@ const CreateOrganizationButton = ({ }: { onCreateOrganizationClick: React.MouseEventHandler; }) => { - const { user } = useUser(); - - if (!user?.createOrganizationEnabled) { - return null; - } - return ( - ({ diff --git a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx index b0604a57582..f26e4fae9d2 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/tasks/TaskChooseOrganization/__tests__/TaskChooseOrganization.test.tsx @@ -205,6 +205,44 @@ describe('TaskChooseOrganization', () => { expect(await findByText(/testuser/)).toBeInTheDocument(); }); + it('does not allow creating organization if not allowed to create additional membership', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.withOrganizations(); + f.withMaxAllowedMemberships({ max: 1 }); + f.withForceOrganizationSelection(); + f.withUser({ + email_addresses: ['test@clerk.com'], + create_organization_enabled: true, + tasks: [{ key: 'choose-organization' }], + }); + }); + + fixtures.clerk.user?.getOrganizationMemberships.mockReturnValueOnce( + Promise.resolve({ + data: [ + createFakeUserOrganizationMembership({ + id: '1', + organization: { + id: '1', + name: 'Existing Org', + slug: 'org1', + membersCount: 1, + adminDeleteEnabled: false, + maxAllowedMemberships: 1, + pendingInvitationsCount: 1, + }, + }), + ], + total_count: 1, + }), + ); + + const { findByText, queryByText } = render(, { wrapper }); + + expect(await findByText('Existing Org')).toBeInTheDocument(); + expect(queryByText('Create new organization')).not.toBeInTheDocument(); + }); + describe('on create organization form', () => { it("does not display slug field if it's disabled on environment", async () => { const { wrapper } = await createFixtures(f => {