Skip to content

Commit

Permalink
feat(clerk-js): Display a callout when updating or removing a verifie…
Browse files Browse the repository at this point in the history
…d domain (#1641)

* feat(clerk-js): Display a callout when updating or removing a verified domain

* chore(clerk-js): Add changeset

* test(clerk-js): Update snapshot of OrganizationDomain

* chore(clerk-js): CallWithAction default icon styling

* fix(clerk-js): Remove complex logic for localizations
  • Loading branch information
panteliselef authored Sep 1, 2023
1 parent 899441f commit d2ffb25
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .changeset/nasty-doors-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 4 additions & 0 deletions packages/clerk-js/src/core/resources/OrganizationDomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export class OrganizationDomain extends BaseResource implements OrganizationDoma
affiliationEmailAddress!: string | null;
createdAt!: Date;
updatedAt!: Date;
totalPendingInvitations!: number;
totalPendingSuggestions!: number;

constructor(data: OrganizationDomainJSON) {
super();
Expand Down Expand Up @@ -77,6 +79,8 @@ export class OrganizationDomain extends BaseResource implements OrganizationDoma
this.organizationId = data.organization_id;
this.enrollmentMode = data.enrollment_mode;
this.affiliationEmailAddress = data.affiliation_email_address;
this.totalPendingSuggestions = data.total_pending_suggestions;
this.totalPendingInvitations = data.total_pending_invitations;
if (data.verification) {
this.verification = {
status: data.verification.status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ OrganizationDomain {
"organizationId": "test_org_id",
"pathRoot": "",
"prepareAffiliationVerification": [Function],
"totalPendingInvitations": undefined,
"totalPendingSuggestions": undefined,
"updateEnrollmentMode": [Function],
"verification": null,
}
Expand All @@ -27,6 +29,8 @@ OrganizationDomain {
"organizationId": "test_org_id",
"pathRoot": "",
"prepareAffiliationVerification": [Function],
"totalPendingInvitations": undefined,
"totalPendingSuggestions": undefined,
"updateEnrollmentMode": [Function],
"verification": {
"attempts": 1,
Expand Down
58 changes: 39 additions & 19 deletions packages/clerk-js/src/ui/common/CalloutWithAction.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,67 @@
import type { MouseEvent } from 'react';
import type { ComponentType, MouseEvent, PropsWithChildren } from 'react';

import { Col, Flex, Link, Text } from '../customizables';
import { Col, Flex, Link, Text, Icon } from '../customizables';
import type { LocalizationKey } from '../localization';
import type { ThemableCssProp } from '../styledSystem';

type CalloutWithActionProps = {
text: LocalizationKey;
text?: LocalizationKey | string;
textSx?: ThemableCssProp;
actionLabel?: LocalizationKey;
onClick?: (e: MouseEvent<HTMLAnchorElement>) => Promise<any>;
icon: ComponentType;
};
export const CalloutWithAction = (props: CalloutWithActionProps) => {
const { text, actionLabel, onClick: onClickProp } = props;
export const CalloutWithAction = (props: PropsWithChildren<CalloutWithActionProps>) => {
const { icon, text, textSx, actionLabel, onClick: onClickProp } = props;

const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
if (onClickProp) {
void onClickProp?.(e);
}
};

console.log(props.children);

return (
<Flex
sx={theme => ({
background: theme.colors.$blackAlpha50,
padding: theme.space.$4,
padding: `${theme.space.$2x5} ${theme.space.$4}`,
justifyContent: 'space-between',
alignItems: 'flex-start',
borderRadius: theme.radii.$md,
})}
>
<Col gap={4}>
<Text
sx={t => ({
lineHeight: t.lineHeights.$tall,
})}
localizationKey={text}
<Flex gap={2}>
<Icon
colorScheme='neutral'
icon={icon}
sx={t => ({ marginTop: t.space.$1 })}
/>
<Col gap={4}>
<Text
colorScheme='neutral'
sx={[
t => ({
lineHeight: t.lineHeights.$base,
}),
textSx,
]}
localizationKey={text}
>
{props.children}
</Text>

<Link
colorScheme={'primary'}
variant='regularMedium'
localizationKey={actionLabel}
onClick={onClick}
/>
</Col>
{actionLabel && (
<Link
colorScheme={'primary'}
variant='regularMedium'
localizationKey={actionLabel}
onClick={onClick}
/>
)}
</Col>
</Flex>
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { OrganizationEnrollmentMode } from '@clerk/types';
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from '@clerk/types';

import { CalloutWithAction } from '../../common';
import { useCoreOrganization, useEnvironment } from '../../contexts';
import { Col, Flex, localizationKeys, Spinner } from '../../customizables';
import type { LocalizationKey } from '../../customizables';
import { Col, Flex, localizationKeys, Spinner, Text } from '../../customizables';
import {
ContentPage,
Form,
Expand All @@ -16,11 +18,39 @@ import {
withCardStateProvider,
} from '../../elements';
import { useFetch, useNavigateToFlowStart } from '../../hooks';
import { InformationCircle } from '../../icons';
import { useRouter } from '../../router';
import { handleError, useFormControl } from '../../utils';
import { LinkButtonWithDescription } from '../UserProfile/LinkButtonWithDescription';
import { OrganizationProfileBreadcrumbs } from './OrganizationProfileNavbar';

const useCalloutLabel = (
domain: OrganizationDomainResource | null,
{
infoLabel: infoLabelKey,
}: {
infoLabel: LocalizationKey;
},
) => {
const totalInvitations = domain?.totalPendingInvitations || 0;
const totalSuggestions = domain?.totalPendingSuggestions || 0;
const totalPending = totalSuggestions + totalInvitations;

if (totalPending === 0) {
return [] as string[];
}

return [
infoLabelKey,
localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutInvitationCountLabel`, {
count: totalInvitations,
}),
localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutSuggestionCountLabel`, {
count: totalInvitations,
}),
];
};

export const VerifiedDomainPage = withCardStateProvider(() => {
const card = useCardState();
const { organizationSettings } = useEnvironment();
Expand All @@ -30,6 +60,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
infinite: true,
},
});

const { navigateToFlowStart } = useNavigateToFlowStart();
const { params, navigate, queryParams } = useRouter();
const mode = (queryParams.mode || 'edit') as 'select' | 'edit';
Expand Down Expand Up @@ -104,6 +135,14 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
domain: domain?.name,
});

const calloutLabel = useCalloutLabel(domain, {
infoLabel: localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutInfoLabel`),
});

const dangerCalloutLabel = useCalloutLabel(domain, {
infoLabel: localizationKeys(`organizationProfile.verifiedDomainPage.dangerTab.calloutInfoLabel`),
});

const updateEnrollmentMode = async () => {
if (!domain || !organization) {
return;
Expand Down Expand Up @@ -149,6 +188,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
if (!(domain.verification && domain.verification.status === 'verified')) {
void navigateToFlowStart();
}

return (
<ContentPage
headerTitle={domain.name}
Expand All @@ -175,6 +215,24 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
direction={'col'}
gap={4}
>
{calloutLabel.length > 0 && (
<CalloutWithAction icon={InformationCircle}>
{calloutLabel.map((label, index) => (
<Text
key={index}
as={'span'}
sx={[
t => ({
lineHeight: t.lineHeights.$short,
color: 'inherit',
display: 'block',
}),
]}
localizationKey={label}
/>
))}
</CalloutWithAction>
)}
<Header.Root>
<Header.Subtitle
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.subtitle')}
Expand Down Expand Up @@ -207,22 +265,42 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
{allowsEdit && (
<TabPanel
direction={'col'}
sx={[
{ width: '100%' },
t => ({
padding: `${t.space.$none} ${t.space.$4}`,
}),
]}
gap={4}
sx={{ width: '100%' }}
>
<LinkButtonWithDescription
title={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainTitle')}
subtitle={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainSubtitle')}
actionLabel={localizationKeys(
'organizationProfile.verifiedDomainPage.dangerTab.removeDomainActionLabel__remove',
)}
colorScheme='danger'
onClick={() => navigate(`../../domain/${domain.id}/remove`)}
/>
{dangerCalloutLabel.length > 0 && (
<CalloutWithAction icon={InformationCircle}>
{dangerCalloutLabel.map((label, index) => (
<Text
key={index}
as={'span'}
sx={[
t => ({
lineHeight: t.lineHeights.$short,
color: 'inherit',
display: 'block',
}),
]}
localizationKey={label}
/>
))}
</CalloutWithAction>
)}
<Col
sx={t => ({
padding: `${t.space.$none} ${t.space.$4}`,
})}
>
<LinkButtonWithDescription
title={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainTitle')}
subtitle={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainSubtitle')}
actionLabel={localizationKeys(
'organizationProfile.verifiedDomainPage.dangerTab.removeDomainActionLabel__remove',
)}
colorScheme='danger'
onClick={() => navigate(`../../domain/${domain.id}/remove`)}
/>
</Col>
</TabPanel>
)}
</TabPanels>
Expand Down
4 changes: 4 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,15 @@ export const enUS: LocalizationResource = {
automaticSuggestionOption__description:
'Users receive a suggestion to request to join, but must be approved by an admin before they are able to join the organization.',
formButton__save: 'Save',
calloutInfoLabel: 'Changing the enrollment mode will only affect new users.',
calloutInvitationCountLabel: 'Pending invitations sent to users: {{count}}',
calloutSuggestionCountLabel: 'Pending suggestions sent to users: {{count}}',
},
dangerTab: {
removeDomainTitle: 'Remove domain',
removeDomainSubtitle: 'Remove this domain from your verified domains',
removeDomainActionLabel__remove: 'Remove domain',
calloutInfoLabel: 'Removing this domain will affect invited users.',
},
},
invitePage: {
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON {
affiliation_email_address: string | null;
created_at: number;
updated_at: number;
total_pending_invitations: number;
total_pending_suggestions: number;
}

export interface PublicOrganizationDataJSON {
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -631,11 +631,15 @@ type _LocalizationResource = {
automaticSuggestionOption__label: LocalizationValue;
automaticSuggestionOption__description: LocalizationValue;
formButton__save: LocalizationValue;
calloutInfoLabel: LocalizationValue;
calloutInvitationCountLabel: LocalizationValue;
calloutSuggestionCountLabel: LocalizationValue;
};
dangerTab: {
removeDomainTitle: LocalizationValue;
removeDomainSubtitle: LocalizationValue;
removeDomainActionLabel__remove: LocalizationValue;
calloutInfoLabel: LocalizationValue;
};
};
removeDomainPage: {
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/organizationDomain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface OrganizationDomainResource extends ClerkResource {
createdAt: Date;
updatedAt: Date;
affiliationEmailAddress: string | null;
totalPendingInvitations: number;
totalPendingSuggestions: number;
prepareAffiliationVerification: (params: PrepareAffiliationVerificationParams) => Promise<OrganizationDomainResource>;

attemptAffiliationVerification: (params: AttemptAffiliationVerificationParams) => Promise<OrganizationDomainResource>;
Expand Down

0 comments on commit d2ffb25

Please sign in to comment.