Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clerk-js): Add list of suggestions in OrganizationSwitcher #1577

Merged
merged 5 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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