From 73e4bbb0fa60fbada7103e817bae770538f5b759 Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Fri, 9 Oct 2026 19:11:06 -0400
Subject: [PATCH 1/3] refactor(mosaic): render self-contained sections in the
organization general panel
---
.../mosaic-org-general-panel-sections.md | 2 +
.../organization-profile.layout.test.tsx | 2 +-
.../organization-profile.view.test.tsx | 2 +-
...ion-profile-general-panel.feature.test.tsx | 40 +++++++++++++++++--
.../organization-profile-general-panel.tsx | 39 +++++++++---------
...rganization-profile-general-panel.view.tsx | 32 ++-------------
...nization-profile-profile-section.model.ts} | 14 +++----
.../organization-profile-profile-section.tsx | 27 +++++++++++++
.../stories/fixtures/organization-profile.tsx | 22 ++++++----
.../organization-profile-general-panel.mdx | 23 ++++++++++-
...nization-profile-general-panel.stories.tsx | 17 ++++----
...zation-profile-profile-section.stories.tsx | 36 ++++++++---------
12 files changed, 161 insertions(+), 95 deletions(-)
create mode 100644 .changeset/mosaic-org-general-panel-sections.md
rename packages/mosaic/src/features/organization-profile/{organization-profile-general-panel.model.ts => organization-profile-profile-section/organization-profile-profile-section.model.ts} (77%)
create mode 100644 packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.tsx
diff --git a/.changeset/mosaic-org-general-panel-sections.md b/.changeset/mosaic-org-general-panel-sections.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/mosaic-org-general-panel-sections.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.layout.test.tsx b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.layout.test.tsx
index b9a99f16665..a82af4dfba5 100644
--- a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.layout.test.tsx
+++ b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.layout.test.tsx
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
import { getAvailableOrganizationProfilePages } from '../organization-profile.layout';
import type { OrganizationProfilePages } from '../organization-profile.types';
-const general: OrganizationProfilePages['general'] = { name: 'Acme', slug: 'acme', memberCount: 1 };
+const general: OrganizationProfilePages['general'] = {};
describe('getAvailableOrganizationProfilePages', () => {
it('lists api keys given content', () => {
diff --git a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
index d26c79c8c8d..5c50d715f39 100644
--- a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
+++ b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.view.test.tsx
@@ -11,7 +11,7 @@ it('keeps the Members placeholder until a table is configured', () => {
activePage='members'
onPageChange={vi.fn()}
pages={{
- general: { name: 'Acme', slug: 'acme', memberCount: 1 },
+ general: {},
members: {},
}}
/>
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx
index 868d34526f2..db60148b232 100644
--- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx
@@ -15,7 +15,9 @@ import {
} from '../../__tests__/feature/fapi';
import { renderWithClerk } from '../../__tests__/feature/render';
import { MosaicProvider } from '../../mosaic-provider';
+import { OrganizationProfileDangerSection } from './organization-profile-danger-section/organization-profile-danger-section';
import { OrganizationProfileGeneralPanel } from './organization-profile-general-panel';
+import { OrganizationProfileProfileSection } from './organization-profile-profile-section/organization-profile-profile-section';
const acme = fapiOrganization({ id: 'org_1', name: 'Acme', slug: 'acme' });
@@ -47,9 +49,41 @@ async function renderPanel(seed: FakeFapiSeed = signedIn()) {
describe('OrganizationProfileGeneralPanel', () => {
describe('availability', () => {
- it('hides without an active organization', async () => {
- const { container } = await renderPanel(signedIn({ activeOrganizationId: null }));
- expect(container).toBeEmptyDOMElement();
+ it('keeps the title and hides every section without an active organization', async () => {
+ await renderPanel(signedIn({ activeOrganizationId: null }));
+ expect(await screen.findByRole('heading', { name: 'General', level: 2 })).toBeVisible();
+ expect(screen.queryAllByRole('heading', { level: 3 })).toHaveLength(0);
+ });
+
+ it('renders the given sections in order instead of the defaults', async () => {
+ serveFapi(signedIn());
+ await renderWithClerk(
+
+
+
+ ,
+ );
+ await screen.findByRole('heading', { name: 'Organization details' });
+ expect(screen.getAllByRole('heading', { level: 3 }).map(heading => heading.textContent)).toEqual([
+ 'Danger zone',
+ 'Organization details',
+ ]);
+ });
+
+ it('shows the section fallback while the organization loads', async () => {
+ serveFapi(signedIn());
+ const loading = renderWithClerk(
+
+ Loading details
} />
+ ,
+ );
+ try {
+ expect(screen.getByText('Loading details')).toBeInTheDocument();
+ } finally {
+ await loading;
+ }
+ expect(await screen.findByRole('heading', { name: 'Organization details' })).toBeVisible();
+ expect(screen.queryByText('Loading details')).toBeNull();
});
it('shows details and danger but omits mutation controls without manage permission', async () => {
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
index cbc6ffc1d91..a9d4a821641 100644
--- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
@@ -1,29 +1,26 @@
+import type { ReactNode } from 'react';
+
import { OrganizationProfileDangerSection } from './organization-profile-danger-section/organization-profile-danger-section';
-import { useOrganizationProfileGeneralPanelModel } from './organization-profile-general-panel.model';
import { OrganizationProfileGeneralPanelView } from './organization-profile-general-panel.view';
+import { OrganizationProfileProfileSection } from './organization-profile-profile-section/organization-profile-profile-section';
-export function OrganizationProfileGeneralPanel({
- fallback,
- afterLeaveOrganizationUrl,
-}: {
- fallback?: React.ReactNode;
+export interface OrganizationProfileGeneralPanelProps {
+ children?: ReactNode;
afterLeaveOrganizationUrl?: string;
-}) {
- const model = useOrganizationProfileGeneralPanelModel();
-
- if (model.status === 'loading') {
- return fallback ?? null;
- }
- if (model.status === 'hidden') {
- return null;
- }
+}
- const { status, organizationId, ...general } = model;
+export function OrganizationProfileGeneralPanel({
+ children,
+ afterLeaveOrganizationUrl,
+}: OrganizationProfileGeneralPanelProps) {
return (
- }
- />
+
+ {children ?? (
+ <>
+
+
+ >
+ )}
+
);
}
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.view.tsx
index e201a7037a3..a2a9e5d110c 100644
--- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.view.tsx
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.view.tsx
@@ -3,44 +3,20 @@ import type { ReactElement, ReactNode } from 'react';
import { Panel } from '../../components/panel';
import { useMessages } from '../../localization';
import { themeProps } from '../../props';
-import type { OrganizationProfileProfileSectionViewProps } from './organization-profile-profile-section/organization-profile-profile-section.view';
-import { OrganizationProfileProfileSectionView } from './organization-profile-profile-section/organization-profile-profile-section.view';
-export interface OrganizationProfileGeneralPanelViewProps extends OrganizationProfileProfileSectionViewProps {
- dangerSlot?: ReactNode;
+export interface OrganizationProfileGeneralPanelViewProps {
+ children?: ReactNode;
}
export function OrganizationProfileGeneralPanelView({
- name,
- slug,
- imageUrl,
- hasImage,
- onLogoChange,
- onLogoReject,
- onRemoveLogo,
- onSubmitName,
- onSubmitSlug,
- dangerSlot,
+ children,
}: OrganizationProfileGeneralPanelViewProps): ReactElement {
const m = useMessages('organizationProfile');
return (
}>
{m.pages.general}
-
-
- {dangerSlot}
-
+ {children}
);
}
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.model.ts b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.model.ts
similarity index 77%
rename from packages/mosaic/src/features/organization-profile/organization-profile-general-panel.model.ts
rename to packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.model.ts
index c75bf9d847e..76dbbf1ce2a 100644
--- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.model.ts
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.model.ts
@@ -1,18 +1,16 @@
import { useClerk, useOrganization, useSession } from '@clerk/shared/react';
import type { OrganizationResource } from '@clerk/shared/types';
-import { useMosaicEnvironment } from '../../hooks/use-mosaic-environment';
-import { save, SaveError, UNEXPECTED_ERROR } from '../../utils/errors';
-import type { OrganizationProfileGeneralPanelViewProps } from './organization-profile-general-panel.view';
+import { useMosaicEnvironment } from '../../../hooks/use-mosaic-environment';
+import { save, SaveError, UNEXPECTED_ERROR } from '../../../utils/errors';
+import type { OrganizationProfileProfileSectionViewProps } from './organization-profile-profile-section.view';
-type GeneralData = Omit;
-
-type OrganizationProfileGeneralPanelModel =
+type OrganizationProfileProfileSectionModel =
| { status: 'loading' }
| { status: 'hidden' }
- | (GeneralData & { status: 'ready'; organizationId: string });
+ | (OrganizationProfileProfileSectionViewProps & { status: 'ready'; organizationId: string });
-export function useOrganizationProfileGeneralPanelModel(): OrganizationProfileGeneralPanelModel {
+export function useOrganizationProfileProfileSectionModel(): OrganizationProfileProfileSectionModel {
const { isLoaded, organization, membership } = useOrganization();
const { isLoaded: isSessionLoaded, session } = useSession();
const clerk = useClerk();
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.tsx
new file mode 100644
index 00000000000..48ee7502d6e
--- /dev/null
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from 'react';
+
+import { useOrganizationProfileProfileSectionModel } from './organization-profile-profile-section.model';
+import { OrganizationProfileProfileSectionView } from './organization-profile-profile-section.view';
+
+export interface OrganizationProfileProfileSectionProps {
+ fallback?: ReactNode;
+}
+
+export function OrganizationProfileProfileSection({ fallback }: OrganizationProfileProfileSectionProps) {
+ const model = useOrganizationProfileProfileSectionModel();
+
+ if (model.status === 'loading') {
+ return fallback ?? null;
+ }
+ if (model.status === 'hidden') {
+ return null;
+ }
+
+ const { status, organizationId, ...profile } = model;
+ return (
+
+ );
+}
diff --git a/packages/swingset/src/stories/fixtures/organization-profile.tsx b/packages/swingset/src/stories/fixtures/organization-profile.tsx
index 38c0c4eaeaf..f41e850d29f 100644
--- a/packages/swingset/src/stories/fixtures/organization-profile.tsx
+++ b/packages/swingset/src/stories/fixtures/organization-profile.tsx
@@ -1,6 +1,8 @@
import { useDestructiveController } from '@clerk/mosaic/blocks/destructive/destructive.controller';
import type { OrganizationProfileViewProps } from '@clerk/mosaic/features/organization-profile/organization-profile.view';
import { OrganizationProfileDangerSectionView } from '@clerk/mosaic/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view';
+import type { OrganizationProfileProfileSectionViewProps } from '@clerk/mosaic/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view';
+import { OrganizationProfileProfileSectionView } from '@clerk/mosaic/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view';
import { SaveError } from '@clerk/mosaic/utils/errors';
import { useState } from 'react';
@@ -61,7 +63,7 @@ export function useOrganizationProfileFixture({ failWith }: OrganizationProfileF
apply();
};
- const general: OrganizationProfileViewProps['pages']['general'] = {
+ const profile: OrganizationProfileProfileSectionViewProps = {
name,
slug,
imageUrl,
@@ -70,11 +72,17 @@ export function useOrganizationProfileFixture({ failWith }: OrganizationProfileF
onRemoveLogo: clearImage,
onSubmitName: async next => save('name', () => setName(next)),
onSubmitSlug: async next => save('slug', () => setSlug(next)),
- dangerSlot: (
-
+ };
+
+ const general: OrganizationProfileViewProps['pages']['general'] = {
+ children: (
+ <>
+
+
+ >
),
};
@@ -86,5 +94,5 @@ export function useOrganizationProfileFixture({ failWith }: OrganizationProfileF
apiKeys: ,
};
- return { activePage, setActivePage, pages, general, name };
+ return { activePage, setActivePage, pages, general, profile, name };
}
diff --git a/packages/swingset/src/stories/organization-profile-general-panel.mdx b/packages/swingset/src/stories/organization-profile-general-panel.mdx
index cb7ec17a2ec..3484037b66e 100644
--- a/packages/swingset/src/stories/organization-profile-general-panel.mdx
+++ b/packages/swingset/src/stories/organization-profile-general-panel.mdx
@@ -13,9 +13,30 @@ The organization's General page: its details, and the danger zone under them.
]}
/>
+## Usage
+
+`OrganizationProfileGeneralPanel` renders the General title with the organization details and danger zone sections. Each section shows itself only when there is an active organization. `afterLeaveOrganizationUrl` is passed to the default danger zone section.
+
+```tsx
+import { OrganizationProfileGeneralPanel } from '@clerk/mosaic/features/organization-profile/organization-profile-general-panel';
+
+ ;
+```
+
+Pass sections as children to choose which ones render and in what order.
+
+```tsx
+import { OrganizationProfileGeneralPanel } from '@clerk/mosaic/features/organization-profile/organization-profile-general-panel';
+import { OrganizationProfileProfileSection } from '@clerk/mosaic/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section';
+
+
+
+ ;
+```
+
## Without the danger zone
-The danger zone renders from `dangerSlot`. Without it, the panel shows only the organization details.
+The panel shows only the organization details, read-only.
+
+
+
);
}
diff --git a/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx b/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx
index 31a9f54eb46..44cb61aed6c 100644
--- a/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx
+++ b/packages/swingset/src/stories/organization-profile-profile-section.stories.tsx
@@ -18,33 +18,33 @@ export const meta: StoryMeta = {
};
export function Default() {
- const { general } = useOrganizationProfileFixture();
+ const { profile } = useOrganizationProfileFixture();
return (
);
}
export function SaveFails() {
- const { general } = useOrganizationProfileFixture({ failWith: { slug: 'That slug is already taken.' } });
+ const { profile } = useOrganizationProfileFixture({ failWith: { slug: 'That slug is already taken.' } });
return (
);
}
From 42557b4899ef2f650f3aa068bf3d363861ae8c93 Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Fri, 9 Oct 2026 19:16:14 -0400
Subject: [PATCH 2/3] feat(swingset): add live organization details section
---
.../live/organization-details/page.tsx | 40 +++++++++++++++++++
packages/swingset/src/lib/live-navigation.ts | 1 +
2 files changed, 41 insertions(+)
create mode 100644 packages/swingset/src/app/(clerk)/live/organization-details/page.tsx
diff --git a/packages/swingset/src/app/(clerk)/live/organization-details/page.tsx b/packages/swingset/src/app/(clerk)/live/organization-details/page.tsx
new file mode 100644
index 00000000000..5cd80e0b7d7
--- /dev/null
+++ b/packages/swingset/src/app/(clerk)/live/organization-details/page.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import { OrganizationProfileProfileSection } from '@clerk/mosaic/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section';
+import { MosaicProvider } from '@clerk/mosaic/mosaic-provider';
+import { useOrganization, useUser } from '@clerk/nextjs';
+import Link from 'next/link';
+
+export default function OrganizationDetailsLivePage() {
+ const { isLoaded, isSignedIn } = useUser();
+ const { organization } = useOrganization();
+
+ return (
+
+
+
+
Organization details
+
+ The active organization's logo, name, and slug. Editing needs the manage profile permission.
+
+
+ {!isLoaded ?
Loading…
: null}
+ {isLoaded && !isSignedIn ? (
+
+
+ Sign in
+ {' '}
+ and pick an active organization.
+
+ ) : null}
+ {isLoaded && isSignedIn && !organization ? (
+
Pick an active organization to see its details.
+ ) : null}
+ {isLoaded && isSignedIn ?
: null}
+
+
+ );
+}
diff --git a/packages/swingset/src/lib/live-navigation.ts b/packages/swingset/src/lib/live-navigation.ts
index d75b60268e2..ff450aed57f 100644
--- a/packages/swingset/src/lib/live-navigation.ts
+++ b/packages/swingset/src/lib/live-navigation.ts
@@ -25,6 +25,7 @@ const liveRoutes = new Map([
['/organization-profile/organization-profile-general-panel', '/live/organization-general'],
['/organization-profile/organization-profile-members-panel', '/live/organization-members'],
['/organization-profile/organization-profile-api-keys-panel', '/live/organization-api-keys'],
+ ['/organization-profile/organization-profile-profile-section', '/live/organization-details'],
['/organization-profile/organization-profile-danger-section', '/live/organization-danger'],
['/user-profile/user-profile-passkeys-section', '/live/passkeys'],
['/user-profile/user-profile-web3wallets-section', '/live/web3-wallets'],
From a1094fff9020462a521adb1e7dee9e28660ba50f Mon Sep 17 00:00:00 2001
From: Alex Carpenter
Date: Fri, 9 Oct 2026 19:23:07 -0400
Subject: [PATCH 3/3] refactor(mosaic): drop afterLeaveOrganizationUrl from the
organization general panel
---
.../organization-profile-general-panel.tsx | 8 ++------
.../src/app/(clerk)/live/organization-general/page.tsx | 2 +-
.../src/stories/organization-profile-general-panel.mdx | 4 ++--
3 files changed, 5 insertions(+), 9 deletions(-)
diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
index a9d4a821641..7b0833e2340 100644
--- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
+++ b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.tsx
@@ -6,19 +6,15 @@ import { OrganizationProfileProfileSection } from './organization-profile-profil
export interface OrganizationProfileGeneralPanelProps {
children?: ReactNode;
- afterLeaveOrganizationUrl?: string;
}
-export function OrganizationProfileGeneralPanel({
- children,
- afterLeaveOrganizationUrl,
-}: OrganizationProfileGeneralPanelProps) {
+export function OrganizationProfileGeneralPanel({ children }: OrganizationProfileGeneralPanelProps) {
return (
{children ?? (
<>
-
+
>
)}
diff --git a/packages/swingset/src/app/(clerk)/live/organization-general/page.tsx b/packages/swingset/src/app/(clerk)/live/organization-general/page.tsx
index 52fc3208d95..435c51857fb 100644
--- a/packages/swingset/src/app/(clerk)/live/organization-general/page.tsx
+++ b/packages/swingset/src/app/(clerk)/live/organization-general/page.tsx
@@ -27,7 +27,7 @@ export default function OrganizationGeneralLivePage() {
{isLoaded && isSignedIn && !organization ? (
Pick an active organization to see its general panel.
) : null}
- {isLoaded && isSignedIn ? : null}
+ {isLoaded && isSignedIn ? : null}
);
diff --git a/packages/swingset/src/stories/organization-profile-general-panel.mdx b/packages/swingset/src/stories/organization-profile-general-panel.mdx
index 3484037b66e..d556647a7ab 100644
--- a/packages/swingset/src/stories/organization-profile-general-panel.mdx
+++ b/packages/swingset/src/stories/organization-profile-general-panel.mdx
@@ -15,12 +15,12 @@ The organization's General page: its details, and the danger zone under them.
## Usage
-`OrganizationProfileGeneralPanel` renders the General title with the organization details and danger zone sections. Each section shows itself only when there is an active organization. `afterLeaveOrganizationUrl` is passed to the default danger zone section.
+`OrganizationProfileGeneralPanel` renders the General title with the organization details and danger zone sections. Each section shows itself only when there is an active organization.
```tsx
import { OrganizationProfileGeneralPanel } from '@clerk/mosaic/features/organization-profile/organization-profile-general-panel';
- ;
+ ;
```
Pass sections as children to choose which ones render and in what order.