diff --git a/.changeset/shaggy-terms-train.md b/.changeset/shaggy-terms-train.md new file mode 100644 index 0000000000..909b9d7c80 --- /dev/null +++ b/.changeset/shaggy-terms-train.md @@ -0,0 +1,8 @@ +--- +'@clerk/localizations': patch +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Introduces an invitation list within ++ Users can accept the invitation that is sent to them diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx index f2dc5c28a2..b318712736 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -1,5 +1,5 @@ import { withOrganizationsEnabledGuard } from '../../common'; -import { withCoreUserGuard } from '../../contexts'; +import { useCoreOrganizationList, withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; import { Popover, withCardStateProvider, withFloatingTree } from '../../elements'; import { usePopover } from '../../hooks'; @@ -12,6 +12,12 @@ const _OrganizationSwitcher = withFloatingTree(() => { offset: 8, }); + useCoreOrganizationList({ + userInvitations: { + infinite: true, + }, + }); + return ( unknown; -}; +} -export const OrganizationActionList = (props: OrganizationActionListProps) => { - const { onCreateOrganizationClick, onPersonalWorkspaceClick, onOrganizationClick } = props; - const { organizationList } = useCoreOrganizationList(); - const { organization: currentOrg } = useCoreOrganization(); +const CreateOrganizationButton = ({ + onCreateOrganizationClick, +}: Pick) => { const user = useCoreUser(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { username, primaryEmailAddress, primaryPhoneNumber, ...userWithoutIdentifiers } = user; - const { hidePersonal } = useOrganizationSwitcherContext(); - const otherOrgs = (organizationList || []).map(e => e.organization).filter(o => o.id !== currentOrg?.id); + if (!user.createOrganizationEnabled) { + return null; + } - const createOrganizationButton = ( + return ( { onClick={onCreateOrganizationClick} /> ); +}; + +export const OrganizationActionList = (props: OrganizationActionListProps) => { + const { onCreateOrganizationClick, onPersonalWorkspaceClick, onOrganizationClick } = props; return ( - ({ - maxHeight: `calc(4 * ${t.sizes.$12})`, - overflowY: 'auto', - ...common.unstyledScrollbar(t), - })} - > - {currentOrg && !hidePersonal && ( - - ({ margin: `0 calc(${t.space.$3}/2)` })} - title={localizationKeys('organizationSwitcher.personalWorkspace')} - /> - - )} - {otherOrgs.map(organization => ( - onOrganizationClick(organization)} - > - ({ margin: `0 calc(${t.space.$3}/2)` })} - organization={organization} - size='sm' - /> - - ))} - - {user.createOrganizationEnabled && createOrganizationButton} + + + ); }; diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationList.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationList.tsx new file mode 100644 index 0000000000..966640ffe5 --- /dev/null +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationList.tsx @@ -0,0 +1,155 @@ +import type { UserOrganizationInvitationResource } from '@clerk/types'; + +import { useCoreOrganizationList } from '../../contexts'; +import { Box, Button, descriptors, Flex, localizationKeys, Spinner, Text } from '../../customizables'; +import { OrganizationPreview, useCardState, withCardStateProvider } from '../../elements'; +import { useInView } from '../../hooks'; +import { common } from '../../styledSystem'; +import { handleError } from '../../utils'; + +export const UserInvitationList = () => { + const { ref } = useInView({ + threshold: 0, + onChange: inView => { + if (inView) { + userInvitations.fetchNext?.(); + } + }, + }); + + const { userInvitations } = useCoreOrganizationList({ + userInvitations: { + infinite: true, + }, + }); + + if ((userInvitations.count ?? 0) === 0) { + return null; + } + + return ( + + ({ + minHeight: 'unset', + height: t.space.$12, + padding: `${t.space.$3} ${t.space.$6}`, + display: 'flex', + alignItems: 'center', + })} + // Handle plurals + localizationKey={localizationKeys( + (userInvitations.count ?? 0) > 1 + ? 'organizationSwitcher.invitationCountLabel_many' + : 'organizationSwitcher.invitationCountLabel_single', + { + count: userInvitations.count, + }, + )} + /> + ({ + maxHeight: `calc(4 * ${t.sizes.$12})`, + overflowY: 'auto', + ...common.unstyledScrollbar(t), + })} + > + {userInvitations?.data?.map(inv => { + return ( + + ); + })} + + {(userInvitations.hasNextPage || userInvitations.isFetching) && ( + ({ + width: '100%', + height: t.space.$12, + position: 'relative', + })} + > + + + + + )} + + + ); +}; + +const AcceptRejectInvitationButtons = (props: UserOrganizationInvitationResource) => { + const card = useCardState(); + const { userInvitations } = useCoreOrganizationList({ + userInvitations: { + infinite: true, + }, + }); + + const mutateSwrState = () => { + (userInvitations as any)?.unstable__mutate?.(); + }; + + const handleAccept = () => { + return card + .runAsync(props.accept()) + .then(mutateSwrState) + .catch(err => handleError(err, [], card.setError)); + }; + + return ( + <> +