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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Ads: Note in the Earnings History widget when the site has adjustments, linking to the Adjustments history tab of the Earnings report.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@
text-decoration: underline;
}

// A badge or other element stands in for the text: no underline, and the
// flex context blockifies it so it stands its full height.
.hasContent {
display: inline-flex;
align-items: center;
}

.hasContent,
.hasContent:hover {
text-decoration: none;
}

// Widgets clip their own overflow and the footer sits flush against that edge,
// so the design system's outset ring is cut off on the inline-start side. Draw
// it inset instead. The block padding gives the ring room around the text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import clsx from 'clsx';
*/
import { useWidgetNavigationSearch } from '../../hooks/use-widget-navigation-search';
import styles from './report-link.module.scss';
import type { ReactNode } from 'react';

export type ReportLinkProps = {
/**
Expand All @@ -36,6 +37,11 @@ export type ReportLinkProps = {
* Optional class for widget-specific layout tweaks.
*/
className?: string;

/**
* Rendered in place of the label, e.g. a badge; the hover underline is dropped for it.
*/
children?: ReactNode;
};

/**
Expand All @@ -46,7 +52,14 @@ export type ReportLinkProps = {
*
* @return The rendered report link.
*/
export function ReportLink( { report, section, label, ariaLabel, className }: ReportLinkProps ) {
export function ReportLink( {
report,
section,
label,
ariaLabel,
className,
children,
}: ReportLinkProps ) {
const search = useWidgetNavigationSearch( { section } );

return (
Expand All @@ -58,10 +71,10 @@ export function ReportLink( { report, section, label, ariaLabel, className }: Re
search={ search as unknown as never }
/>
}
className={ clsx( styles.reportLink, className ) }
className={ clsx( styles.reportLink, children && styles.hasContent, className ) }
aria-label={ ariaLabel }
>
{ label ?? __( 'View all', 'jetpack-premium-analytics-pkg' ) }
{ children ?? label ?? __( 'View all', 'jetpack-premium-analytics-pkg' ) }
</Link>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { queryClient } from '@jetpack-premium-analytics/data';
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
Expand Down Expand Up @@ -109,6 +109,43 @@ describe( 'WordAdsEarningsHistory', () => {
);
} );

it( 'shows no adjustments line when the site has none', async () => {
render( <WordAdsEarningsHistory attributes={ {} } /> );
await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument();

expect(
screen.queryByRole( 'link', { name: /view adjustments history/ } )
).not.toBeInTheDocument();
} );

it.each( [
[ 1, '1 adjustment', { '2026-03': { amount: '-2.50', pageviews: 0, status: 1 } } ],
[
2,
'2 adjustments',
{
'2026-03': { amount: '-2.50', pageviews: 0, status: 1 },
'2025-11': { amount: '12.00', pageviews: 0, status: 0 },
},
],
] )(
'counts %i adjustment row(s) in a line linking to the Adjustments tab',
async ( _count, label, adjustment ) => {
mockApiFetch.mockResolvedValue( { earnings: { ...EARNINGS.earnings, adjustment } } );
render( <WordAdsEarningsHistory attributes={ {} } /> );
await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument();

const link = screen.getByRole( 'link', {
name: `${ label }, view adjustments history`,
} );
expect( within( link ).getByText( label ) ).toBeInTheDocument();
expect( link ).toHaveAttribute( 'href', expect.stringContaining( '/reports/earnings' ) );
expect( link ).toHaveAttribute( 'href', expect.stringContaining( 'section=adjustments' ) );
// The adjustment amounts stay on the report; the widget lists WordAds rows only.
expect( screen.queryByText( '-$2.50' ) ).not.toBeInTheDocument();
}
);

it( 'recovers via Retry after a failed earnings request', async () => {
// Only the first request fails, so rows can only come from Retry's refetch.
mockApiFetch.mockRejectedValueOnce( { status: 403, message: 'Forbidden' } );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* External dependencies
*/
import { useStatsWordAdsEarnings } from '@jetpack-premium-analytics/data';
import { Stack } from '@jetpack-premium-analytics/externals';
import { Badge, Stack } from '@jetpack-premium-analytics/externals';
import {
EarningsHistoryList,
ReportLink,
Expand All @@ -12,7 +12,7 @@ import {
flattenEarningsBreakdown,
type ReportParamsFieldAttributes,
} from '@jetpack-premium-analytics/widgets-toolkit';
import { __ } from '@wordpress/i18n';
import { __, _n, sprintf } from '@wordpress/i18n';
import { useMemo } from 'react';
/**
* Internal dependencies
Expand All @@ -36,6 +36,9 @@ function WordAdsEarningsHistoryReport() {
const { data, isLoading, isFetching, isError, refetch } = useStatsWordAdsEarnings();

const rows = useMemo( () => flattenEarningsBreakdown( data?.wordads ), [ data ] );
// Adjustments are why the all-time balance can differ from the WordAds rows,
// and the report is their only home now; most sites have none.
const adjustmentCount = Object.keys( data?.adjustment ?? {} ).length;

return (
<Stack className={ styles.root }>
Expand Down Expand Up @@ -66,6 +69,36 @@ function WordAdsEarningsHistoryReport() {
report="earnings"
ariaLabel={ __( 'View all earnings history', 'jetpack-premium-analytics-pkg' ) }
/>
{ /* Second in the footer: View all keeps its place, this takes the far end. */ }
{ adjustmentCount > 0 && (
<ReportLink
report="earnings"
section="adjustments"
ariaLabel={ sprintf(
/* translators: %d: number of adjustment rows in the site's earnings history. */
_n(
'%d adjustment, view adjustments history',
'%d adjustments, view adjustments history',
adjustmentCount,
'jetpack-premium-analytics-pkg'
),
adjustmentCount
) }
>
<Badge intent="high">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if Badge should be the link itself? We have to remove the link styling to make it fit, and @wordpress/ui describes Badge as a status label, not something you click.

If we want it to look like a chip, LinkButton may be a better fit. Right now the widget draws its own footer, but later, we will replace this with native widget actions the host draws it (WordPress/gutenberg#81740) and only uses Link or LinkButton. cc @retrofox

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point: nothing in the monorepo puts a Badge inside a link today, so this is a first. I tried a plain Adjustments link with a count badge beside it, but it reads as two things and drifts from Eder's single-chip design for this line; LinkButton keeps the shape but loses the intent colour.

I'd like to ship this as designed and settle the component question in a follow-up with Eder and @retrofox. The change is contained (ReportLink only gained a children slot), so swapping to Link or LinkButton later is a small local change. I'll open the follow-up and link it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up opened: WOOA7S-2194.

{ sprintf(
/* translators: %d: number of adjustment rows in the site's earnings history. */
_n(
'%d adjustment',
'%d adjustments',
adjustmentCount,
'jetpack-premium-analytics-pkg'
),
adjustmentCount
) }
</Badge>
</ReportLink>
) }
</WidgetFooter>
</Stack>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { getDefaultQueryParams } from '@jetpack-premium-analytics/data';
import {
forceWordAdsEarningsState,
registerReportMocks,
setReportMockResponse,
} from '../../../packages/widgets-toolkit/src/stories/mocks/register-report-mocks';
import {
DEFAULT_WIDGET_DASHBOARD_STORY_ARGS,
Expand Down Expand Up @@ -50,7 +51,7 @@ const meta = {
docs: {
description: {
component:
'The "WordAds earnings" widget — WordAds earnings by period (amount, payment status), ported from the Jetpack Stats WordAds page. Ads served lives in the full report.',
'The "WordAds earnings" widget — WordAds earnings by period (amount, payment status), ported from the Jetpack Stats WordAds page. Ads served lives in the full report. A site with adjustment rows gets an "N adjustments" badge at the far end of the footer, into the Adjustments history tab of the report; the shared fixture has two, so the default story shows it.',
},
},
},
Expand Down Expand Up @@ -79,6 +80,30 @@ export const Error: Story = {
beforeEach: forceWordAdsEarningsState( 'error' ),
};

// The shared fixture carries adjustments; most sites have none.
const NO_ADJUSTMENTS = {
earnings: {
total_earnings: 166.3,
total_amount_owed: 75.99,
wordads: {
'2026-05': { amount: '90.31', pageviews: 65921, status: 1 },
'2026-06': { amount: '75.99', pageviews: 59367, status: 0 },
},
sponsored: {},
adjustment: {},
},
};

/** No adjustment rows — the widget renders the list and footer alone. */
export const NoAdjustments: Story = {
tags: [ '!autodocs' ],
decorators: [ withWidgetCanvas, withStoryRouter ],
beforeEach: () => {
setReportMockResponse( 'wordads/earnings', NO_ADJUSTMENTS );
return () => setReportMockResponse( 'wordads/earnings', null );
},
};

/** Resolved but empty — no earnings history for this breakdown. */
export const Empty: Story = {
tags: [ '!autodocs' ],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: enhancement

Premium Analytics: Note in the Earnings History widget when the site has adjustments, linking to the Adjustments history tab of the Earnings report.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Ads: Note in the Earnings History widget when the site has adjustments, linking to the Adjustments history tab of the Earnings report.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Premium Analytics: Note in the Earnings History widget when the site has adjustments, linking to the Adjustments history tab of the Earnings report.
Loading