-
Notifications
You must be signed in to change notification settings - Fork 252
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(clerk-js): List invitations in
<OrganizationSwitcher />
(#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 <OrganizationSwitcher/> * 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
- Loading branch information
1 parent
34da40a
commit 09bfb79
Showing
17 changed files
with
428 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
'@clerk/localizations': patch | ||
'@clerk/clerk-js': patch | ||
'@clerk/types': patch | ||
--- | ||
|
||
Introduces an invitation list within <OrganizationSwitcher/> | ||
+ Users can accept the invitation that is sent to them |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<Flex | ||
direction='col' | ||
elementDescriptor={descriptors.organizationSwitcherPopoverInvitationActions} | ||
> | ||
<Text | ||
variant='smallRegular' | ||
sx={t => ({ | ||
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, | ||
}, | ||
)} | ||
/> | ||
<Box | ||
sx={t => ({ | ||
maxHeight: `calc(4 * ${t.sizes.$12})`, | ||
overflowY: 'auto', | ||
...common.unstyledScrollbar(t), | ||
})} | ||
> | ||
{userInvitations?.data?.map(inv => { | ||
return ( | ||
<InvitationPreview | ||
key={inv.id} | ||
{...inv} | ||
/> | ||
); | ||
})} | ||
|
||
{(userInvitations.hasNextPage || userInvitations.isFetching) && ( | ||
<Box | ||
ref={ref} | ||
sx={t => ({ | ||
width: '100%', | ||
height: t.space.$12, | ||
position: 'relative', | ||
})} | ||
> | ||
<Box | ||
sx={{ | ||
margin: 'auto', | ||
position: 'absolute', | ||
left: '50%', | ||
top: '50%', | ||
transform: 'translateY(-50%) translateX(-50%)', | ||
}} | ||
> | ||
<Spinner | ||
size='md' | ||
colorScheme='primary' | ||
/> | ||
</Box> | ||
</Box> | ||
)} | ||
</Box> | ||
</Flex> | ||
); | ||
}; | ||
|
||
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 ( | ||
<> | ||
<Button | ||
elementDescriptor={descriptors.organizationSwitcherInvitationAcceptButton} | ||
textVariant='buttonExtraSmallBold' | ||
variant='solid' | ||
isLoading={card.isLoading} | ||
onClick={handleAccept} | ||
localizationKey={localizationKeys('organizationSwitcher.invitationAccept')} | ||
/> | ||
</> | ||
); | ||
}; | ||
|
||
const InvitationPreview = withCardStateProvider((props: UserOrganizationInvitationResource) => { | ||
return ( | ||
<Flex | ||
align='center' | ||
gap={2} | ||
sx={t => ({ | ||
minHeight: 'unset', | ||
height: t.space.$12, | ||
justifyContent: 'space-between', | ||
padding: `0 ${t.space.$6}`, | ||
})} | ||
> | ||
<OrganizationPreview | ||
elementId='organizationSwitcher' | ||
avatarSx={t => ({ margin: `0 calc(${t.space.$3}/2)` })} | ||
organization={props.publicOrganizationData} | ||
size='sm' | ||
/> | ||
|
||
<AcceptRejectInvitationButtons {...props} /> | ||
</Flex> | ||
); | ||
}); |
73 changes: 73 additions & 0 deletions
73
packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import type { OrganizationResource } from '@clerk/types'; | ||
import React from 'react'; | ||
|
||
import { | ||
useCoreOrganization, | ||
useCoreOrganizationList, | ||
useCoreUser, | ||
useOrganizationSwitcherContext, | ||
} from '../../contexts'; | ||
import { Box, descriptors, localizationKeys } from '../../customizables'; | ||
import { OrganizationPreview, PersonalWorkspacePreview, PreviewButton } from '../../elements'; | ||
import { SwitchArrows } from '../../icons'; | ||
import { common } from '../../styledSystem'; | ||
|
||
export type UserMembershipListProps = { | ||
onPersonalWorkspaceClick: React.MouseEventHandler; | ||
onOrganizationClick: (org: OrganizationResource) => unknown; | ||
}; | ||
export const UserMembershipList = (props: UserMembershipListProps) => { | ||
const { onPersonalWorkspaceClick, onOrganizationClick } = props; | ||
|
||
const { hidePersonal } = useOrganizationSwitcherContext(); | ||
const { organization: currentOrg } = useCoreOrganization(); | ||
const { organizationList } = useCoreOrganizationList(); | ||
const user = useCoreUser(); | ||
|
||
const otherOrgs = (organizationList || []).map(e => e.organization).filter(o => o.id !== currentOrg?.id); | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
const { username, primaryEmailAddress, primaryPhoneNumber, ...userWithoutIdentifiers } = user; | ||
|
||
return ( | ||
<Box | ||
sx={t => ({ | ||
maxHeight: `calc(4 * ${t.sizes.$12})`, | ||
overflowY: 'auto', | ||
...common.unstyledScrollbar(t), | ||
})} | ||
> | ||
{currentOrg && !hidePersonal && ( | ||
<PreviewButton | ||
elementDescriptor={descriptors.organizationSwitcherPreviewButton} | ||
icon={SwitchArrows} | ||
sx={{ borderRadius: 0 }} | ||
onClick={onPersonalWorkspaceClick} | ||
> | ||
<PersonalWorkspacePreview | ||
user={userWithoutIdentifiers} | ||
size='sm' | ||
avatarSx={t => ({ margin: `0 calc(${t.space.$3}/2)` })} | ||
title={localizationKeys('organizationSwitcher.personalWorkspace')} | ||
/> | ||
</PreviewButton> | ||
)} | ||
{otherOrgs.map(organization => ( | ||
<PreviewButton | ||
key={organization.id} | ||
elementDescriptor={descriptors.organizationSwitcherPreviewButton} | ||
icon={SwitchArrows} | ||
sx={{ borderRadius: 0 }} | ||
onClick={() => onOrganizationClick(organization)} | ||
> | ||
<OrganizationPreview | ||
elementId='organizationSwitcher' | ||
avatarSx={t => ({ margin: `0 calc(${t.space.$3}/2)` })} | ||
organization={organization} | ||
size='sm' | ||
/> | ||
</PreviewButton> | ||
))} | ||
</Box> | ||
); | ||
}; |
Oops, something went wrong.