diff --git a/.changeset/swift-stingrays-cough.md b/.changeset/swift-stingrays-cough.md new file mode 100644 index 00000000000..755e801d138 --- /dev/null +++ b/.changeset/swift-stingrays-cough.md @@ -0,0 +1,5 @@ +--- +"@clerk/astro": minor +--- + +Add support for custom pages and links in the `` Astro component. diff --git a/integration/templates/astro-node/src/pages/custom-pages.astro b/integration/templates/astro-node/src/pages/custom-pages.astro deleted file mode 100644 index 21d598adc7b..00000000000 --- a/integration/templates/astro-node/src/pages/custom-pages.astro +++ /dev/null @@ -1,22 +0,0 @@ ---- -import { UserProfile } from "@clerk/astro/components"; -import Layout from "../layouts/Layout.astro"; ---- - - -
- - -
Icon
-
-

Custom Terms Page

-

This is the custom terms page

-
-
- -
Icon
-
- -
-
-
diff --git a/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro b/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro new file mode 100644 index 00000000000..a00aeed5672 --- /dev/null +++ b/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro @@ -0,0 +1,51 @@ +--- +import { + OrganizationProfile as OrganizationProfileAstro, + OrganizationSwitcher +} from "@clerk/astro/components"; +import Layout from "../../layouts/Layout.astro"; + +// Added a dedicatedPage query param to conditionally render the OrganizationProfile +// as for some reason, the menu items in the OrganizationSwitcher +// goes out of bounds in test environment. +const dedicatedPage = Astro.url.searchParams.get('dedicatedPage') === 'true'; +--- + + +
+ + { + !dedicatedPage && ( + +
Icon
+
+

Custom Terms Page

+

This is the custom terms page

+
+
+ +
Icon
+
+ + ) + } +
+ { + dedicatedPage && ( + + +
Icon
+
+

Custom Terms Page

+

This is the custom terms page

+
+
+ +
Icon
+
+ +
+ ) + } +
+
diff --git a/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro b/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro new file mode 100644 index 00000000000..fd9da776934 --- /dev/null +++ b/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro @@ -0,0 +1,22 @@ +--- +import { UserProfile as UserProfileAstro } from "@clerk/astro/components"; +import Layout from "../../layouts/Layout.astro"; +--- + + +
+ + +
Icon
+
+

Custom Terms Page

+

This is the custom terms page

