From d2ffb25fb96e60520ca032f3acfc3ca79d20eba8 Mon Sep 17 00:00:00 2001 From: panteliselef Date: Fri, 1 Sep 2023 14:05:21 +0300 Subject: [PATCH] feat(clerk-js): Display a callout when updating or removing a verified 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 --- .changeset/nasty-doors-watch.md | 2 + .../src/core/resources/OrganizationDomain.ts | 4 + .../OrganizationDomain.test.ts.snap | 4 + .../src/ui/common/CalloutWithAction.tsx | 58 ++++++--- .../VerifiedDomainPage.tsx | 112 +++++++++++++++--- packages/localizations/src/en-US.ts | 4 + packages/types/src/json.ts | 2 + packages/types/src/localization.ts | 4 + packages/types/src/organizationDomain.ts | 2 + 9 files changed, 156 insertions(+), 36 deletions(-) create mode 100644 .changeset/nasty-doors-watch.md diff --git a/.changeset/nasty-doors-watch.md b/.changeset/nasty-doors-watch.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/nasty-doors-watch.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/core/resources/OrganizationDomain.ts b/packages/clerk-js/src/core/resources/OrganizationDomain.ts index faf537f315..420349002f 100644 --- a/packages/clerk-js/src/core/resources/OrganizationDomain.ts +++ b/packages/clerk-js/src/core/resources/OrganizationDomain.ts @@ -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(); @@ -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, diff --git a/packages/clerk-js/src/core/resources/__snapshots__/OrganizationDomain.test.ts.snap b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationDomain.test.ts.snap index bfb447ee10..e868b7543e 100644 --- a/packages/clerk-js/src/core/resources/__snapshots__/OrganizationDomain.test.ts.snap +++ b/packages/clerk-js/src/core/resources/__snapshots__/OrganizationDomain.test.ts.snap @@ -11,6 +11,8 @@ OrganizationDomain { "organizationId": "test_org_id", "pathRoot": "", "prepareAffiliationVerification": [Function], + "totalPendingInvitations": undefined, + "totalPendingSuggestions": undefined, "updateEnrollmentMode": [Function], "verification": null, } @@ -27,6 +29,8 @@ OrganizationDomain { "organizationId": "test_org_id", "pathRoot": "", "prepareAffiliationVerification": [Function], + "totalPendingInvitations": undefined, + "totalPendingSuggestions": undefined, "updateEnrollmentMode": [Function], "verification": { "attempts": 1, diff --git a/packages/clerk-js/src/ui/common/CalloutWithAction.tsx b/packages/clerk-js/src/ui/common/CalloutWithAction.tsx index ed58a8977f..8a99e59e2c 100644 --- a/packages/clerk-js/src/ui/common/CalloutWithAction.tsx +++ b/packages/clerk-js/src/ui/common/CalloutWithAction.tsx @@ -1,15 +1,18 @@ -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) => Promise; + icon: ComponentType; }; -export const CalloutWithAction = (props: CalloutWithActionProps) => { - const { text, actionLabel, onClick: onClickProp } = props; +export const CalloutWithAction = (props: PropsWithChildren) => { + const { icon, text, textSx, actionLabel, onClick: onClickProp } = props; const onClick = (e: MouseEvent) => { if (onClickProp) { @@ -17,31 +20,48 @@ export const CalloutWithAction = (props: CalloutWithActionProps) => { } }; + console.log(props.children); + return ( ({ 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, })} > - - ({ - lineHeight: t.lineHeights.$tall, - })} - localizationKey={text} + + ({ marginTop: t.space.$1 })} /> + + ({ + lineHeight: t.lineHeights.$base, + }), + textSx, + ]} + localizationKey={text} + > + {props.children} + - - + {actionLabel && ( + + )} + + ); }; diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx index 70accbe037..51143f4204 100644 --- a/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationProfile/VerifiedDomainPage.tsx @@ -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, @@ -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(); @@ -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'; @@ -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; @@ -149,6 +188,7 @@ export const VerifiedDomainPage = withCardStateProvider(() => { if (!(domain.verification && domain.verification.status === 'verified')) { void navigateToFlowStart(); } + return ( { direction={'col'} gap={4} > + {calloutLabel.length > 0 && ( + + {calloutLabel.map((label, index) => ( + ({ + lineHeight: t.lineHeights.$short, + color: 'inherit', + display: 'block', + }), + ]} + localizationKey={label} + /> + ))} + + )} { {allowsEdit && ( ({ - padding: `${t.space.$none} ${t.space.$4}`, - }), - ]} + gap={4} + sx={{ width: '100%' }} > - navigate(`../../domain/${domain.id}/remove`)} - /> + {dangerCalloutLabel.length > 0 && ( + + {dangerCalloutLabel.map((label, index) => ( + ({ + lineHeight: t.lineHeights.$short, + color: 'inherit', + display: 'block', + }), + ]} + localizationKey={label} + /> + ))} + + )} + ({ + padding: `${t.space.$none} ${t.space.$4}`, + })} + > + navigate(`../../domain/${domain.id}/remove`)} + /> + )} diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 9ade7b465c..cb912c7c71 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -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: { diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index e08541fe07..f24384dd7f 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -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 { diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 5dc4acdb58..88366e7256 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -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: { diff --git a/packages/types/src/organizationDomain.ts b/packages/types/src/organizationDomain.ts index 924cf3b516..01c967e5eb 100644 --- a/packages/types/src/organizationDomain.ts +++ b/packages/types/src/organizationDomain.ts @@ -20,6 +20,8 @@ export interface OrganizationDomainResource extends ClerkResource { createdAt: Date; updatedAt: Date; affiliationEmailAddress: string | null; + totalPendingInvitations: number; + totalPendingSuggestions: number; prepareAffiliationVerification: (params: PrepareAffiliationVerificationParams) => Promise; attemptAffiliationVerification: (params: AttemptAffiliationVerificationParams) => Promise;