Skip to content

Commit

Permalink
feat(clerk-js): Add list of suggestions in OrganizationSwitcher (#1577)
Browse files Browse the repository at this point in the history
* feat(clerk-js): Add list of suggestions in OrganizationSwitcher

* feat(clerk-js): Create a "small" size button

* feat(clerk-js): Allow `automatic_suggestion` to be set for a verified domain

* chore(clerk-js): Add changeset

* fix(types): PublicOrganizationDataJSON should not extend ClerkResourceJSON
  • Loading branch information
panteliselef authored Aug 11, 2023
1 parent a412a50 commit 1e117be
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 13 deletions.
9 changes: 9 additions & 0 deletions .changeset/thin-moles-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/types': patch
---

Introduces list of suggestions within <OrganizationSwitcher/>
+ Users can request to join a suggested organization
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
const enrollmentMode = useFormControl('enrollmentMode', '', {
type: 'radio',
radioOptions: [
{
value: 'automatic_suggestion',
label: 'Automatic suggestion',
},
{
value: 'automatic_invitation',
// TODO: Add labels
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ const _OrganizationSwitcher = withFloatingTree(() => {
offset: 8,
});

/**
* Prefetch user invitations and suggestions
*/
useCoreOrganizationList({
userInvitations: {
infinite: true,
},
userSuggestions: {
infinite: true,
},
});

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Action, SecondaryActions } from '../../elements';
import { UserInvitationList } from './UserInvitationList';
import type { UserMembershipListProps } from './UserMembershipList';
import { UserMembershipList } from './UserMembershipList';
import { UserSuggestionList } from './UserSuggestionList';

export interface OrganizationActionListProps extends UserMembershipListProps {
onCreateOrganizationClick: React.MouseEventHandler;
Expand Down Expand Up @@ -45,6 +46,7 @@ export const OrganizationActionList = (props: OrganizationActionListProps) => {
<SecondaryActions elementDescriptor={descriptors.organizationSwitcherPopoverActions}>
<UserMembershipList {...{ onPersonalWorkspaceClick, onOrganizationClick }} />
<UserInvitationList />
<UserSuggestionList />
<CreateOrganizationButton {...{ onCreateOrganizationClick }} />
</SecondaryActions>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,15 @@ const AcceptRejectInvitationButtons = (props: UserOrganizationInvitationResource
};

return (
<>
<Button
elementDescriptor={descriptors.organizationSwitcherInvitationAcceptButton}
textVariant='buttonExtraSmallBold'
variant='solid'
isLoading={card.isLoading}
onClick={handleAccept}
localizationKey={localizationKeys('organizationSwitcher.invitationAccept')}
/>
</>
<Button
elementDescriptor={descriptors.organizationSwitcherInvitationAcceptButton}
textVariant='buttonExtraSmallBold'
variant='solid'
size='sm'
isLoading={card.isLoading}
onClick={handleAccept}
localizationKey={localizationKeys('organizationSwitcher.invitationAccept')}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import type { OrganizationSuggestionResource } 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 UserSuggestionList = () => {
const { userSuggestions } = useCoreOrganizationList({
userSuggestions: {
infinite: true,
},
});

const { ref } = useInView({
threshold: 0,
onChange: inView => {
if (inView) {
userSuggestions.fetchNext?.();
}
},
});

if ((userSuggestions.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(
(userSuggestions.count ?? 0) > 1
? 'organizationSwitcher.suggestionCountLabel_many'
: 'organizationSwitcher.suggestionCountLabel_single',
{
count: userSuggestions.count,
},
)}
/>
<Box
sx={t => ({
maxHeight: `calc(4 * ${t.sizes.$12})`,
overflowY: 'auto',
...common.unstyledScrollbar(t),
})}
>
{userSuggestions?.data?.map(inv => {
return (
<SuggestionPreview
key={inv.id}
{...inv}
/>
);
})}

{(userSuggestions.hasNextPage || userSuggestions.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 AcceptRejectSuggestionButtons = (props: OrganizationSuggestionResource) => {
const card = useCardState();
const { userSuggestions } = useCoreOrganizationList({
userSuggestions: {
infinite: true,
},
});

const mutateSwrState = () => {
(userSuggestions 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'
size='sm'
isLoading={card.isLoading}
onClick={handleAccept}
localizationKey={localizationKeys('organizationSwitcher.suggestionsAccept')}
/>
);
};

const SuggestionPreview = withCardStateProvider((props: OrganizationSuggestionResource) => {
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'
/>

<AcceptRejectSuggestionButtons {...props} />
</Flex>
);
});
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/elements/OrganizationPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const OrganizationPreview = (props: OrganizationPreviewProps) => {
elementId={descriptors.organizationPreview.setId(elementId)}
gap={4}
align='center'
sx={[{ minWidth: '0px', width: '100%' }, sx]}
sx={[{ minWidth: '0' }, sx]}
{...rest}
>
<Flex
Expand Down
5 changes: 4 additions & 1 deletion packages/clerk-js/src/ui/primitives/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const { applyVariants, filterProps } = createVariants((theme, props: OwnProps) =
size: {
iconLg: { minHeight: theme.sizes.$14, width: theme.sizes.$14 },
xs: { minHeight: theme.sizes.$1x5, padding: `${theme.space.$1x5} ${theme.space.$1x5}` },
sm: {
minHeight: theme.sizes.$8,
padding: `${theme.space.$2} ${theme.space.$3x5}`,
},
md: {
minHeight: theme.sizes.$9,
padding: `${theme.space.$2x5} ${theme.space.$5}`,
Expand Down Expand Up @@ -131,7 +135,6 @@ const { applyVariants, filterProps } = createVariants((theme, props: OwnProps) =
},
};
});

type OwnProps = PrimitiveProps<'button'> & {
isLoading?: boolean;
loadingText?: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ export const enUS: LocalizationResource = {
invitationCountLabel_single: '1 pending invitation to join:',
invitationCountLabel_many: '{{count}} pending invitations to join:',
invitationAccept: 'Join',
suggestionCountLabel_single: '1 suggested organization:',
suggestionCountLabel_many: '{{count}} suggested organizations:',
suggestionsAccept: 'Request to join',
},
impersonationFab: {
title: 'Signed in as {{identifier}}',
Expand Down
11 changes: 10 additions & 1 deletion packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON {
updated_at: number;
}

export interface PublicOrganizationDataJSON extends ClerkResourceJSON {
export interface PublicOrganizationDataJSON {
id: string;
name: string;
slug: string | null;
Expand All @@ -381,6 +381,15 @@ export interface OrganizationSuggestionJSON extends ClerkResourceJSON {
updated_at: number;
}

export interface OrganizationSuggestionJSON extends ClerkResourceJSON {
object: 'organization_suggestion';
id: string;
public_organization_data: PublicOrganizationDataJSON;
status: OrganizationSuggestionStatus;
created_at: number;
updated_at: number;
}

export interface OrganizationMembershipRequestJSON extends ClerkResourceJSON {
object: 'organization_membership_request';
id: string;
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ type _LocalizationResource = {
invitationCountLabel_single: LocalizationValue;
invitationCountLabel_many: LocalizationValue;
invitationAccept: LocalizationValue;
suggestionCountLabel_single: LocalizationValue;
suggestionCountLabel_many: LocalizationValue;
suggestionsAccept: LocalizationValue;
};
impersonationFab: {
title: LocalizationValue;
Expand Down

0 comments on commit 1e117be

Please sign in to comment.