Custom Pages for <UserProfile /> and <OrganizationProfile /> components#1822
Custom Pages for <UserProfile /> and <OrganizationProfile /> components#1822
<UserProfile /> and <OrganizationProfile /> components#1822Conversation
🦋 Changeset detectedLatest commit: 3d8b86a The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
4f147e1 to
19a6314
Compare
|
!snapshot |
|
Hey @anagstef - the snapshot version command generated the following package versions:
Tip: use the snippet copy button below to quickly install the required packages. # @clerk/backend
npm i @clerk/backend@0.30.2-snapshot.875725a# @clerk/chrome-extension
npm i @clerk/chrome-extension@0.4.5-snapshot.875725a# @clerk/clerk-js
npm i @clerk/clerk-js@4.60.0-snapshot.875725a# @clerk/clerk-expo
npm i @clerk/clerk-expo@0.19.7-snapshot.875725a# @clerk/fastify
npm i @clerk/fastify@0.6.12-snapshot.875725a# gatsby-plugin-clerk
npm i gatsby-plugin-clerk@4.4.13-snapshot.875725a# @clerk/localizations
npm i @clerk/localizations@1.26.3-snapshot.875725a# @clerk/nextjs
npm i @clerk/nextjs@4.25.2-snapshot.875725a# @clerk/clerk-react
npm i @clerk/clerk-react@4.27.0-snapshot.875725a# @clerk/remix
npm i @clerk/remix@3.0.4-snapshot.875725a# @clerk/clerk-sdk-node
npm i @clerk/clerk-sdk-node@4.12.11-snapshot.875725a# @clerk/shared
npm i @clerk/shared@0.24.2-snapshot.875725a# @clerk/themes
npm i @clerk/themes@1.7.7-snapshot.875725a# @clerk/types
npm i @clerk/types@3.54.0-snapshot.875725a |
packages/react/src/types.ts
Outdated
| export type UserProfilePageProps = { | ||
| url?: string; | ||
| label: string; | ||
| labelIcon?: React.ReactElement; |
There was a problem hiding this comment.
❓ Couldn't this be ReactNode ?
brkalow
left a comment
There was a problem hiding this comment.
Looking good! Nice test cases.
| <Route | ||
| index={!pages.isAccountPageRoot && index === 0} | ||
| path={!pages.isAccountPageRoot && index === 0 ? undefined : customPage.url} | ||
| key={`custom-page-${index}`} |
There was a problem hiding this comment.
💭 I don't expect the custom pages list to change really, but using url in the key might be better here.
| throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.'); | ||
| } | ||
|
|
||
| const pages = useMemo(() => createCustomPages(customPages || []), [customPages]); |
There was a problem hiding this comment.
❓ Is customPages memoized upstream from this? Otherwise, this useMemo() call might be getting recomputed on every call to this hook.
There was a problem hiding this comment.
From what I can tell, there isn't anything that guarantees referential equality of customPages, so this might run more than we intend.
There was a problem hiding this comment.
You are right! We cannot hook on the customPages changes, because we can't really be 100% sure when custom pages contents change. So, I may just remove the useMemo here.
| export const NavBar = (props: NavBarProps) => { | ||
| const { contentRef, routes, header } = props; | ||
| const [activeId, setActiveId] = React.useState<RouteId>(routes[0]['id']); | ||
| const [activeId, setActiveId] = React.useState<RouteId>(''); |
There was a problem hiding this comment.
❓ Why are we removing this default value?
There was a problem hiding this comment.
This was causing a flicker when accessing directly a subpath in the UserProfile. In the first render, it always highlighted the first item in the navbar and then changed to the actual active. Now, it highlights nothing for half a second and then the correct item, which looks smoother in the eye.
| <> | ||
| <div ref={nodeRef}></div> | ||
| </> |
There was a problem hiding this comment.
| <> | |
| <div ref={nodeRef}></div> | |
| </> | |
| <div ref={nodeRef} /> |
| if (isDevelopmentEnvironment()) { | ||
| checkForDuplicateUsageOfReorderingItems(customPages); | ||
| } |
There was a problem hiding this comment.
love the helpful error checks here 👏
| }; | ||
|
|
||
| const checkForDuplicateUsageOfReorderingItems = (customPages: CustomPage[]) => { | ||
| const reorderItems = customPages.filter(cp => isAccountReorderItem(cp) || isSecurityReorderItem(cp)); |
There was a problem hiding this comment.
Just thinking for the future, if we ever add another internal page that can be reordered, we'll have a few places that need updating. Is there a way we can consolidate that logic?
There was a problem hiding this comment.
I have refactored this part. Thanks for pointing out.
| const sanitizeCustomPageURL = (url: string): string => { | ||
| if (!url) { | ||
| throw new Error('URL is required for custom pages'); | ||
| } | ||
| if (isValidUrl(url)) { | ||
| throw new Error('Absolute URLs are not supported for custom pages'); | ||
| } | ||
| return (url as string).charAt(0) === '/' && (url as string).length > 1 ? (url as string).substring(1) : url; | ||
| }; | ||
|
|
||
| const sanitizeCustomLinkURL = (url: string): string => { | ||
| if (!url) { | ||
| throw new Error('URL is required for custom links'); | ||
| } | ||
| if (isValidUrl(url)) { | ||
| return url; | ||
| } | ||
| return (url as string).charAt(0) === '/' ? url : `/${url}`; | ||
| }; |
There was a problem hiding this comment.
❓ Why are we removing the leading / in one of these methods and prepending it in another?
There was a problem hiding this comment.
We want the Custom Link items (external links) to have a leading / so that navigate works correctly, and on the other hand Custom Page items need a relative segment, to navigate inside the UserProfile/OrganizationProfile routing mechanism.
| export function UserProfilePage({ children }: PropsWithChildren<UserProfilePageProps>) { | ||
| if (isDevelopmentEnvironment()) { | ||
| console.error(userProfilePageRenderedError); | ||
| } | ||
| return <div>{children}</div>; | ||
| } | ||
|
|
||
| export function UserProfileLink({ children }: PropsWithChildren<UserProfileLinkProps>) { | ||
| if (isDevelopmentEnvironment()) { | ||
| console.error(userProfileLinkRenderedError); | ||
| } | ||
| return <div>{children}</div>; | ||
| } |
There was a problem hiding this comment.
Should we not export these instead of logging an error when they are used directly?
There was a problem hiding this comment.
We don't export them from our package. We do this so we can import it in the useCustomPages hook, and check if this is the component used as a child inside the UserProfile
| Page: ({ children }: PropsWithChildren<UserProfilePageProps>) => React.JSX.Element; | ||
| Link: ({ children }: PropsWithChildren<UserProfileLinkProps>) => React.JSX.Element; |
There was a problem hiding this comment.
Does this work?
| Page: ({ children }: PropsWithChildren<UserProfilePageProps>) => React.JSX.Element; | |
| Link: ({ children }: PropsWithChildren<UserProfileLinkProps>) => React.JSX.Element; | |
| Page: typeof UserProfilePage; | |
| Link: typeof UserProfileLink; |
There was a problem hiding this comment.
It might also be helpful to place these types at the top of the file, instead of interleaved with the source.
| if (isDevelopmentEnvironment()) { | ||
| console.error(userProfilePageRenderedError); | ||
| } |
There was a problem hiding this comment.
Your errorInDevMode helper could be used here.
|
!snapshot |
|
Hey @anagstef - the snapshot version command generated the following package versions:
Tip: use the snippet copy button below to quickly install the required packages. # @clerk/backend
npm i @clerk/backend@0.30.2-snapshot.3bf4d1a# @clerk/chrome-extension
npm i @clerk/chrome-extension@0.4.5-snapshot.3bf4d1a# @clerk/clerk-js
npm i @clerk/clerk-js@4.60.0-snapshot.3bf4d1a# @clerk/clerk-expo
npm i @clerk/clerk-expo@0.19.7-snapshot.3bf4d1a# @clerk/fastify
npm i @clerk/fastify@0.6.12-snapshot.3bf4d1a# gatsby-plugin-clerk
npm i gatsby-plugin-clerk@4.4.13-snapshot.3bf4d1a# @clerk/localizations
npm i @clerk/localizations@1.26.3-snapshot.3bf4d1a# @clerk/nextjs
npm i @clerk/nextjs@4.25.2-snapshot.3bf4d1a# @clerk/clerk-react
npm i @clerk/clerk-react@4.27.0-snapshot.3bf4d1a# @clerk/remix
npm i @clerk/remix@3.0.4-snapshot.3bf4d1a# @clerk/clerk-sdk-node
npm i @clerk/clerk-sdk-node@4.12.11-snapshot.3bf4d1a# @clerk/shared
npm i @clerk/shared@0.24.2-snapshot.3bf4d1a# @clerk/themes
npm i @clerk/themes@1.7.7-snapshot.3bf4d1a# @clerk/types
npm i @clerk/types@3.54.0-snapshot.3bf4d1a |
fadab47 to
09b5917
Compare
|
!snapshot |
|
Hey @anagstef - the snapshot version command generated the following package versions:
Tip: use the snippet copy button below to quickly install the required packages. # @clerk/backend
npm i @clerk/backend@0.30.3-snapshot.09b5917# @clerk/chrome-extension
npm i @clerk/chrome-extension@0.4.6-snapshot.09b5917# @clerk/clerk-js
npm i @clerk/clerk-js@4.61.0-snapshot.09b5917# @clerk/clerk-expo
npm i @clerk/clerk-expo@0.19.8-snapshot.09b5917# @clerk/fastify
npm i @clerk/fastify@0.6.13-snapshot.09b5917# gatsby-plugin-clerk
npm i gatsby-plugin-clerk@4.4.14-snapshot.09b5917# @clerk/localizations
npm i @clerk/localizations@1.26.4-snapshot.09b5917# @clerk/nextjs
npm i @clerk/nextjs@4.25.3-snapshot.09b5917# @clerk/clerk-react
npm i @clerk/clerk-react@4.27.0-snapshot.09b5917# @clerk/remix
npm i @clerk/remix@3.0.5-snapshot.09b5917# @clerk/clerk-sdk-node
npm i @clerk/clerk-sdk-node@4.12.12-snapshot.09b5917# @clerk/shared
npm i @clerk/shared@0.24.3-snapshot.09b5917# @clerk/types
npm i @clerk/types@3.55.0-snapshot.09b5917 |
|
|
||
| export const OrganizationProfileRoutes = (props: PropsOfComponent<typeof ProfileCardContent>) => { | ||
| const { pages } = useOrganizationProfileContext(); | ||
| const isMembersPageRoot = pages.routes[0].id === 'members'; |
There was a problem hiding this comment.
Any chance this array is empty?
There was a problem hiding this comment.
No chance; it always appends Clerk default routes.
There was a problem hiding this comment.
❓ is there a way to use a constant or enum instead of hard-coding members?
| path='profile' | ||
| flowStart | ||
| index={!isPredefinedPageRoot && index === 0} | ||
| path={!isPredefinedPageRoot && index === 0 ? undefined : customPage.url} |
There was a problem hiding this comment.
Can we extract this condition and give it some context with a good variable name?
| export type NavbarItemId = 'account' | 'security' | 'members' | 'settings'; | ||
|
|
There was a problem hiding this comment.
If we are interested in keeping this we could do
export type NavbarItemId = 'account' | 'security' | 'members' | 'settings' | (string & {});There was a problem hiding this comment.
Does it make sense to keep this?
|
|
||
| const portal = () => <>{nodes[index] ? createPortal(el.component, nodes[index] as Element) : null}</>; |
There was a problem hiding this comment.
🤔 Is it a common pattern to have a react hook returning JSX ?
There was a problem hiding this comment.
I think it's not that common, but we need this to be updated on each render. Do you have any suggestions on different approaches?
| type UserProfileCustomPage = { | ||
| label: string; | ||
| url: string; | ||
| mountIcon: (el: HTMLDivElement) => void; | ||
| unmountIcon: (el?: HTMLDivElement) => void; | ||
| mount: (el: HTMLDivElement) => void; | ||
| unmount: (el?: HTMLDivElement) => void; | ||
| }; |
There was a problem hiding this comment.
🙃
| type UserProfileCustomPage = { | |
| label: string; | |
| url: string; | |
| mountIcon: (el: HTMLDivElement) => void; | |
| unmountIcon: (el?: HTMLDivElement) => void; | |
| mount: (el: HTMLDivElement) => void; | |
| unmount: (el?: HTMLDivElement) => void; | |
| }; | |
| type UserProfileCustomPage = CustomPage |
There was a problem hiding this comment.
This is a different type. CustomPage is more generic for all three types of custom items (pages, links, reordering), which is why all of its properties except label are optional.
3e4efab to
198cfa1
Compare
<UserProfile /> and <OrganizationProfile /> components
|
!snapshot |
|
Hey @anagstef - the snapshot version command generated the following package versions:
Tip: use the snippet copy button below to quickly install the required packages. # @clerk/backend
npm i @clerk/backend@0.30.3-snapshot.93fdf20# @clerk/chrome-extension
npm i @clerk/chrome-extension@0.4.6-snapshot.93fdf20# @clerk/clerk-js
npm i @clerk/clerk-js@4.61.0-snapshot.93fdf20# @clerk/clerk-expo
npm i @clerk/clerk-expo@0.19.8-snapshot.93fdf20# @clerk/fastify
npm i @clerk/fastify@0.6.13-snapshot.93fdf20# gatsby-plugin-clerk
npm i gatsby-plugin-clerk@4.4.14-snapshot.93fdf20# @clerk/localizations
npm i @clerk/localizations@1.26.4-snapshot.93fdf20# @clerk/nextjs
npm i @clerk/nextjs@4.25.3-snapshot.93fdf20# @clerk/clerk-react
npm i @clerk/clerk-react@4.27.0-snapshot.93fdf20# @clerk/remix
npm i @clerk/remix@3.0.5-snapshot.93fdf20# @clerk/clerk-sdk-node
npm i @clerk/clerk-sdk-node@4.12.12-snapshot.93fdf20# @clerk/shared
npm i @clerk/shared@0.24.3-snapshot.93fdf20# @clerk/types
npm i @clerk/types@3.55.0-snapshot.93fdf20 |
93fdf20 to
593194c
Compare
6bef329 to
2053d26
Compare
…n the custom pages in dev
…ofileRoutes to be more readable
2053d26 to
3d8b86a
Compare
|
This PR has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Description
This PR introduces custom pages and links inside the
UserProfileandOrganizationProfilecomponents.UserProfile
Custom Pages
The
UserProfilecomponent supports the addition of custom pages. These custom pages can be rendered inside theUserProfilecomponent and provide a way to incorporate app-specific settings or additional functionality.To add a custom page, use the
<UserProfile.Page>component. It accepts the following props:label: The name that will be displayed in the navigation sidebar for the custom page.labelIcon: An icon displayed next to the label in the navigation sidebar.url: The path segment that will be used to navigate to the custom page. (e.g. if theUserProfilecomponent is rendered at/user, then the custom page will be accessed at/user/{url}when using path routing)children: The components to be rendered as content inside the custom page.Types:
Example usage:
Custom Links
In addition to custom pages, you can add external links to the
UserProfilenavigation sidebar using the<UserProfile.Link>component. It accepts the following props:label: The name that will be displayed in the navigation sidebar for the link.labelIcon: An icon displayed next to the label in the navigation sidebar.url: The absolute or relative url to navigate to.Types:
Example usage:
Advanced
Reordering Default Routes
If you want to reorder the default routes (
AccountandSecurity) in theUserProfilenavigation sidebar, you can use the<UserProfile.Page>component with thelabelprop set to'account'or'security'. This will target the existing default page and allow you to rearrange it.Example usage:
The above will result in the following order:
Notes
/(itsurlwill be ignored) and the Clerk pages will be rendered under the path/account.<UserProfile.Link>component.Using custom pages with the UserButton component
If you are using the
UserButtoncomponent with the default props (where theUserProfileopens as a modal), then you should also be providing these custom pages as children to the component (using the<UserButton.UserProfilePage>and<UserButton.UserProfileLink>components respectively).Example usage:
This repetition of the same property can be avoided when the user is using the
userProfileMode='navigation'anduserProfileUrl='<some url>'props on theUserButtoncomponent and has implemented a dedicated page for theUserProfilecomponent.OrganizationProfile
Custom Pages
The
OrganizationProfilecomponent supports the addition of custom pages. These custom pages can be rendered inside theOrganizationProfilecomponent and provide a way to incorporate organization-specific settings or additional functionality.To add a custom page, use the
<OrganizationProfile.Page>component. It accepts the following props:label: The name that will be displayed in the navigation sidebar for the custom page.labelIcon: An icon displayed next to the label in the navigation sidebar.url: The path segment that will be used to navigate to the custom page. (e.g. if theOrganizationProfilecomponent is rendered at/organization, then the custom page will be accessed at/organization/{url}when using path routing)children: The components to be rendered as content inside the custom page.Types:
Example usage:
Custom Links
In addition to custom pages, you can add external links to the
OrganizationProfilenavigation sidebar using the<OrganizationProfile.Link>component. It accepts the following props:label: The name that will be displayed in the navigation sidebar for the link.labelIcon: An icon displayed next to the label in the navigation sidebar.url: The absolute or relative url to navigate to.Types:
Example usage:
Advanced
Reordering Default Routes
If you want to reorder the default routes (
MembersandSettings) in theOrganizationProfilenavigation sidebar, you can use the<OrganizationProfile.Page>component with thelabelprop set to'members'or'settings'. This will target the existing default page and allow you to rearrange it.Example usage:
The above will result in the following order:
Notes
/(itsurlwill be ignored) and the Clerk pages will be rendered under the path/organitzation-membersand/organization-settings.<OrganizationProfile.Link>component.Using custom pages with the OrganizationSwitcher component
If you are using the
OrganizationSwitchercomponent with the default props (where theOrganizationProfileopens as a modal), then you should also be providing these custom pages as children to the component (using the<OrganizationSwitcher.OrganizationProfilePage>and<OrganizationSwitcher.OrganizationProfileLink>components respectively).Example usage:
This repetition of the same property can be avoided when the user is using the
organizationProfileMode='navigation'andorganizationProfileUrl='<some url>'props on theOrganizationSwitchercomponent and has implemented a dedicated page for theOrganizationProfilecomponent.Caveats
"use client";flag.Checklist
npm testruns as expected.npm run buildruns as expected.Type of change
Packages affected
@clerk/clerk-js@clerk/clerk-react@clerk/nextjs@clerk/remix@clerk/types@clerk/themes@clerk/localizations@clerk/clerk-expo@clerk/backend@clerk/clerk-sdk-node@clerk/shared@clerk/fastify@clerk/chrome-extensiongatsby-plugin-clerkbuild/tooling/chore