+
+
+ +
Icon
+
+ +
+
+
diff --git a/integration/tests/astro/components.test.ts b/integration/tests/astro/components.test.ts index 62a697e71d9..8f9b3f9dfd3 100644 --- a/integration/tests/astro/components.test.ts +++ b/integration/tests/astro/components.test.ts @@ -164,7 +164,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); await u.po.expect.toBeSignedIn(); - await u.page.goToRelative('/custom-pages'); + await u.page.goToRelative('/custom-pages/user-profile'); await u.po.userProfile.waitForMounted(); // Check if custom pages and links are visible @@ -176,7 +176,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); // Check reordered default label. Security tab is now the last item. - await u.page.locator('.cl-navbarButton').nth(3).click(); + await u.page.locator('.cl-navbarButton').last().click(); await expect(u.page.getByRole('heading', { name: 'Security' })).toBeVisible(); // Click custom link and check navigation @@ -252,7 +252,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f await fakeAdmin.deleteIfExists(); }); - test('test updateClerkOptions by changing localization on the fly', async ({ page, context }) => { + test('updateClerkOptions by changing localization on the fly', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.signIn.goTo(); await u.po.signIn.waitForMounted(); @@ -268,6 +268,70 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f await expect(u.page.getByText('pour continuer vers')).toBeVisible(); }); + test('render organization profile with custom pages and links in dedicated page', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/sign-in'); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/custom-pages/organization-profile?dedicatedPage=true'); + await u.po.organizationSwitcher.waitForMounted(); + await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); + + // Check if custom pages and links are visible + await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible(); + await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible(); + + // Navigate to custom page + await u.page.getByRole('button', { name: /Terms/i }).click(); + await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); + + // Check reordered default label. General tab is now the last item. + await u.page.locator('.cl-navbarButton').last().click(); + await expect(u.page.getByRole('heading', { name: 'General' })).toBeVisible(); + + // Click custom link and check navigation + await u.page.getByRole('button', { name: /Homepage/i }).click(); + await u.page.waitForAppUrl('/'); + }); + + test('render organization profile with custom pages and links inside organization switcher', async ({ + page, + context, + }) => { + const u = createTestUtils({ app, page, context }); + await u.page.goToRelative('/sign-in'); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password }); + await u.po.expect.toBeSignedIn(); + + await u.page.goToRelative('/custom-pages/organization-profile?dedicatedPage=false'); + await u.po.organizationSwitcher.waitForMounted(); + await u.po.organizationSwitcher.waitForAnOrganizationToSelected(); + + // Open organization profile inside organization switcher + await u.po.organizationSwitcher.toggleTrigger(); + await u.page.waitForSelector('.cl-organizationSwitcherPopoverCard', { state: 'visible' }); + await u.page.locator('.cl-button__manageOrganization').click(); + + // Check if custom pages and links are visible + await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible(); + await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible(); + + // Navigate to custom page + await u.page.getByRole('button', { name: /Terms/i }).click(); + await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible(); + + // Check reordered default label. Members tab is now the last item. + await u.page.locator('.cl-navbarButton').last().click(); + await expect(u.page.getByRole('heading', { name: 'Members' })).toBeVisible(); + + // Click custom link and check navigation + await u.page.getByRole('button', { name: /Homepage/i }).click(); + await u.page.waitForAppUrl('/'); + }); + // ---- react/protect test('only admin react', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts index dd6725c1642..bd41eafb34f 100644 --- a/packages/astro/src/astro-components/index.ts +++ b/packages/astro/src/astro-components/index.ts @@ -20,7 +20,7 @@ export { default as SignIn } from './interactive/SignIn.astro'; export { default as SignUp } from './interactive/SignUp.astro'; export { UserButton } from './interactive/UserButton'; export { UserProfile } from './interactive/UserProfile'; -export { default as OrganizationProfile } from './interactive/OrganizationProfile.astro'; -export { default as OrganizationSwitcher } from './interactive/OrganizationSwitcher.astro'; +export { OrganizationProfile } from './interactive/OrganizationProfile'; +export { OrganizationSwitcher } from './interactive/OrganizationSwitcher'; export { default as OrganizationList } from './interactive/OrganizationList.astro'; export { default as GoogleOneTap } from './interactive/GoogleOneTap.astro'; diff --git a/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro b/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro new file mode 100644 index 00000000000..a35b39c0564 --- /dev/null +++ b/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro @@ -0,0 +1,76 @@ +--- +interface Props { + url: string + label: string + type: 'page' | 'link' + component: 'organization-profile' | 'user-profile' | 'organization-switcher' + reorderItemsLabels?: Readonly> +} + +const { url, label, type, component, reorderItemsLabels = [] } = Astro.props + +let labelIcon = ''; +let content = '' + +if (Astro.slots.has('label-icon')) { + labelIcon = await Astro.slots.render('label-icon'); +} + +if (Astro.slots.has('default') && type === 'page') { + content = await Astro.slots.render('default'); +} +--- + + diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile.astro deleted file mode 100644 index b504c003180..00000000000 --- a/packages/astro/src/astro-components/interactive/OrganizationProfile.astro +++ /dev/null @@ -1,8 +0,0 @@ ---- -import type { OrganizationProfileProps } from "@clerk/types"; -type Props = OrganizationProfileProps - -import InternalUIComponentRenderer from './InternalUIComponentRenderer.astro' ---- - - diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro new file mode 100644 index 00000000000..759c9584ca9 --- /dev/null +++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro @@ -0,0 +1,10 @@ +--- +import type { OrganizationProfileProps, Without } from '@clerk/types' + +type Props = Without + +import InternalUIComponentRenderer from '../InternalUIComponentRenderer.astro' +--- + + + diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro new file mode 100644 index 00000000000..3f1280adcff --- /dev/null +++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro @@ -0,0 +1,14 @@ +--- +import CustomProfilePageRenderer from '../CustomProfilePageRenderer.astro' + +interface Props { + url: string + label: string +} + +const { url, label } = Astro.props +--- + + + + diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro new file mode 100644 index 00000000000..8b919a6aa35 --- /dev/null +++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro @@ -0,0 +1,24 @@ +--- +import CustomProfilePageRenderer from '../CustomProfilePageRenderer.astro' + +const reorderItemsLabels = ['general', 'members'] as const; +type ReorderItemsLabels = typeof reorderItemsLabels[number]; + +type Props