From 09bfb793ee54d50eb54ef4e3a5eb385ea2f2fb54 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Thu, 10 Aug 2023 12:18:16 +0300 Subject: [PATCH] feat(clerk-js): List invitations in `` (#1554) * feat(clerk-js): Load invitation with infinite scrolling * chore(clerk-js): Custom useInView * feat(localizations): New invitation Org switcher keys * feat(clerk-js,types): Users can accept invitations within * fix(clerk-js,types): UserOrganizationInvitation nullish slug * fix(clerk-js): Update OrganizationPreviewProps to accept only the public organization data * test(clerk-js): Display list of invitation in OrganizationSwitcher * chore(repo): Add changeset + Add default values in useLoadingStatus * chore(clerk-js): Split `OrganizationActionList` into smaller components --- .changeset/shaggy-terms-train.md | 8 + .../OrganizationSwitcher.tsx | 8 +- .../OtherOrganizationActions.tsx | 87 +++------- .../UserInvitationList.tsx | 155 ++++++++++++++++++ .../UserMembershipList.tsx | 73 +++++++++ .../__tests__/OrganizationSwitcher.test.tsx | 44 +++++ .../OrganizationSwitcher/__tests__/utlis.ts | 36 ++++ .../ui/customizables/elementDescriptors.ts | 3 + .../src/ui/elements/OrganizationPreview.tsx | 4 +- packages/clerk-js/src/ui/hooks/index.ts | 1 + packages/clerk-js/src/ui/hooks/useInView.ts | 63 +++++++ .../clerk-js/src/ui/hooks/useLoadingStatus.ts | 3 +- packages/localizations/src/en-US.ts | 3 + packages/types/src/appearance.ts | 3 + packages/types/src/json.ts | 2 +- packages/types/src/localization.ts | 3 + .../types/src/userOrganizationInvitation.ts | 2 +- 17 files changed, 428 insertions(+), 70 deletions(-) create mode 100644 .changeset/shaggy-terms-train.md create mode 100644 packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationList.tsx create mode 100644 packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx create mode 100644 packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/utlis.ts create mode 100644 packages/clerk-js/src/ui/hooks/useInView.ts 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 ( + <> +