Skip to content

Commit 18e84ec

Browse files
committed
feat(mosaic): Configure the profile on MosaicProvider
1 parent 16ff75a commit 18e84ec

24 files changed

Lines changed: 511 additions & 197 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@clerk/mosaic': minor
3+
---
4+
5+
Configure the profile on `MosaicProvider` with the new `profile` prop, so `UserButton`, `OrganizationSwitcher` and `UserProfile` share one setup. This makes pairings like a user-only `UserButton` beside an `OrganizationSwitcher` with one combined profile possible.
6+
7+
```tsx
8+
<MosaicProvider profile={{ combined: true, userProfileProps, organizationProfileProps }}>
9+
<OrganizationSwitcher />
10+
<UserButton combined={false} />
11+
</MosaicProvider>
12+
```
13+
14+
Breaking changes:
15+
16+
- `UserButton`: `mode` is replaced by `combined`, which only decides what the menu lists. `mode='user'` becomes `combined={false}`, and to keep the account-only profile too, also set `profile={{ combined: false }}` on `MosaicProvider`.
17+
- `UserButton` and `OrganizationSwitcher`: `userProfileProps` and `organizationProfileProps` move to `MosaicProvider`'s `profile`.
18+
- `UserProfile`: `mode` is replaced by `combined`. When unset it follows `profile.combined`, and it also picks up the provider's profile props, with its own props winning.
19+
- With `experimental.newProfiles`, `OrganizationSwitcher` now opens the combined profile by default. Set `profile={{ combined: false }}` for the standalone organization profile.
20+
- The `UserProfileMode` type is removed.

‎.changeset/profile-permutations-swingset.md‎

Lines changed: 0 additions & 2 deletions
This file was deleted.

‎integration/templates/next-app-router-mosaic/src/app/custom/page.tsx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { MosaicProvider, UserButton } from '@clerk/mosaic';
44

55
export default function Page() {
66
return (
7-
<MosaicProvider experimental={{ newProfiles: true }}>
8-
<UserButton
9-
customMenuItems={[{ id: 'custom-link', label: 'Custom link', href: '/custom/link-target' }]}
10-
userProfileProps={{
7+
<MosaicProvider
8+
experimental={{ newProfiles: true }}
9+
profile={{
10+
userProfileProps: {
1111
customPages: [{ label: 'Custom page', path: 'custom-page', content: <p>custom-page-content</p> }],
12-
}}
13-
/>
12+
},
13+
}}
14+
>
15+
<UserButton customMenuItems={[{ id: 'custom-link', label: 'Custom link', href: '/custom/link-target' }]} />
1416
</MosaicProvider>
1517
);
1618
}

‎integration/templates/next-app-router-mosaic/src/app/organization-switcher/page.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { MosaicProvider, OrganizationSwitcher } from '@clerk/mosaic';
44

