Skip to content

Commit d2ffb25

Browse files
authored
feat(clerk-js): Display a callout when updating or removing a verified domain (clerk#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
1 parent 899441f commit d2ffb25

9 files changed

Lines changed: 156 additions & 36 deletions

File tree

.changeset/nasty-doors-watch.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/clerk-js/src/core/resources/OrganizationDomain.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export class OrganizationDomain extends BaseResource implements OrganizationDoma
2020
affiliationEmailAddress!: string | null;
2121
createdAt!: Date;
2222
updatedAt!: Date;
23+
totalPendingInvitations!: number;
24+
totalPendingSuggestions!: number;
2325

2426
constructor(data: OrganizationDomainJSON) {
2527
super();
@@ -77,6 +79,8 @@ export class OrganizationDomain extends BaseResource implements OrganizationDoma
7779
this.organizationId = data.organization_id;
7880
this.enrollmentMode = data.enrollment_mode;
7981
this.affiliationEmailAddress = data.affiliation_email_address;
82+
this.totalPendingSuggestions = data.total_pending_suggestions;
83+
this.totalPendingInvitations = data.total_pending_invitations;
8084
if (data.verification) {
8185
this.verification = {
8286
status: data.verification.status,

packages/clerk-js/src/core/resources/__snapshots__/OrganizationDomain.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ OrganizationDomain {
1111
"organizationId": "test_org_id",
1212
"pathRoot": "",
1313
"prepareAffiliationVerification": [Function],
14+
"totalPendingInvitations": undefined,
15+
"totalPendingSuggestions": undefined,
1416
"updateEnrollmentMode": [Function],
1517
"verification": null,
1618
}
@@ -27,6 +29,8 @@ OrganizationDomain {
2729
"organizationId": "test_org_id",
2830
"pathRoot": "",
2931
"prepareAffiliationVerification": [Function],
32+
"totalPendingInvitations": undefined,
33+
"totalPendingSuggestions": undefined,
3034
"updateEnrollmentMode": [Function],
3135
"verification": {
3236
"attempts": 1,
Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,67 @@
1-
import type { MouseEvent } from 'react';
1+
import type { ComponentType, MouseEvent, PropsWithChildren } from 'react';
22

3-
import { Col, Flex, Link, Text } from '../customizables';
3+
import { Col, Flex, Link, Text, Icon } from '../customizables';
44
import type { LocalizationKey } from '../localization';
5+
import type { ThemableCssProp } from '../styledSystem';
56

67
type CalloutWithActionProps = {
7-
text: LocalizationKey;
8+
text?: LocalizationKey | string;
9+
textSx?: ThemableCssProp;
810
actionLabel?: LocalizationKey;
911
onClick?: (e: MouseEvent<HTMLAnchorElement>) => Promise<any>;
12+
icon: ComponentType;
1013
};
11-
export const CalloutWithAction = (props: CalloutWithActionProps) => {
12-
const { text, actionLabel, onClick: onClickProp } = props;
14+
export const CalloutWithAction = (props: PropsWithChildren<CalloutWithActionProps>) => {
15+
const { icon, text, textSx, actionLabel, onClick: onClickProp } = props;
1316

1417
const onClick = (e: MouseEvent<HTMLAnchorElement>) => {
1518
if (onClickProp) {
1619
void onClickProp?.(e);
1720
}
1821
};
1922

23+
console.log(props.children);
24+
2025
return (
2126
<Flex
2227
sx={theme => ({
2328
background: theme.colors.$blackAlpha50,
24-
padding: theme.space.$4,
29+
padding: `${theme.space.$2x5} ${theme.space.$4}`,
2530
justifyContent: 'space-between',
2631
alignItems: 'flex-start',
2732
borderRadius: theme.radii.$md,
2833
})}
2934
>
30-
<Col gap={4}>
31-
<Text
32-
sx={t => ({
33-
lineHeight: t.lineHeights.$tall,
34-
})}
35-
localizationKey={text}
35+
<Flex gap={2}>
36+
<Icon
37+
colorScheme='neutral'
38+
icon={icon}
39+
sx={t => ({ marginTop: t.space.$1 })}
3640
/>
41+
<Col gap={4}>
42+
<Text
43+
colorScheme='neutral'
44+
sx={[
45+
t => ({
46+
lineHeight: t.lineHeights.$base,
47+
}),
48+
textSx,
49+
]}
50+
localizationKey={text}
51+
>
52+
{props.children}
53+
</Text>
3754

38-
<Link
39-
colorScheme={'primary'}
40-
variant='regularMedium'
41-
localizationKey={actionLabel}
42-
onClick={onClick}
43-
/>
44-
</Col>
55+
{actionLabel && (
56+
<Link
57+
colorScheme={'primary'}
58+
variant='regularMedium'
59+
localizationKey={actionLabel}
60+
onClick={onClick}
61+
/>
62+
)}
63+
</Col>
64+
</Flex>
4565
</Flex>
4666
);
4767
};

packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import type { OrganizationEnrollmentMode } from '@clerk/types';
1+
import type { OrganizationDomainResource, OrganizationEnrollmentMode } from '@clerk/types';
22

3+
import { CalloutWithAction } from '../../common';
34
import { useCoreOrganization, useEnvironment } from '../../contexts';
4-
import { Col, Flex, localizationKeys, Spinner } from '../../customizables';
5+
import type { LocalizationKey } from '../../customizables';
6+
import { Col, Flex, localizationKeys, Spinner, Text } from '../../customizables';
57
import {
68
ContentPage,
79
Form,
@@ -16,11 +18,39 @@ import {
1618
withCardStateProvider,
1719
} from '../../elements';
1820
import { useFetch, useNavigateToFlowStart } from '../../hooks';
21+
import { InformationCircle } from '../../icons';
1922
import { useRouter } from '../../router';
2023
import { handleError, useFormControl } from '../../utils';
2124
import { LinkButtonWithDescription } from '../UserProfile/LinkButtonWithDescription';
2225
import { OrganizationProfileBreadcrumbs } from './OrganizationProfileNavbar';
2326

27+
const useCalloutLabel = (
28+
domain: OrganizationDomainResource | null,
29+
{
30+
infoLabel: infoLabelKey,
31+
}: {
32+
infoLabel: LocalizationKey;
33+
},
34+
) => {
35+
const totalInvitations = domain?.totalPendingInvitations || 0;
36+
const totalSuggestions = domain?.totalPendingSuggestions || 0;
37+
const totalPending = totalSuggestions + totalInvitations;
38+
39+
if (totalPending === 0) {
40+
return [] as string[];
41+
}
42+
43+
return [
44+
infoLabelKey,
45+
localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutInvitationCountLabel`, {
46+
count: totalInvitations,
47+
}),
48+
localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutSuggestionCountLabel`, {
49+
count: totalInvitations,
50+
}),
51+
];
52+
};
53+
2454
export const VerifiedDomainPage = withCardStateProvider(() => {
2555
const card = useCardState();
2656
const { organizationSettings } = useEnvironment();
@@ -30,6 +60,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
3060
infinite: true,
3161
},
3262
});
63+
3364
const { navigateToFlowStart } = useNavigateToFlowStart();
3465
const { params, navigate, queryParams } = useRouter();
3566
const mode = (queryParams.mode || 'edit') as 'select' | 'edit';
@@ -104,6 +135,14 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
104135
domain: domain?.name,
105136
});
106137

138+
const calloutLabel = useCalloutLabel(domain, {
139+
infoLabel: localizationKeys(`organizationProfile.verifiedDomainPage.enrollmentTab.calloutInfoLabel`),
140+
});
141+
142+
const dangerCalloutLabel = useCalloutLabel(domain, {
143+
infoLabel: localizationKeys(`organizationProfile.verifiedDomainPage.dangerTab.calloutInfoLabel`),
144+
});
145+
107146
const updateEnrollmentMode = async () => {
108147
if (!domain || !organization) {
109148
return;
@@ -149,6 +188,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
149188
if (!(domain.verification && domain.verification.status === 'verified')) {
150189
void navigateToFlowStart();
151190
}
191+
152192
return (
153193
<ContentPage
154194
headerTitle={domain.name}
@@ -175,6 +215,24 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
175215
direction={'col'}
176216
gap={4}
177217
>
218+
{calloutLabel.length > 0 && (
219+
<CalloutWithAction icon={InformationCircle}>
220+
{calloutLabel.map((label, index) => (
221+
<Text
222+
key={index}
223+
as={'span'}
224+
sx={[
225+
t => ({
226+
lineHeight: t.lineHeights.$short,
227+
color: 'inherit',
228+
display: 'block',
229+
}),
230+
]}
231+
localizationKey={label}
232+
/>
233+
))}
234+
</CalloutWithAction>
235+
)}
178236
<Header.Root>
179237
<Header.Subtitle
180238
localizationKey={localizationKeys('organizationProfile.verifiedDomainPage.enrollmentTab.subtitle')}
@@ -207,22 +265,42 @@ export const VerifiedDomainPage = withCardStateProvider(() => {
207265
{allowsEdit && (
208266
<TabPanel
209267
direction={'col'}
210-
sx={[
211-
{ width: '100%' },
212-
t => ({
213-
padding: `${t.space.$none} ${t.space.$4}`,
214-
}),
215-
]}
268+
gap={4}
269+
sx={{ width: '100%' }}
216270
>
217-
<LinkButtonWithDescription
218-
title={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainTitle')}
219-
subtitle={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainSubtitle')}
220-
actionLabel={localizationKeys(
221-
'organizationProfile.verifiedDomainPage.dangerTab.removeDomainActionLabel__remove',
222-
)}
223-
colorScheme='danger'
224-
onClick={() => navigate(`../../domain/${domain.id}/remove`)}
225-
/>
271+
{dangerCalloutLabel.length > 0 && (
272+
<CalloutWithAction icon={InformationCircle}>
273+
{dangerCalloutLabel.map((label, index) => (
274+
<Text
275+
key={index}
276+
as={'span'}
277+
sx={[
278+
t => ({
279+
lineHeight: t.lineHeights.$short,
280+
color: 'inherit',
281+
display: 'block',
282+
}),
283+
]}
284+
localizationKey={label}
285+
/>
286+
))}
287+
</CalloutWithAction>
288+
)}
289+
<Col
290+
sx={t => ({
291+
padding: `${t.space.$none} ${t.space.$4}`,
292+
})}
293+
>
294+
<LinkButtonWithDescription
295+
title={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainTitle')}
296+
subtitle={localizationKeys('organizationProfile.verifiedDomainPage.dangerTab.removeDomainSubtitle')}
297+
actionLabel={localizationKeys(
298+
'organizationProfile.verifiedDomainPage.dangerTab.removeDomainActionLabel__remove',
299+
)}
300+
colorScheme='danger'
301+
onClick={() => navigate(`../../domain/${domain.id}/remove`)}
302+
/>
303+
</Col>
226304
</TabPanel>
227305
)}
228306
</TabPanels>

packages/localizations/src/en-US.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,11 +616,15 @@ export const enUS: LocalizationResource = {
616616
automaticSuggestionOption__description:
617617
'Users receive a suggestion to request to join, but must be approved by an admin before they are able to join the organization.',
618618
formButton__save: 'Save',
619+
calloutInfoLabel: 'Changing the enrollment mode will only affect new users.',
620+
calloutInvitationCountLabel: 'Pending invitations sent to users: {{count}}',
621+
calloutSuggestionCountLabel: 'Pending suggestions sent to users: {{count}}',
619622
},
620623
dangerTab: {
621624
removeDomainTitle: 'Remove domain',
622625
removeDomainSubtitle: 'Remove this domain from your verified domains',
623626
removeDomainActionLabel__remove: 'Remove domain',
627+
calloutInfoLabel: 'Removing this domain will affect invited users.',
624628
},
625629
},
626630
invitePage: {

packages/types/src/json.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,8 @@ export interface OrganizationDomainJSON extends ClerkResourceJSON {
362362
affiliation_email_address: string | null;
363363
created_at: number;
364364
updated_at: number;
365+
total_pending_invitations: number;
366+
total_pending_suggestions: number;
365367
}
366368

367369
export interface PublicOrganizationDataJSON {

packages/types/src/localization.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,15 @@ type _LocalizationResource = {
631631
automaticSuggestionOption__label: LocalizationValue;
632632
automaticSuggestionOption__description: LocalizationValue;
633633
formButton__save: LocalizationValue;
634+
calloutInfoLabel: LocalizationValue;
635+
calloutInvitationCountLabel: LocalizationValue;
636+
calloutSuggestionCountLabel: LocalizationValue;
634637
};
635638
dangerTab: {
636639
removeDomainTitle: LocalizationValue;
637640
removeDomainSubtitle: LocalizationValue;
638641
removeDomainActionLabel__remove: LocalizationValue;
642+
calloutInfoLabel: LocalizationValue;
639643
};
640644
};
641645
removeDomainPage: {

packages/types/src/organizationDomain.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface OrganizationDomainResource extends ClerkResource {
2020
createdAt: Date;
2121
updatedAt: Date;
2222
affiliationEmailAddress: string | null;
23+
totalPendingInvitations: number;
24+
totalPendingSuggestions: number;
2325
prepareAffiliationVerification: (params: PrepareAffiliationVerificationParams) => Promise<OrganizationDomainResource>;
2426

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

0 commit comments

Comments
 (0)