From 6571b23a8309143704bb581378f690fb1de13c9b Mon Sep 17 00:00:00 2001 From: dognose24 Date: Mon, 21 Sep 2026 15:17:15 +0800 Subject: [PATCH 1/5] Premium Analytics: note adjustments in the Earnings History widget When the earnings payload carries adjustment rows, the widget renders an "Includes N adjustments" link above its footer, into the Adjustments history tab of the Earnings report. Sites without adjustments render exactly as before. The amounts and notes stay on the report. Co-Authored-By: Claude Fable 5.1 --- ...nalytics-earnings-history-adjustments-line | 4 +++ .../wordads-earnings-history.test.tsx | 35 +++++++++++++++++++ .../wordads-earnings-history/render.tsx | 24 ++++++++++++- ...ordads-earnings-history-widget.stories.tsx | 27 +++++++++++++- .../wordads-earnings-history/style.module.css | 6 ++++ ...nalytics-earnings-history-adjustments-line | 4 +++ ...nalytics-earnings-history-adjustments-line | 4 +++ ...nalytics-earnings-history-adjustments-line | 4 +++ 8 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line create mode 100644 projects/plugins/jetpack/changelog/add-premium-analytics-earnings-history-adjustments-line create mode 100644 projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line create mode 100644 projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-history-adjustments-line diff --git a/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line b/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line new file mode 100644 index 000000000000..dfe1349338f6 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line @@ -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. diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx index d041c0fc41d5..98c92bbacc1a 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx @@ -109,6 +109,41 @@ describe( 'WordAdsEarningsHistory', () => { ); } ); + it( 'shows no adjustments line when the site has none', async () => { + render( ); + await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument(); + + expect( + screen.queryByRole( 'link', { name: 'View adjustments history' } ) + ).not.toBeInTheDocument(); + } ); + + it.each( [ + [ 1, 'Includes 1 adjustment', { '2026-03': { amount: '-2.50', pageviews: 0, status: 1 } } ], + [ + 2, + 'Includes 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( ); + await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument(); + + const link = screen.getByRole( 'link', { name: 'View adjustments history' } ); + expect( link ).toHaveTextContent( label ); + 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' } ); diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx index 39200948c9c1..263804196d80 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx @@ -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 @@ -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 ( @@ -61,6 +64,25 @@ function WordAdsEarningsHistoryReport() { + { adjustmentCount > 0 && ( +
+ +
+ ) } { + 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' ], diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css b/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css index d81bc86bb62d..5763a3fd1bfa 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css @@ -12,3 +12,9 @@ flex: 1 1 0; min-block-size: 0; } + +/* Above the footer rule, so the list gives up a row rather than the link. */ +.adjustments { + flex: 0 0 auto; + padding-block-start: var(--wpds-dimension-padding-sm); +} diff --git a/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-history-adjustments-line b/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-history-adjustments-line new file mode 100644 index 000000000000..3f030a247769 --- /dev/null +++ b/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-history-adjustments-line @@ -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. diff --git a/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line b/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line new file mode 100644 index 000000000000..dfe1349338f6 --- /dev/null +++ b/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-history-adjustments-line @@ -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. diff --git a/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-history-adjustments-line b/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-history-adjustments-line new file mode 100644 index 000000000000..c3c497ce0262 --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-history-adjustments-line @@ -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. From 3534e450b8e3837a212553cbbbe4aa156557dd84 Mon Sep 17 00:00:00 2001 From: dognose24 Date: Mon, 21 Sep 2026 16:16:58 +0800 Subject: [PATCH 2/5] Premium Analytics: put the adjustments link in the widget footer Second child of the footer, so View all keeps its place and the link takes the far end only on sites that have adjustments. The tile height no longer changes for them either. Co-Authored-By: Claude Fable 5.1 --- .../widgets/wordads-earnings-history/render.tsx | 17 ++++++++--------- .../wordads-earnings-history/style.module.css | 6 ------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx index 263804196d80..290397ccef14 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx @@ -64,8 +64,13 @@ function WordAdsEarningsHistoryReport() { - { adjustmentCount > 0 && ( -
+ + + { /* Second in the footer: View all keeps its place, this takes the far end. */ } + { adjustmentCount > 0 && ( -
- ) } - - + ) }
); diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css b/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css index 5763a3fd1bfa..d81bc86bb62d 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/style.module.css @@ -12,9 +12,3 @@ flex: 1 1 0; min-block-size: 0; } - -/* Above the footer rule, so the list gives up a row rather than the link. */ -.adjustments { - flex: 0 0 auto; - padding-block-start: var(--wpds-dimension-padding-sm); -} From 8dcc8a34536667fee97257e3dbdd929d10ffffd3 Mon Sep 17 00:00:00 2001 From: dognose24 Date: Tue, 22 Sep 2026 01:20:19 +0800 Subject: [PATCH 3/5] Premium Analytics: badge the adjustments count in the widget footer Per Eder's design pass: the footer link is an "N adjustments" badge, without "Includes", so it cannot crowd View all in a narrow tile. ReportLink gains a children slot for it and drops its hover underline there. Co-Authored-By: Claude Fable 5.1 --- .../report-link/report-link.module.scss | 7 +++++ .../components/report-link/report-link.tsx | 19 ++++++++++--- .../wordads-earnings-history.test.tsx | 4 +-- .../wordads-earnings-history/render.tsx | 27 ++++++++++--------- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss index c233811577e4..bc7acad43862 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss @@ -10,6 +10,13 @@ text-decoration: underline; } +// A badge or other element stands in for the text; underlining it reads as +// a glitch. +.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 diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.tsx index 4958ce052879..79dbb6d20d81 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.tsx @@ -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 = { /** @@ -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; }; /** @@ -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 ( @@ -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' ) } ); } diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx index 98c92bbacc1a..8e03c5fc5443 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx @@ -119,10 +119,10 @@ describe( 'WordAdsEarningsHistory', () => { } ); it.each( [ - [ 1, 'Includes 1 adjustment', { '2026-03': { amount: '-2.50', pageviews: 0, status: 1 } } ], + [ 1, '1 adjustment', { '2026-03': { amount: '-2.50', pageviews: 0, status: 1 } } ], [ 2, - 'Includes 2 adjustments', + '2 adjustments', { '2026-03': { amount: '-2.50', pageviews: 0, status: 1 }, '2025-11': { amount: '12.00', pageviews: 0, status: 0 }, diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx index 290397ccef14..9e57d0209349 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx @@ -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, @@ -74,18 +74,21 @@ function WordAdsEarningsHistoryReport() { + > + + { sprintf( + /* translators: %d: number of adjustment rows in the site's earnings history. */ + _n( + '%d adjustment', + '%d adjustments', + adjustmentCount, + 'jetpack-premium-analytics-pkg' + ), + adjustmentCount + ) } + + ) } From 5519c0649a88585024d61ccf278abea9fb1136e6 Mon Sep 17 00:00:00 2001 From: dognose24 Date: Tue, 22 Sep 2026 01:49:50 +0800 Subject: [PATCH 4/5] Premium Analytics: stand the footer badge at its full height The link with content is an inline-flex box, so the badge inside it takes its line height plus padding, matching the status badges. Co-Authored-By: Claude Fable 5.1 --- .../src/components/report-link/report-link.module.scss | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss index bc7acad43862..85316758e824 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/report-link/report-link.module.scss @@ -10,8 +10,13 @@ text-decoration: underline; } -// A badge or other element stands in for the text; underlining it reads as -// a glitch. +// 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; From 4ed966386277d6a50fdf8737d8bdc927be0f7f92 Mon Sep 17 00:00:00 2001 From: dognose24 Date: Tue, 22 Sep 2026 22:49:02 +0800 Subject: [PATCH 5/5] Premium Analytics: keep the adjustment count in the badge link's name The aria-label replaced the badge text as the link's accessible name, so screen readers never heard the count and voice control could not match the visible "2 adjustments". Build the label from the same _n() string. The test now matches the badge text exactly, so the singular row fails on its own if the plural forms are swapped. Co-Authored-By: Claude Fable 5.1 --- .../__tests__/wordads-earnings-history.test.tsx | 10 ++++++---- .../widgets/wordads-earnings-history/render.tsx | 11 ++++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx index 8e03c5fc5443..588bcfd052e3 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/__tests__/wordads-earnings-history.test.tsx @@ -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 @@ -114,7 +114,7 @@ describe( 'WordAdsEarningsHistory', () => { await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument(); expect( - screen.queryByRole( 'link', { name: 'View adjustments history' } ) + screen.queryByRole( 'link', { name: /view adjustments history/ } ) ).not.toBeInTheDocument(); } ); @@ -135,8 +135,10 @@ describe( 'WordAdsEarningsHistory', () => { render( ); await expect( screen.findByText( 'July 2026' ) ).resolves.toBeInTheDocument(); - const link = screen.getByRole( 'link', { name: 'View adjustments history' } ); - expect( link ).toHaveTextContent( label ); + 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. diff --git a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx index 9e57d0209349..2ce679649383 100644 --- a/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx +++ b/projects/packages/premium-analytics/widgets/wordads-earnings-history/render.tsx @@ -74,7 +74,16 @@ function WordAdsEarningsHistoryReport() { { sprintf(