-
Notifications
You must be signed in to change notification settings - Fork 2k
Dashboard v2: implement breadcrumbs #103294
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Jetpack Cloud live (direct link)
Automattic for Agencies live (direct link)
|
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser: App Entrypoints (~14220 bytes removed 📉 [gzipped])
Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used. Sections (~3475 bytes added 📈 [gzipped])
Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to. Async-loaded Components (~1565 bytes added 📈 [gzipped])
React components that are loaded lazily, when a certain part of UI is displayed for the first time. Legend What is parsed and gzip size?Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory. Generated by performance advisor bot at iscalypsofastyet.com. |
This PR modifies the release build for the following Calypso Apps: For info about this notification, see here: PCYsg-OT6-p2
To test WordPress.com changes, run |
client/dashboard/app/router.tsx
Outdated
loader: async ( { params: { siteSlug } } ) => { | ||
await queryClient.ensureQueryData( siteSettingsQuery( siteSlug ) ); | ||
return { | ||
breadcrumbItemLabel: __( 'Settings' ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this have to be in the loader? Can't it be added as route meta or something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It needs to call __()
, so it can't be a static data. Also, I imagine other screens might have dynamic breadcrumb item label such as plugin name in the Plugins dashboard (it's the case now in v1 if you go to https://wordpress.com/plugins)
BTW as per docs, meta
has been replaced with https://tanstack.com/router/latest/docs/framework/react/guide/static-route-data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my conclusion after spending a day reading the docs and consulting with AI 🥲 but I'm open if you have better ideas! This is literally my first project with TanStack stack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__()
can be called anywhere, it doesn't have to be called at render time.
But if you really want, you could create a function that maps a route to a label:
function getRouteBreadcrumbLabel( route ) {
switch ( route.path ) {
case 'setting': return __( 'Settings' );
}
}
The loader function doesn't seem meant for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The design is not available yet in v2 actually. I just wanted to prepare for this case in case we want to bring this pattern to v2 😬
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think yeah we can try to put that in staticData
(formerly meta
) first and try to figure out the dynamic case when it's actually needed in the design...
__() can be called anywhere, it doesn't have to be called at render time.
I was afraid the translation might not be ready yet when we construct the routing, but I'm happy to be wrong. I can try.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, imo we shouldn't think about use cases that might not exist. If for now a simple link with a slash works, then imo that's good for now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was afraid the translation might not be ready yet when we construct the routing, but I'm happy to be wrong. I can try.
For @wordpress/i18n
in GB, I don't think it matters, I'm not sure about Calypso. But if you want to be sure it could be a function that maps it instead that is called during render
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this have to be in the loader? Can't it be added as route meta or something?
Thanks for the feedback! I ended up putting the label inside staticData
instead, with a small caveat:
__()
can be called anywhere, it doesn't have to be called at render time.
It doesn't work with the approach in:
as the translation is not ready yet when the routing is constructed, so I made it a function instead, e.g.:
const siteSettingsRoute = createRoute( {
...
staticData: {
breadcrumbItemLabel: () => __( 'Settings' ),
},
'!@automattic/components/src/core-badge', | ||
'!@automattic/components/src/breadcrumbs', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clicking the breadcrumb items will trigger a page reload. This is because it's a regular link, not a router link. I don't have any good idea yet how to solve it. Need help from @Automattic/io.
How difficult is it to just create the breadcrumb links ourselves? In this case, it looks like there will only ever be a single breadcrumb link, looking at the designs, so this doesn't seem that complex to add a single {link} /
above the header?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I honestly initially thought about that... but then why are we creating a full-blown <Breadcrumbs />
DS component if it's never going to be used beyond the above simple case? That thinking made me end up trying to come up with a more general breadcrumbs system 🥲
Even if we have to hardcode the router link, we will need to copy-paste the styles from the <Breadcrumbs />
component. Not sure if that's a good idea design-wise 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if we're only ever going to add one link then we can remove that component eventually. I don't think we should necessarily import styles from Calypso components, we should focus on using @wordpress
styles with limited customisation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this is not what I anticipated; I thought the idea is to make use of the reusable DS components. I'll see what I can do next week; it's getting late for me 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or I can try to pass something to the link's onClick
props and see if I can properly call TanStack router navigation with it.
<a href={ href } onClick={ onClick } className="a8c-components-breadcrumbs__item"> |
I'd love to get input from @ntsekouras as the component author 🙏
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or I can try to pass something to the link's onClick props and see if I can properly call TanStack router navigation with it.
For start that what I'd try to do personally, just for now. Or it might had to do with the path (relative/full, etc..).
Breadcrumbs
will be part of the PageHeader component, that will land soon. My plan was to implement what this PR does, after the merge of PageHeader and try to see if we can make it work with a
tags, but if it can't, we'll add support in the Breadcrumbs component to pass the element we want instead of a
( in this case Link
). This was mentioned in the Breadcrumbs PR already.
So, for me the important part here is to create the functionality to retrieve the proper breadcrumb items and for now we could just use directly a Link and not the Breadcrumbs if not possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I opened a PR for this: #103321
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see if I can properly call TanStack router navigation with it.
If you do want to shortcut #103321 you can implement the onClick
handler like
export default function Breadcrumbs() {
const matches = useMatches();
const navigate = useNavigate();
const items = [] as BreadcrumbItemProps[];
let hasParent = false;
matches.forEach( ( match ) => {
if ( match.loaderData?.breadcrumbItemLabel || hasParent ) {
items.push( {
label: match.loaderData?.breadcrumbItemLabel || '',
href: match.pathname,
onClick: ( e ) => {
e.preventDefault();
navigate( { to: match.pathname } );
},
} );
hasParent = true;
}
} );
return <BreadcrumbsComponent items={ items } />;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@p-jackson The downside of this approach is that it doesn't support "preloading". See #103283 and same issue also being solved for DataViews #103328
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks all. I understand that we're discussing how we will support route links for the breadcrumbs in the linked PR above. To keep progressing, can we then move forward with the existing Breadcrumbs component behavior in this PR, and then we can follow-up later when we have decided how to support route links? At least, in this PR, we managed to collect the breadcrumbs and show them in the appropriate location.
07aea7f
to
008ea30
Compare
let hasParent = false; | ||
matches.forEach( ( match ) => { | ||
const breadcrumbItemLabel = match.staticData?.breadcrumbItemLabel; | ||
if ( breadcrumbItemLabel || hasParent ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd expect extracting the breadcrumbs based on how we register the routes (route.addChildren). Isn't that possible? It seems weird to have a hasParent
flag based on breadcrumbItemLabel
existence.
const breadcrumbItemLabel = match.staticData?.breadcrumbItemLabel; | ||
if ( breadcrumbItemLabel || hasParent ) { | ||
items.push( { | ||
label: breadcrumbItemLabel?.() || '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It wouldn't make any sense to have an empty label, so we could move this check before pushing the item.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a "hack" because the individual settings subpage needs to be in the breadcrumb items; even if it's not shown (because it's the last item).
Let me think on how to make it cleaner...
@@ -109,6 +110,9 @@ const siteSettingsRoute = createRoute( { | |||
path: 'settings', | |||
loader: ( { params: { siteSlug } } ) => | |||
queryClient.ensureQueryData( siteSettingsQuery( siteSlug ) ), | |||
staticData: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have the full picture yet regarding the proper implementation, but here we decouple the route breadcrumb label with the label we use for the menus in the app. Would it be possible and/or preferable to have a single label in the route and extract this in the menus?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already the case right now, we're duplicating the page title (e.g. Overview
, Deployments
) in the menu and in the individual component for that menu. Let me think how to best address this...
Hello hello @youknowriad @ntsekouras @ellatrix, I am a bit stuck here, wanted to get your guidance. Here is my brain dump... When I first implemented this PR, I was not aware of @ntsekouras's PageHeader PR. Looking at the PR, it seems that the idea is render the breadcrumbs inside the page header. Looking at the proposed Figma designs, it looks like for now, we only show the breadcrumbs in the Settings tab from the "Sites" dashboard. @ellatrix raised a good point above: if that is the only place, can we just statically render that without any logic? That translates to roughly something like this: export function SubscriptionGiftingSettings() {
return (
<PageLayout ...>
<PageHeader
breadcrumbs={[
{ label: __('Settings'), href:`/sites/{$siteSlug}/settings`},
{ label: '', href: '#'}
]}
title={__('Accept subscription gifting')} />
...
);
} , which means we're duplicating the breadcrumbs in EVERY subpages of the Settings tabs. Maybe that works for now. If we're not comfortable with that, we can take one step further and extract the breadcrumbs logic centrally, as hooks / helpers. So, something like this... export function useSettingsBreadcrumbs() {
return [
{ label: __('Settings'), href:`/sites/{$siteSlug}/settings`},
{ label: '', href: '#'}
];
}
...
export function SubscriptionGiftingSettings() {
const breadcrumbs = useSettingsBreadcrumbs();
return (
<PageLayout ...>
<PageHeader
breadcrumbs={breadcrumbs}
title={__('Accept subscription gifting')} />
...
);
} but that means each and every page need to decide whether to show breadcrumbs. In our case, the problem is the same: each subpage of the Settings tab needs to remember to pass that breadcrumbs. Another alternative is, which is the one proposed in this PR, is to render the breadcrumbs component at the "layout" level; in this case, the const siteSettingsRoute = createRoute( {
...,
staticData: {
breadcrumbItemLabel: () => __( 'Settings' ),
},
} then, we create a component called Breadcrumbs that traverse the current route matches, and show the breadcrumbs starting the route that declares the label: export default function Breadcrumbs() {
const matches = useMatches();
// construct the breadcrumb items based on match(es) that declares the label
// and then return the Breadcrumbs component
} then we can simply show it from export function PageLayout() {
...
return (
<>
<Breadcrumbs />
{children}
</>
);
} However, it needs to take @ntsekouras 's PR into account. This means rendering the breadcrumbs inside that That's the gist of this PR! This PR also gets a good feedback from @ntsekouras above about how to share the breadcrumb string between the page title, the breadcrumb, and the nav (menu), which is something I haven't solved yet. Need help on this -- or maybe we don't need to solve it now? This has been a long comment, but my point is: there's a virtually infinite way to do this. I'd appreciate a suggestion -- which direction do you think should we take to move us forward with the dashboard MVP? I didn't anticipate spending too much time on this PR 😄 |
Tbh, I don't think that's a big deal for now, it's fine to duplicate it a few times? I also agree with building breadcrumbs into the header. This will be something we'll have to provide view transitions for eventually. Currently navigation link labels are also not tied to the routes. Should they? I think it would make more sense to attach it to the route if it were the same thing for navigation links for example. Meaning: I'm not sure right now if it should, and maybe some duplication is fine for now and we can revisit that later. |
if ( ! isExactMatch ) { | ||
return <Outlet />; | ||
} | ||
|
||
return ( | ||
<PageLayout title={ __( 'Settings' ) } size="small"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be the same label as the breadcrumb? Could it be more generally the route label?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually yes. @ntsekouras also had a similar comment in that it should also match with the nav menus (
wp-calypso/client/dashboard/sites/site-menu/index.tsx
Lines 7 to 18 in c320da3
<ResponsiveMenu.Item to={ `/sites/${ siteSlug }` } activeOptions={ { exact: true } }> | |
{ __( 'Overview' ) } | |
</ResponsiveMenu.Item> | |
<ResponsiveMenu.Item to={ `/sites/${ siteSlug }/deployments` }> | |
{ __( 'Deployments' ) } | |
</ResponsiveMenu.Item> | |
<ResponsiveMenu.Item to={ `/sites/${ siteSlug }/performance` }> | |
{ __( 'Performance' ) } | |
</ResponsiveMenu.Item> | |
<ResponsiveMenu.Item to={ `/sites/${ siteSlug }/settings` }> | |
{ __( 'Settings' ) } | |
</ResponsiveMenu.Item> |
If we want to use route label, given we want to put the labels in the router level as proposed in this PR, this will then become something like
<PageLayout title={siteSettingsRoute.options.staticData.breadcrumbItemLabel()} />
but I'm not sure if it's a good pattern in TanStack. Another way is maybe write some hook called usePageTitle()
then get the label of the last matching route. But then I wonder if this logic should be in <PageLayout />
directly instead.
In any case, it's a good follow-up thing to do, whatever approach we take!
if ( ! siteData || ! settings ) { | ||
return null; | ||
} | ||
|
||
const isExactMatch = match.id === matches[ matches.length - 1 ].id; | ||
if ( ! isExactMatch ) { | ||
return <Outlet />; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why <Outlet />
is needed here. It doesn't seem like the right place especially after already calling useQuery
. Why is it needed here, but not for ${siteSlug}
(Overview) and {$siteSlug}/sub-page
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want the subpage routes (in this case siteSettingsSubscriptionGiftingRoute
) to be a child of siteSettingsRoute
for some reasons:
- so that they share the parent route prefix (i.e.
settings/
) - so that they share the parent loader logic (i.e. preloading
siteSettingsQuery()
) - (especially for this PR) so that the parent route MATCHES so its route label will be collected properly to construct the breadcrumbs. In other words, we want
/sites/${siteSlug}/settings
to be always matched in the subpages.
So, if it's an exact match, we want to render the Settings page, otherwise, we should render <Outlet />
so that it renders the child component instead (renders the settings subpages).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it needed here, but not for ${siteSlug} (Overview) and {$siteSlug}/sub-page?
siteRoute
is already a parent of its children routes. It's already always shown in the subroutes. It renders as the site navigation menu (Overview, Deployments, etc).
On the other hands, we cant really compose Settings and its subpages. In /sites/${slug}/settings
we render the list of setting summaries; in the child pages we render the settings of the subpages only. Hence the conditionals.
So my understanding here is that this PR is trying to build the breadcrumb based on the route hierarchy, and in doing so, it's forcing a special set of parent routes... and enhancing the route definition to be able to do so. I'm not convinced that breadcrumbs generation/rendering should be tied to the route hierarchy. I understand the appeal but this largely feels to me as early abstraction. I'd be in favor of just rendering the
|
Cool, thanks all for the feedback. I believe, the consensus is to build the simplest thing possible without early abstraction. I also understand that there might be some future changes to the breadcrumbs based on puPv3-pw6-p2. I'll raise another PR instead and close this one for posterity. Thanks again! |
It's fine to consider this an earyl abstraction, but @youknowriad I'm curious why you think that way for the first part. Isn't the breadcrumbs all about route hierarchy? Currently you can just pass the breadcrumb items independently of course, but in the context of the dashboard IMO these should be tied with an extra wrapper or hook that would do it based on routes. This is also related to the comments about |
You are right, but by making it an abstraction now, you're creating a lot of constraints:
These are also for me small things and I'm not sure abstracting is important. Some titles might require extra icons or things like that. When you start abstracting these things, you're forced to over abstract everything. I'm not saying these abstractions are necessarily bad, I'm saying that we're too early design wise to introduce them, especially since the alternative is so simple, just render a reusable component. |
Part of DOTCOM-13067
Proposed Changes
This PR implements breadcrumbs and show it on the child pages of Settings as per design (p1746709722501659/1746709518.587029-slack-C08JZTRS6NA)
Implementation details
breadcrumbItemLabel
prop to the route's context. Any route can append this prop by returning this in itsloader()
.-siteSettingsSubscriptionGiftingRoute
(and future setting routes) as a child ofsiteSettingsRoute
.siteSettingsRoute
.<PageLayout />
.Open discussions
<PageLayout />
toapp/
notcomponents/
? Because it is not a "pure" component anymore; it contains logic (breadcrumbs).Testing instructions
/v2/sites
Pre-merge Checklist