Skip to content

Commit 9c65dee

Browse files
committed
refactor(ui): extract reusable Section components from profile pages
1 parent 06330fc commit 9c65dee

14 files changed

Lines changed: 594 additions & 140 deletions

packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const AuthenticatedContent = withCoreUserGuard(() => {
4343
);
4444
});
4545

46-
const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
46+
export const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
4747
const {
4848
isLoading,
4949
enterpriseConnection,

packages/ui/src/components/OrganizationProfile/OrganizationBillingPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => {
2929
<ProfileCard.Page>
3030
<Col
3131
elementDescriptor={descriptors.page}
32-
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
32+
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
3333
>
3434
<Col
3535
elementDescriptor={descriptors.profilePage}

packages/ui/src/components/OrganizationProfile/OrganizationGeneralPage.tsx

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useOrganization } from '@clerk/shared/react';
22

3-
import { Header } from '@/ui/elements/Header';
43
import { OrganizationPreview } from '@/ui/elements/OrganizationPreview';
54
import { ProfileCard } from '@/ui/elements/ProfileCard';
65
import { ProfileSection } from '@/ui/elements/Section';
76

87
import { Protect, useProtect } from '../../common';
98
import { useEnvironment } from '../../contexts';
10-
import { Col, descriptors, localizationKeys, Text } from '../../customizables';
9+
import { Col, localizationKeys, Text } from '../../customizables';
1110
import { Action } from '../../elements/Action';
1211
import { useActionContext } from '../../elements/Action/ActionRoot';
1312
import { DeleteOrganizationForm, LeaveOrganizationForm } from './ActionConfirmationPage';
@@ -58,34 +57,28 @@ const DeleteOrganizationScreen = () => {
5857
export const OrganizationGeneralPage = () => {
5958
return (
6059
<ProfileCard.Page>
61-
<Col
62-
elementDescriptor={descriptors.page}
63-
sx={t => ({ gap: t.space.$8 })}
60+
<ProfileCard.PagePanel
61+
pageId='organizationGeneral'
62+
titleKey={localizationKeys('organizationProfile.start.headerTitle__general')}
6463
>
65-
<Col
66-
elementDescriptor={descriptors.profilePage}
67-
elementId={descriptors.profilePage.setId('organizationGeneral')}
68-
>
69-
<Header.Root>
70-
<Header.Title
71-
localizationKey={localizationKeys('organizationProfile.start.headerTitle__general')}
72-
sx={t => ({ marginBottom: t.space.$4 })}
73-
textVariant='h2'
74-
/>
75-
</Header.Root>
76-
<OrganizationProfileSection />
77-
<Protect permission='org:sys_domains:read'>
78-
<OrganizationDomainsSection />
79-
</Protect>
80-
<OrganizationLeaveSection />
81-
<OrganizationDeleteSection />
82-
</Col>
83-
</Col>
64+
<OrganizationProfileSection />
65+
<Protect permission='org:sys_domains:read'>
66+
<OrganizationDomainsSection />
67+
</Protect>
68+
<OrganizationLeaveSection />
69+
<OrganizationDeleteSection />
70+
</ProfileCard.PagePanel>
8471
</ProfileCard.Page>
8572
);
8673
};
8774

88-
const OrganizationProfileSection = () => {
75+
/**
76+
* Renders the organization profile section (name, logo) with inline edit when the user has
77+
* `org:sys_profile:manage`.
78+
*
79+
* @returns The profile section, or `null` when no organization is active.
80+
*/
81+
export const OrganizationProfileSection = (): JSX.Element | null => {
8982
const { organization } = useOrganization();
9083

9184
if (!organization) {
@@ -134,7 +127,13 @@ const OrganizationProfileSection = () => {
134127
);
135128
};
136129

137-
const OrganizationDomainsSection = () => {
130+
/**
131+
* Renders the verified-domains section.
132+
*
133+
* @returns The domains section, or `null` when domains are disabled, no organization is active, or
134+
* there are no domains and the user cannot add any.
135+
*/
136+
export const OrganizationDomainsSection = (): JSX.Element | null => {
138137
const { organizationSettings } = useEnvironment();
139138
const { organization, domains } = useOrganization({ domains: { infinite: true } });
140139
const canManageDomains = useProtect({ permission: 'org:sys_domains:manage' });
@@ -190,7 +189,12 @@ const OrganizationDomainsSection = () => {
190189
);
191190
};
192191

193-
const OrganizationLeaveSection = () => {
192+
/**
193+
* Renders the "leave organization" action in the danger section.
194+
*
195+
* @returns The leave-organization section, or `null` when no organization is active.
196+
*/
197+
export const OrganizationLeaveSection = (): JSX.Element | null => {
194198
const { organization } = useOrganization();
195199

196200
if (!organization) {
@@ -236,7 +240,13 @@ const OrganizationLeaveSection = () => {
236240
);
237241
};
238242

239-
const OrganizationDeleteSection = () => {
243+
/**
244+
* Renders the "delete organization" action in the danger section.
245+
*
246+
* @returns The delete-organization section, or `null` when no organization is active, the user
247+
* lacks `org:sys_profile:delete`, or admin delete is disabled.
248+
*/
249+
export const OrganizationDeleteSection = (): JSX.Element | null => {
240250
const { organization } = useOrganization();
241251
const canDeleteOrganization = useProtect({ permission: 'org:sys_profile:delete' });
242252

packages/ui/src/components/OrganizationProfile/OrganizationMembers.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const OrganizationMembers = withCardStateProvider(() => {
5555
<Col
5656
elementDescriptor={descriptors.page}
5757
gap={2}
58+
sx={{ isolation: 'isolate' }}
5859
>
5960
<Col
6061
elementDescriptor={descriptors.profilePage}

packages/ui/src/components/UserProfile/APIKeysPage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const APIKeysPage = () => {
2323
<Col
2424
gap={4}
2525
elementDescriptor={descriptors.page}
26+
sx={{ isolation: 'isolate' }}
2627
>
2728
<Header.Root>
2829
<Header.Title
Lines changed: 22 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,36 @@
1-
import { useUser } from '@clerk/shared/react';
2-
3-
import { Card } from '@/ui/elements/Card';
41
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
5-
import { Header } from '@/ui/elements/Header';
62
import { ProfileCard } from '@/ui/elements/ProfileCard';
73

8-
import { useEnvironment, useUserProfileContext } from '../../contexts';
9-
import { Col, descriptors, localizationKeys } from '../../customizables';
10-
import { ConnectedAccountsSection } from './ConnectedAccountsSection';
11-
import { EmailsSection } from './EmailsSection';
12-
import { EnterpriseAccountsSection } from './EnterpriseAccountsSection';
13-
import { PhoneSection } from './PhoneSection';
14-
import { UsernameSection } from './UsernameSection';
4+
import { localizationKeys } from '../../customizables';
5+
import {
6+
AccountConnectedAccounts,
7+
AccountEmails,
8+
AccountEnterpriseAccounts,
9+
AccountPhone,
10+
AccountUsername,
11+
AccountWeb3,
12+
} from './AccountSections';
1513
import { UserProfileSection } from './UserProfileSection';
16-
import { isAttributeAvailable } from './utils';
17-
import { Web3Section } from './Web3Section';
1814

1915
export const AccountPage = withCardStateProvider(() => {
20-
const { attributes, social, enterpriseSSO } = useEnvironment().userSettings;
2116
const card = useCardState();
22-
const { user } = useUser();
23-
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();
24-
25-
const showUsername = isAttributeAvailable(attributes.username);
26-
const showEmail = isAttributeAvailable(attributes.email_address);
27-
const showPhone = isAttributeAvailable(attributes.phone_number);
28-
const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0;
29-
const showEnterpriseAccounts = user && enterpriseSSO.enabled;
30-
const showWeb3 = attributes.web3_wallet?.enabled;
31-
32-
const isEmailImmutable = immutableAttributes.has('email_address');
33-
const isPhoneImmutable = immutableAttributes.has('phone_number');
34-
const isUsernameImmutable = immutableAttributes.has('username');
3517

3618
return (
3719
<ProfileCard.Page>
38-
<Col
39-
elementDescriptor={descriptors.page}
40-
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
20+
<ProfileCard.PagePanel
21+
pageId='account'
22+
titleKey={localizationKeys('userProfile.start.headerTitle__account')}
23+
alertContent={card.error}
24+
outerSx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
4125
>
42-
<Col
43-
elementDescriptor={descriptors.profilePage}
44-
elementId={descriptors.profilePage.setId('account')}
45-
>
46-
<Header.Root>
47-
<Header.Title
48-
localizationKey={localizationKeys('userProfile.start.headerTitle__account')}
49-
sx={t => ({ marginBottom: t.space.$4 })}
50-
textVariant='h2'
51-
/>
52-
</Header.Root>
53-
54-
<Card.Alert>{card.error}</Card.Alert>
55-
56-
<UserProfileSection />
57-
{showUsername && <UsernameSection isImmutable={isUsernameImmutable} />}
58-
{showEmail && (
59-
<EmailsSection
60-
shouldAllowCreation={shouldAllowIdentificationCreation && !isEmailImmutable}
61-
shouldAllowDeletion={!isEmailImmutable}
62-
/>
63-
)}
64-
{showPhone && (
65-
<PhoneSection
66-
shouldAllowCreation={shouldAllowIdentificationCreation && !isPhoneImmutable}
67-
shouldAllowDeletion={!isPhoneImmutable}
68-
/>
69-
)}
70-
{showConnectedAccounts && (
71-
<ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />
72-
)}
73-
74-
{/*TODO-STEP-UP: Verify that these work as expected*/}
75-
{showEnterpriseAccounts && <EnterpriseAccountsSection />}
76-
{showWeb3 && <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />}
77-
</Col>
78-
</Col>
26+
<UserProfileSection />
27+
<AccountUsername />
28+
<AccountEmails />
29+
<AccountPhone />
30+
<AccountConnectedAccounts />
31+
<AccountEnterpriseAccounts />
32+
<AccountWeb3 />
33+
</ProfileCard.PagePanel>
7934
</ProfileCard.Page>
8035
);
8136
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useUser } from '@clerk/shared/react';
2+
import type { ReactNode } from 'react';
3+
4+
import { useEnvironment, useUserProfileContext } from '../../contexts';
5+
import { ConnectedAccountsSection } from './ConnectedAccountsSection';
6+
import { EmailsSection } from './EmailsSection';
7+
import { EnterpriseAccountsSection } from './EnterpriseAccountsSection';
8+
import { PhoneSection } from './PhoneSection';
9+
import { UsernameSection } from './UsernameSection';
10+
import { isAttributeAvailable } from './utils';
11+
import { Web3Section } from './Web3Section';
12+
13+
export function AccountUsername(): ReactNode {
14+
const { attributes } = useEnvironment().userSettings;
15+
const { immutableAttributes } = useUserProfileContext();
16+
17+
if (!isAttributeAvailable(attributes.username)) {
18+
return null;
19+
}
20+
21+
const isImmutable = immutableAttributes.has('username');
22+
return <UsernameSection isImmutable={isImmutable} />;
23+
}
24+
25+
export function AccountEmails(): ReactNode {
26+
const { attributes } = useEnvironment().userSettings;
27+
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();
28+
29+
if (!isAttributeAvailable(attributes.email_address)) {
30+
return null;
31+
}
32+
33+
const isImmutable = immutableAttributes.has('email_address');
34+
return (
35+
<EmailsSection
36+
shouldAllowCreation={shouldAllowIdentificationCreation && !isImmutable}
37+
shouldAllowDeletion={!isImmutable}
38+
/>
39+
);
40+
}
41+
42+
export function AccountPhone(): ReactNode {
43+
const { attributes } = useEnvironment().userSettings;
44+
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();
45+
46+
if (!isAttributeAvailable(attributes.phone_number)) {
47+
return null;
48+
}
49+
50+
const isImmutable = immutableAttributes.has('phone_number');
51+
return (
52+
<PhoneSection
53+
shouldAllowCreation={shouldAllowIdentificationCreation && !isImmutable}
54+
shouldAllowDeletion={!isImmutable}
55+
/>
56+
);
57+
}
58+
59+
export function AccountConnectedAccounts(): ReactNode {
60+
const { social } = useEnvironment().userSettings;
61+
const { shouldAllowIdentificationCreation } = useUserProfileContext();
62+
63+
if (!social || Object.values(social).filter(p => p.enabled).length === 0) {
64+
return null;
65+
}
66+
67+
return <ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />;
68+
}
69+
70+
export function AccountEnterpriseAccounts(): ReactNode {
71+
const { enterpriseSSO } = useEnvironment().userSettings;
72+
const { user } = useUser();
73+
74+
if (!user || !enterpriseSSO.enabled) {
75+
return null;
76+
}
77+
78+
return <EnterpriseAccountsSection />;
79+
}
80+
81+
export function AccountWeb3(): ReactNode {
82+
const { attributes } = useEnvironment().userSettings;
83+
const { shouldAllowIdentificationCreation } = useUserProfileContext();
84+
85+
if (!attributes.web3_wallet?.enabled) {
86+
return null;
87+
}
88+
89+
return <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />;
90+
}

packages/ui/src/components/UserProfile/BillingPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const BillingPageInternal = withCardStateProvider(() => {
2727
<ProfileCard.Page>
2828
<Col
2929
elementDescriptor={descriptors.page}
30-
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
30+
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
3131
>
3232
<Col
3333
elementDescriptor={descriptors.profilePage}

0 commit comments

Comments
 (0)