Repository navigation
Premium Analytics: link Most viewed post titles through the router #50891
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
Merged
Merged
Changes from 3 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
b917488
Premium Analytics: link Most viewed post titles through the router
Nikschavan d175f09
Premium Analytics: move View post to the post detail page header
Nikschavan 94e4aa4
Premium Analytics: fix View post button semantics and cover post detail
Nikschavan 699be58
Premium Analytics: preserve links for non-REST posts
Nikschavan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
4 changes: 4 additions & 0 deletions
4
projects/packages/premium-analytics/changelog/fix-top-posts-router-navigation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Significance: patch | ||
| Type: fixed | ||
|
|
||
| Most viewed: navigate to post details in the app instead of reloading the page. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
97 changes: 97 additions & 0 deletions
97
...ackages/widgets-toolkit/src/components/post-title-link/__tests__/post-title-link.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { render, screen } from '@testing-library/react'; | ||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { PostTitleLink } from '../post-title-link'; | ||
| import type { AnchorHTMLAttributes, ReactNode } from 'react'; | ||
|
|
||
| type MockRouteLinkProps = { | ||
| to: string; | ||
| params?: Record< string, unknown >; | ||
| search?: Record< string, unknown >; | ||
| children: ReactNode; | ||
| } & Omit< AnchorHTMLAttributes< HTMLAnchorElement >, 'href' >; | ||
|
|
||
| jest.mock( '@wordpress/route', () => ( { | ||
| Link: ( { to, params, search, children, ...props }: MockRouteLinkProps ) => { | ||
| const path = Object.entries( params ?? {} ).reduce( | ||
| ( result, [ key, value ] ) => result.replace( `$${ key }`, String( value ) ), | ||
| to | ||
| ); | ||
| const query = new URLSearchParams(); | ||
| Object.entries( search ?? {} ).forEach( ( [ key, value ] ) => { | ||
| if ( value !== undefined && value !== null ) { | ||
| query.set( key, String( value ) ); | ||
| } | ||
| } ); | ||
| const queryString = query.toString(); | ||
|
|
||
| return ( | ||
| <a href={ queryString ? `${ path }?${ queryString }` : path } { ...props }> | ||
| { children } | ||
| </a> | ||
| ); | ||
| }, | ||
| } ) ); | ||
|
|
||
| describe( 'PostTitleLink', () => { | ||
| it( 'routes a row with a post ID to the detail page, carrying the report window', () => { | ||
| render( | ||
| <PostTitleLink | ||
| id={ 41 } | ||
| label="Hello world" | ||
| link="https://example.com/hello-world/" | ||
| search={ { from: '2026-03-01', to: '2026-03-10' } } | ||
| /> | ||
| ); | ||
|
|
||
| const link = screen.getByRole( 'link', { name: 'Hello world' } ); | ||
| const href = link.getAttribute( 'href' ) ?? ''; | ||
| const search = new URL( href, 'https://example.com' ).searchParams; | ||
|
|
||
| expect( link ).toHaveAttribute( 'href', expect.stringContaining( '/post/41' ) ); | ||
| expect( search.get( 'from' ) ).toBe( '2026-03-01' ); | ||
| expect( search.get( 'to' ) ).toBe( '2026-03-10' ); | ||
| } ); | ||
|
|
||
| it( 'gives an internal link no outbound target and no external icon', () => { | ||
| const { container } = render( <PostTitleLink id={ 41 } label="Hello world" /> ); | ||
|
|
||
| const link = screen.getByRole( 'link', { name: 'Hello world' } ); | ||
| expect( link ).not.toHaveAttribute( 'target' ); | ||
| expect( link ).not.toHaveAttribute( 'rel' ); | ||
| // The icon marks a destination outside the app; an internal route is not one. | ||
| // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- the icon renders as an unlabelled SVG. | ||
| expect( container.querySelector( 'svg' ) ).toBeNull(); | ||
| } ); | ||
|
|
||
| it( 'falls back to the public URL with an external icon when there is no post ID', () => { | ||
| const { container } = render( | ||
| <PostTitleLink label="Pricing" link="https://example.com/?s=pricing" /> | ||
| ); | ||
|
|
||
| const link = screen.getByRole( 'link', { name: 'Open Pricing in a new tab' } ); | ||
| expect( link ).toHaveAttribute( 'href', 'https://example.com/?s=pricing' ); | ||
| expect( link ).toHaveAttribute( 'target', '_blank' ); | ||
| expect( link ).toHaveAttribute( 'rel', 'noopener noreferrer' ); | ||
| // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- the icon renders as an unlabelled SVG. | ||
| expect( container.querySelector( 'svg' ) ).not.toBeNull(); | ||
| } ); | ||
|
|
||
| it( 'treats the homepage entry (id 0) as having no detail page', () => { | ||
| render( <PostTitleLink id={ 0 } label="Homepage (Latest posts)" /> ); | ||
|
|
||
| expect( screen.queryByRole( 'link' ) ).not.toBeInTheDocument(); | ||
| expect( screen.getByText( 'Homepage (Latest posts)' ) ).toBeInTheDocument(); | ||
| } ); | ||
|
|
||
| it( 'renders plain text when the fallback URL has an unsupported scheme', () => { | ||
| render( <PostTitleLink label="Sketchy" link="javascript:alert(1)" /> ); | ||
|
|
||
| expect( screen.queryByRole( 'link' ) ).not.toBeInTheDocument(); | ||
| expect( screen.getByText( 'Sketchy' ) ).toBeInTheDocument(); | ||
| } ); | ||
| } ); |
1 change: 1 addition & 0 deletions
1
...ckages/premium-analytics/packages/widgets-toolkit/src/components/post-title-link/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { PostTitleLink, type PostTitleLinkProps } from './post-title-link'; |
122 changes: 122 additions & 0 deletions
122
...ium-analytics/packages/widgets-toolkit/src/components/post-title-link/post-title-link.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import { safeHttpUrl } from '@jetpack-premium-analytics/ui'; | ||
| import { __, sprintf } from '@wordpress/i18n'; | ||
| import { Icon, external } from '@wordpress/icons'; | ||
| import { Link } from '@wordpress/route'; | ||
|
|
||
| export type PostTitleLinkProps = { | ||
| /** | ||
| * Post or page ID. Rows carrying one link to the internal detail route. | ||
| */ | ||
| id?: number | string; | ||
|
|
||
| /** | ||
| * Visible row title. | ||
| */ | ||
| label: string; | ||
|
|
||
| /** | ||
| * Public URL of the content. Used only when there is no post ID. | ||
| */ | ||
| link?: string | null; | ||
|
|
||
| /** | ||
| * Search parameters for the detail route. Pass the shared report window from | ||
| * `pickReportDateParams()` so the detail page opens on the same date range. | ||
| */ | ||
| search?: Record< string, unknown >; | ||
|
|
||
| /** | ||
| * Optional classes for each rendering branch. `text` applies to the label | ||
| * span in every branch, so one rule can ellipsize the title everywhere. | ||
| */ | ||
| classNames?: { | ||
| internal?: string; | ||
| external?: string; | ||
| icon?: string; | ||
| plain?: string; | ||
| text?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Optional native title attribute, for the full text on hover. | ||
| */ | ||
| title?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Render a post or page title as an internal detail link, an external fallback | ||
| * link, or plain text. | ||
| * | ||
| * Rows with a post ID stay inside the app: they navigate to `/post/{postId}` | ||
| * through the router, so the dashboard does not reload. They carry no | ||
| * external-link icon — the icon marks a destination outside the app, and the | ||
| * detail page holds the link out to the live post. Rows without a post ID have | ||
| * no detail page, so the public URL becomes the fallback and takes the icon. | ||
| * | ||
| * @param props - Component props. | ||
| * @param props.id - Post or page ID. | ||
| * @param props.label - Visible row title. | ||
| * @param props.link - Public URL, used only without a post ID. | ||
| * @param props.search - Search parameters for the detail route. | ||
| * @param props.classNames - Optional classes for each rendering branch. | ||
| * @param props.title - Optional native title attribute. | ||
| * @return The linked or plain post title. | ||
| */ | ||
| export function PostTitleLink( { | ||
| id, | ||
| label, | ||
| link, | ||
| search, | ||
| classNames, | ||
| title, | ||
| }: PostTitleLinkProps ): JSX.Element { | ||
| const postId = Number( id ); | ||
| const text = <span className={ classNames?.text }>{ label }</span>; | ||
|
|
||
| if ( Number.isInteger( postId ) && postId > 0 ) { | ||
| return ( | ||
| <Link | ||
| className={ classNames?.internal } | ||
| to="/post/$postId" | ||
| params={ { postId: String( postId ) } as unknown as never } | ||
| search={ search as unknown as never } | ||
| title={ title } | ||
| > | ||
| { text } | ||
| </Link> | ||
| ); | ||
| } | ||
|
|
||
| // Callers pass `link` straight from report data, so the scheme is guarded | ||
| // here at the sink rather than in each consuming widget or route. | ||
| const href = safeHttpUrl( link ); | ||
|
|
||
| if ( href ) { | ||
| return ( | ||
| <a | ||
| className={ classNames?.external } | ||
| href={ href } | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| title={ title } | ||
| aria-label={ sprintf( | ||
| /* translators: %s is a post, page, or archive page title. */ | ||
| __( 'Open %s in a new tab', 'jetpack-premium-analytics-pkg' ), | ||
| label | ||
| ) } | ||
| > | ||
| { text } | ||
| <Icon className={ classNames?.icon } icon={ external } size={ 16 } /> | ||
| </a> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <span className={ classNames?.plain } title={ title }> | ||
| { text } | ||
| </span> | ||
| ); | ||
| } | ||
1 change: 0 additions & 1 deletion
1
projects/packages/premium-analytics/packages/widgets-toolkit/src/hooks/index.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| export { useAttributesWithSearchFallback } from './use-attributes-with-search-fallback'; | ||
| export { useChartTheme, type WooChartTheme } from './use-chart-theme'; | ||
| export { useElementSize, type ElementSize } from './use-element-size'; | ||
| export { usePostDetailHrefBuilder } from './use-post-detail-href'; | ||
| export { useSegmentStyles } from '../widgets/common'; | ||
| export { useSeriesStyles } from './use-series-styles'; | ||
| export { useWidgetDrillDown } from './use-widget-drill-down'; |
54 changes: 0 additions & 54 deletions
54
...cts/packages/premium-analytics/packages/widgets-toolkit/src/hooks/use-post-detail-href.ts
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
BTW just a nit but you don't need to add these in modern browsers anymore. Link and ExternalLink components don't add them anymore either in Gutenberg.