55
export default function Page() {
66
return (
7-
<MosaicProvider experimental={{ newProfiles: true }}>
7+
<MosaicProvider
8+
experimental={{ newProfiles: true }}
9+
profile={{
10+
organizationProfileProps: {
11+
customPages: [{ label: 'Custom page', path: 'custom-page', content: <p>custom-page-content</p> }],
12+
},
13+
}}
14+
>
815
<OrganizationSwitcher
916
customMenuItems={[{ id: 'custom-link', label: 'Custom link', href: '/custom/link-target' }]}
10-
organizationProfileProps={{
11-
customPages: [{ label: 'Custom page', path: 'custom-page', content: <p>custom-page-content</p> }],
12-
}}
1317
/>
1418
</MosaicProvider>
1519
);

‎packages/mosaic/src/__tests__/feature/render.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { act, render } from '@testing-library/react';
44
import type { ReactElement } from 'react';
55
import { vi } from 'vitest';
66

7+
import type { MosaicProfile } from '../../features/profile-pages/use-mosaic-profile';
78
import type { MosaicExperimental } from '../../hooks/use-mosaic-experimental';
89
import type { MosaicLocalization } from '../../localization';
910
import { MosaicProvider } from '../../mosaic-provider';
@@ -14,6 +15,7 @@ export async function renderWithClerk(
1415
options?: Parameters<Clerk['load']>[0],
1516
localization?: MosaicLocalization,
1617
experimental?: MosaicExperimental,
18+
profile?: MosaicProfile,
1719
) {
1820
const clerk = new Clerk(publishableKey());
1921
const navigate = vi.fn((_to: string) => Promise.resolve());
@@ -26,6 +28,7 @@ export async function renderWithClerk(
2628
<MosaicProvider
2729
localization={localization}
2830
experimental={experimental}
31+
profile={profile}
2932
>
3033
{element}
3134
</MosaicProvider>

‎packages/mosaic/src/features/organization-switcher/__tests__/organization-switcher.feature.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ describe('OrganizationSwitcher with experimental.newProfiles', () => {
381381
await page.viewport(1280, 800);
382382
onTestFinished(() => page.viewport(414, 896));
383383
serveFapi(noOffers());
384-
return renderWithClerk(tree(props), undefined, undefined, { newProfiles: true });
384+
return renderWithClerk(tree(props), undefined, undefined, { newProfiles: true }, { combined: false });
385385
}
386386

387387
it('opens the OrganizationProfile, without the account pages, from Settings', async () => {

‎packages/mosaic/src/features/organization-switcher/organization-switcher.tsx‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import type { OrganizationSwitcherModelOptions } from '../switcher/switcher.mode
88
import type { SwitcherMenuProps } from '../switcher/switcher.types';
99
import type { SwitcherTriggerProps } from '../switcher/switcher.view';
1010

11-
/** Everything `<OrganizationSwitcher />` takes: profile routing, trigger content, the app's own menu rows, and the profile it opens. */
11+
/** Everything `<OrganizationSwitcher />` takes: profile routing, trigger content, and the app's own menu rows. */
1212
export type OrganizationSwitcherProps = OrganizationSwitcherModelOptions &
1313
SwitcherTriggerProps &
1414
SwitcherMenuProps &
15-
Pick<SwitcherProps, 'organizationProfileProps' | 'fallback'>;
15+
Pick<SwitcherProps, 'fallback'>;
1616

1717
/**
1818
* The active organization's avatar, and the menu behind it: switch organization or back to the
@@ -47,14 +47,10 @@ export type OrganizationSwitcherProps = OrganizationSwitcherModelOptions &
4747
* ```
4848
*
4949
* @example
50-
* `customPages` adds your own pages to the organization profile; `customMenuItems` adds your own rows
51-
* beside Settings, each one either an `onClick` action or an `href` link.
50+
* `customMenuItems` adds your own rows beside Settings, each one either an `onClick` action or an
51+
* `href` link. The profile's own pages are set on `MosaicProvider`'s `profile`.
5252
* ```tsx
5353
* <OrganizationSwitcher
54-
* organizationProfileProps={{
55-
* customPages: [{ path: 'audit', label: 'Audit log', icon: <ListIcon />, content: <AuditPage /> }],
56-
* pageOrder: ['general', 'members', 'audit'],
57-
* }}
5854
* customMenuItems={[{ id: 'docs', label: 'Documentation', icon: <BookIcon />, href: 'https://example.com/docs' }]}
5955
* />
6056
* ```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { OrganizationProfileProps, UserProfileProps } from '@clerk/shared/types';
2+
import { createContext, useContext } from 'react';
3+
4+
import type { OrganizationProfilePageId } from '../switcher/switcher.pages';
5+
import type { CustomProfileItem, UserProfilePageId } from '../user-profile/user-profile.types';
6+
7+
export interface MosaicProfile {
8+
/**
9+
* Whether one profile carries the account's pages and the active organization's. `false` keeps them
10+
* in separate profiles. It decides the profile's shape, not what `UserButton`'s menu lists.
11+
*
12+
* @default true
13+
*/
14+
combined?: boolean;
15+
/**
16+
* Options for the user profile, whichever component opens or mounts it.
17+
*/
18+
userProfileProps?: ProfileUserProps;
19+
/**
20+
* Options for the organization profile, or the organization's pages in a combined profile,
21+
* whichever component opens or mounts it.
22+
*/
23+
organizationProfileProps?: ProfileOrganizationProps;
24+
}
25+
26+
/** What a profile takes beyond the profile component's own props. */
27+
export interface ProfilePages<PageId extends string> {
28+
/**
29+
* Provide custom pages and links to be rendered inside the profile.
30+
*/
31+
customPages?: CustomProfileItem[];
32+
/**
33+
* Controls the order of the profile's navigation. Accepts the ids of built-in pages and the
34+
* `path` of custom pages. Pages not listed are placed after the listed ones. The first entry is
35+
* the page the profile opens on, so it cannot be a link.
36+
*
37+
* @default undefined
38+
*/
39+
pageOrder?: (PageId | (string & {}))[];
40+
}
41+
42+
/** Options for the underlying `<UserProfile />` component. */
43+
export interface ProfileUserProps
44+
extends
45+
ProfilePages<UserProfilePageId>,
46+
Pick<UserProfileProps, 'additionalOAuthScopes' | 'apiKeysProps' | 'appearance'> {}
47+
48+
/** Options for the underlying `<OrganizationProfile />` component. */
49+
export interface ProfileOrganizationProps
50+
extends ProfilePages<OrganizationProfilePageId>, Pick<OrganizationProfileProps, 'appearance'> {}
51+
52+
const MosaicProfileContext = createContext<MosaicProfile>({});
53+
54+
export const MosaicProfileProvider = MosaicProfileContext.Provider;
55+
56+
export function useMosaicProfile(): MosaicProfile {
57+
return useContext(MosaicProfileContext);
58+
}

0 commit comments

Comments
 (0)