Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/four-beans-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/vue": patch
---

Add support for custom pages and links
20 changes: 20 additions & 0 deletions integration/templates/vue-vite/src/components/CustomUserButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ const isActionClicked = ref(false);
<div>Icon</div>
</template>
</UserButton.Link>
<UserButton.Action
label="Custom page"
open="terms"
>
<template #labelIcon>
<div>Icon</div>
</template>
</UserButton.Action>
<UserButton.Action
label="Custom action"
@click="isActionClicked = true"
Expand All @@ -27,6 +35,18 @@ const isActionClicked = ref(false);
</template>
</UserButton.Action>
</UserButton.MenuItems>
<UserButton.UserProfilePage
label="Terms"
url="terms"
>
<template #labelIcon>
<div>Icon</div>
</template>
<div>
<h1>Custom Terms Page</h1>
<p>This is the custom terms page</p>
</div>
</UserButton.UserProfilePage>
</UserButton>
<div>Is action clicked: {{ isActionClicked }}</div>
</template>
13 changes: 12 additions & 1 deletion integration/templates/vue-vite/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ const routes = [
path: '/unstyled',
component: () => import('./views/Unstyled.vue'),
},
{
name: 'CustomUserProfile',
path: '/custom-pages/user-profile',
component: () => import('./views/custom-pages/UserProfile.vue'),
},
{
name: 'CustomOrganizationProfile',
path: '/custom-pages/organization-profile',
component: () => import('./views/custom-pages/OrganizationProfile.vue'),
},
];

const router = createRouter({
Expand All @@ -35,14 +45,15 @@ const router = createRouter({

router.beforeEach(async (to, _, next) => {
const { isSignedIn, isLoaded } = useAuth();
const authenticatedPages = ['Profile', 'Admin', 'CustomUserProfile', 'CustomOrganizationProfile'];

if (!isLoaded.value) {
await waitForClerkJsLoaded(isLoaded);
}

if (isSignedIn.value && to.name === 'Sign in') {
next('/profile');
} else if (!isSignedIn.value && ['Profile', 'Admin'].includes(to.name as string)) {
} else if (!isSignedIn.value && authenticatedPages.includes(to.name)) {
next('/sign-in');
} else {
next();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { OrganizationProfile } from '@clerk/vue';
</script>

<template>
<OrganizationProfile>
<OrganizationProfile.Page
label="Terms"
url="terms"
>
<template #labelIcon>
<div>Icon</div>
</template>
<div>
<h1>Custom Terms Page</h1>
<p>This is the custom terms page</p>
</div>
</OrganizationProfile.Page>
<OrganizationProfile.Link
label="Homepage"
url="/"
>
<template #labelIcon>
<div>Icon</div>
</template>
</OrganizationProfile.Link>
<OrganizationProfile.Page label="general" />
</OrganizationProfile>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script setup lang="ts">
import { UserProfile } from '@clerk/vue';
</script>

<template>
<UserProfile>
<UserProfile.Page
label="Terms"
url="terms"
>
<template #labelIcon>
<div>Icon</div>
</template>
<div>
<h1>Custom Terms Page</h1>
<p>This is the custom terms page</p>
</div>
</UserProfile.Page>
<UserProfile.Link
label="Homepage"
url="/"
>
<template #labelIcon>
<div>Icon</div>
</template>
</UserProfile.Link>
<UserProfile.Page label="security" />
</UserProfile>
</template>
67 changes: 66 additions & 1 deletion integration/tests/vue/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te
await u.po.userButton.waitForPopover();

// Check if custom menu items are visible
await u.po.userButton.toHaveVisibleMenuItems([/Custom link/i, /Custom action/i]);
await u.po.userButton.toHaveVisibleMenuItems([/Custom link/i, /Custom page/i, /Custom action/i]);

// Click custom action
await u.page.getByRole('menuitem', { name: /Custom action/i }).click();
Expand All @@ -76,6 +76,16 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te
await u.po.userButton.toggleTrigger();
await u.po.userButton.waitForPopover();

// Click custom action and check for custom page availbility
await u.page.getByRole('menuitem', { name: /Custom page/i }).click();
await u.po.userProfile.waitForUserProfileModal();
await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible();

// Close the modal and trigger the popover again
await u.page.locator('.cl-modalCloseButton').click();
await u.po.userButton.toggleTrigger();
await u.po.userButton.waitForPopover();

// Click custom link and check navigation
await u.page.getByRole('menuitem', { name: /Custom link/i }).click();
await u.page.waitForAppUrl('/profile');
Expand Down Expand Up @@ -111,6 +121,61 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te
await expect(u.page.getByText(`Hello, ${fakeUser.firstName}`)).toBeVisible();
});

test('render user profile with custom pages and links', 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: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.page.goToRelative('/custom-pages/user-profile');
await u.po.userProfile.waitForMounted();

// 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. Security tab is now the last item.
await u.page.locator('.cl-navbarButton').last().click();
await expect(u.page.getByRole('heading', { name: 'Security' })).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', 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: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();

await u.page.goToRelative('/custom-pages/organization-profile');
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('redirects to sign-in when unauthenticated', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/profile');
Expand Down
Loading
Loading