Skip to content

Commit 71a832d

Browse files
committed
fix(mosaic): Open the combined profile on the first organization page
1 parent dac2fc3 commit 71a832d

6 files changed

Lines changed: 57 additions & 11 deletions

File tree

‎packages/mosaic/src/features/profile-pages/profile-pages.controller.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { useState } from 'react';
22

3-
export function useProfilePagesController(initialPage: string, pageIds: readonly string[] | undefined) {
3+
export function useProfilePagesController(
4+
initialPage: string,
5+
pageIds: readonly string[] | undefined,
6+
openingFallbackPage?: string,
7+
) {
48
const [activePage, setActivePage] = useState(initialPage);
9+
const [isOpening, setIsOpening] = useState(true);
510

611
if (pageIds) {
7-
const [firstPage] = pageIds;
8-
if (firstPage !== undefined && !pageIds.includes(activePage)) {
9-
setActivePage(firstPage);
12+
if (isOpening) {
13+
setIsOpening(false);
14+
}
15+
const fallbackPage = (isOpening ? openingFallbackPage : undefined) ?? pageIds[0];
16+
if (fallbackPage !== undefined && !pageIds.includes(activePage)) {
17+
setActivePage(fallbackPage);
1018
}
1119
}
1220

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,26 @@ describe('the profile the switchers open', () => {
235235
expect(await selectedTab('General')).toBeInTheDocument();
236236
});
237237

238+
it('opens a combined profile on the first organization page in the configured order', async () => {
239+
await renderWithProfile(<OrganizationSwitcher />, {
240+
organizationProfileProps: { customPages: [terms], pageOrder: ['terms'] },
241+
});
242+
243+
await clickSettings(switcherTrigger);
244+
245+
expect(await selectedTab('Terms')).toBeInTheDocument();
246+
});
247+
248+
it('skips a page this organization does not have when opening a combined profile', async () => {
249+
await renderWithProfile(<OrganizationSwitcher />, {
250+
organizationProfileProps: { pageOrder: ['apiKeys', 'members'] },
251+
});
252+
253+
await clickSettings(switcherTrigger);
254+
255+
expect(await selectedTab('Members')).toBeInTheDocument();
256+
});
257+
238258
it.each([
239259
{ name: 'the button', ui: <UserButton combined={false} />, trigger: buttonTrigger },
240260
{ name: 'the switcher', ui: <OrganizationSwitcher />, trigger: switcherTrigger },

‎packages/mosaic/src/features/switcher/switcher.model.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ORGANIZATION_PERMISSIONS } from '../../utils/permissions';
2020
import { organizationProfileModal } from '../organization-profile/organization-profile.modal';
2121
import type { MosaicProfile } from '../profile-pages/use-mosaic-profile';
2222
import { userProfileModalOptions } from '../profile-pages/use-mosaic-profile';
23+
import { ORGANIZATION_PAGE_PREFIX } from '../user-profile/user-profile.layout';
2324
import { userProfileModal } from '../user-profile/user-profile.modal';
2425
import type {
2526
SwitcherBrandingProps,
@@ -217,7 +218,13 @@ export function useSwitcherModel(
217218
apiKeysProps: organizationProfileProps?.apiKeysProps,
218219
});
219220
} else if (combined) {
220-
userProfile.open({ ...mosaicUserProfile, page: 'organization/general' }, getContainer?.());
221+
userProfile.open(
222+
{
223+
...mosaicUserProfile,
224+
page: `${ORGANIZATION_PAGE_PREFIX}${organizationProfileProps?.pageOrder?.[0] ?? 'general'}`,
225+
},
226+
getContainer?.(),
227+
);
221228
} else {
222229
organizationProfile.open(organizationProfileProps, getContainer?.());
223230
}

‎packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ describe('UserButton', () => {
165165
it('renders nothing while Clerk is still loading', async () => {
166166
serveFapi(signedIn());
167167
const client = holdRequests('get', '/v1/client');
168-
const rendering = renderWithClerk(tree({ mode }));
168+
const rendering = renderWithClerk(tree({ combined: mode === 'combined' }));
169169

170170
await waitFor(() => expect(client.requests).toHaveLength(1));
171171
expect(host()).toBeEmptyDOMElement();
@@ -176,14 +176,17 @@ describe('UserButton', () => {
176176
});
177177

178178
it('renders nothing, not even its fallback, when nobody is signed in', async () => {
179-
await renderUserButton({ mode, fallback: <output>Loading</output> }, signedIn({ client: fapiClient() }));
179+
await renderUserButton(
180+
{ combined: mode === 'combined', fallback: <output>Loading</output> },
181+
signedIn({ client: fapiClient() }),
182+
);
180183

181184
expect(host()).toBeEmptyDOMElement();
182185
});
183186

184187
it('leaves organizations out entirely when the instance has them disabled', async () => {
185188
await renderUserButton(
186-
{ mode },
189+
{ combined: mode === 'combined' },
187190
signedIn({ environment: fapiEnvironment({ organization_settings: { enabled: false } }) }),
188191
);
189192

‎packages/mosaic/src/features/user-profile/user-profile.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { OrganizationProfileProvider } from '../organization-profile/organizatio
1010
import { useProfilePagesController } from '../profile-pages/profile-pages.controller';
1111
import { useMosaicProfile } from '../profile-pages/use-mosaic-profile';
1212
import { useUserProfilePages } from '../switcher/switcher.pages';
13-
import { userProfilePageIds } from './user-profile.layout';
13+
import { ORGANIZATION_PAGE_PREFIX, userProfilePageIds } from './user-profile.layout';
1414
import type { UserProfileOptions } from './user-profile.provider';
1515
import { UserProfileProvider } from './user-profile.provider';
1616
import type { UserProfileViewProps } from './user-profile.view';
@@ -226,9 +226,13 @@ function CombinedUserProfileView({
226226
model.status === 'ready'
227227
? { pages: organizationProfilePanels(model.pages, apiKeysProps), customPages, pageOrder }
228228
: undefined;
229+
const pageIds = model.status === 'loading' ? undefined : userProfilePageIds({ ...viewProps, organization });
229230
const navigation = useProfilePagesController(
230231
initialPage,
231-
model.status === 'loading' ? undefined : userProfilePageIds({ ...viewProps, organization }),
232+
pageIds,
233+
initialPage.startsWith(ORGANIZATION_PAGE_PREFIX)
234+
? pageIds?.find(id => id.startsWith(ORGANIZATION_PAGE_PREFIX))
235+
: undefined,
232236
);
233237

234238
return (

‎packages/swingset/src/stories/mosaic-provider.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ components open.
99
Wrap the part of your app that uses Mosaic. Every prop is optional.
1010

1111
```tsx
12-
import { MosaicProvider } from '@clerk/mosaic';
12+
import { MosaicProvider, UserButton } from '@clerk/mosaic';
1313

1414
<MosaicProvider>
1515
<UserButton />
@@ -41,6 +41,7 @@ themselves.
4141

4242
```tsx
4343
<MosaicProvider
44+
experimental={{ newProfiles: true }}
4445
profile={{
4546
combined: true,
4647
userProfileUrl: '/account',
@@ -56,6 +57,9 @@ themselves.
5657
`combined` on `MosaicProvider` decides what the profile shows, and `combined` on `UserButton` decides what
5758
its menu lists. [Profile permutations](/user-button/profile-permutations) shows each pairing.
5859

60+
`profile.combined` takes effect only with `experimental.newProfiles`. Without it, `UserButton` and
61+
`OrganizationSwitcher` open Clerk's modals.
62+
5963
| Option | Type | Default | Description |
6064
| -------------------------- | -------------------------- | ------- | --------------------------------------------------------------------------------- |
6165
| `combined` | `boolean` | `true` | Whether one profile carries the account's pages and the active organization's. |

0 commit comments

Comments
 (0)