diff --git a/client/my-sites/stats/stats-download-csv/get-stats-csv-filename.js b/client/my-sites/stats/stats-download-csv/get-stats-csv-filename.js new file mode 100644 index 000000000000..b26fb1dc313b --- /dev/null +++ b/client/my-sites/stats/stats-download-csv/get-stats-csv-filename.js @@ -0,0 +1,41 @@ +import moment from 'moment'; + +/** + * Builds the CSV export filename so it matches the data selection. + * + * When a custom date range query is present (`start_date` + `date`), those dates + * are used and the period segment is omitted (query.period is forced to `day` for + * API reasons and is not meaningful in the filename). Otherwise the legacy period + * object label and bounds are used. + * @param {Object} options + * @param {string} options.siteSlug Site slug used as the filename prefix. + * @param {string} options.path Stats module path segment (e.g. "posts"). + * @param {Object} options.period Period object with `period`, `startOf`, and `endOf`. + * @param {Object} [options.query] Stats query; custom ranges include `start_date` and `date`. + * @param {boolean} [options.includeDates] Pass false for exports that are not date-scoped + * (e.g. the all-time Emails summary) to omit the period and date segments. + * @returns {string} Filename ending in `.csv`. + */ +export function getStatsCsvFileName( { siteSlug, path, period, query, includeDates = true } ) { + if ( ! includeDates ) { + return [ siteSlug, path ].join( '-' ) + '.csv'; + } + + const dateLocale = period.startOf.locale(); + const customStart = query?.start_date ? moment( query.start_date, 'YYYY-MM-DD', true ) : null; + const customEnd = query?.date ? moment( query.date, 'YYYY-MM-DD', true ) : null; + const hasCustomDateRange = Boolean( customStart?.isValid() && customEnd?.isValid() ); + + const startDate = hasCustomDateRange + ? customStart.locale( dateLocale ).format( 'L' ) + : period.startOf.format( 'L' ); + const endDate = hasCustomDateRange + ? customEnd.locale( dateLocale ).format( 'L' ) + : period.endOf.format( 'L' ); + + if ( hasCustomDateRange ) { + return [ siteSlug, path, startDate, endDate ].join( '-' ) + '.csv'; + } + + return [ siteSlug, path, period.period, startDate, endDate ].join( '-' ) + '.csv'; +} diff --git a/client/my-sites/stats/stats-download-csv/index.jsx b/client/my-sites/stats/stats-download-csv/index.jsx index 742a63fe45bc..25caef9370c6 100644 --- a/client/my-sites/stats/stats-download-csv/index.jsx +++ b/client/my-sites/stats/stats-download-csv/index.jsx @@ -16,6 +16,7 @@ import { isRequestingSiteStatsForQuery, } from 'calypso/state/stats/lists/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; +import { getStatsCsvFileName } from './get-stats-csv-filename'; import './style.scss'; @@ -32,6 +33,7 @@ class StatsDownloadCsv extends Component { hideIfNoData: PropTypes.bool, headers: PropTypes.array, rowModifierFn: PropTypes.func, + includeDates: PropTypes.bool, }; processExportData = ( data ) => { @@ -53,16 +55,9 @@ class StatsDownloadCsv extends Component { downloadCsv = ( event ) => { event.preventDefault(); - const { siteSlug, path, period, data, headers } = this.props; - - const fileName = - [ - siteSlug, - path, - period.period, - period.startOf.format( 'L' ), - period.endOf.format( 'L' ), - ].join( '-' ) + '.csv'; + const { siteSlug, path, period, query, data, headers, includeDates } = this.props; + + const fileName = getStatsCsvFileName( { siteSlug, path, period, query, includeDates } ); this.props.recordGoogleEvent( 'Stats', 'CSV Download ' + titlecase( path ) ); diff --git a/client/my-sites/stats/stats-download-csv/test/get-stats-csv-filename.js b/client/my-sites/stats/stats-download-csv/test/get-stats-csv-filename.js new file mode 100644 index 000000000000..5bd1e9dcd934 --- /dev/null +++ b/client/my-sites/stats/stats-download-csv/test/get-stats-csv-filename.js @@ -0,0 +1,117 @@ +import moment from 'moment'; +import { getStatsCsvFileName } from '../get-stats-csv-filename'; + +describe( 'getStatsCsvFileName', () => { + const siteSlug = 'mercantile.wordpress.org'; + const path = 'posts'; + let previousLocale; + + beforeAll( () => { + previousLocale = moment.locale(); + moment.locale( 'en' ); + } ); + + afterAll( () => { + moment.locale( previousLocale ); + } ); + + it( 'uses the selected custom date range for multi-month exports (STATS-420)', () => { + // Period object still reflects a single month unit around the start date, + // which previously produced filenames like posts-month-01/01/2026-01/31/2026. + // query.period is forced to 'day' for custom ranges and is omitted from the filename. + const period = { + period: 'month', + startOf: moment( '2026-01-01' ), + endOf: moment( '2026-01-31' ), + }; + const query = { + period: 'day', + start_date: '2026-01-01', + date: '2026-08-06', + summarize: 1, + }; + + expect( getStatsCsvFileName( { siteSlug, path, period, query } ) ).toBe( + 'mercantile.wordpress.org-posts-01/01/2026-08/06/2026.csv' + ); + } ); + + it( 'uses the selected custom date range for a single-month export (STATS-420)', () => { + // A day-period unit around the start date previously produced + // posts-day-01/01/2026-01/01/2026 for a Jan 1–31 selection. + const period = { + period: 'day', + startOf: moment( '2026-01-01' ), + endOf: moment( '2026-01-01' ), + }; + const query = { + period: 'day', + start_date: '2026-01-01', + date: '2026-01-31', + summarize: 1, + }; + + expect( getStatsCsvFileName( { siteSlug, path, period, query } ) ).toBe( + 'mercantile.wordpress.org-posts-01/01/2026-01/31/2026.csv' + ); + } ); + + it( 'falls back to period bounds when no custom date range is present', () => { + const period = { + period: 'month', + startOf: moment( '2026-01-01' ), + endOf: moment( '2026-01-31' ), + }; + const query = { + period: 'month', + date: '2026-01-31', + }; + + expect( getStatsCsvFileName( { siteSlug, path, period, query } ) ).toBe( + 'mercantile.wordpress.org-posts-month-01/01/2026-01/31/2026.csv' + ); + } ); + + it( 'falls back to period bounds when custom range dates are invalid', () => { + const period = { + period: 'month', + startOf: moment( '2026-01-01' ), + endOf: moment( '2026-01-31' ), + }; + const query = { + period: 'day', + start_date: 'not-a-date', + date: '2026-08-06', + summarize: 1, + }; + + expect( getStatsCsvFileName( { siteSlug, path, period, query } ) ).toBe( + 'mercantile.wordpress.org-posts-month-01/01/2026-01/31/2026.csv' + ); + } ); + + it( 'omits the period and date segments when includeDates is false', () => { + const period = { + period: 'day', + startOf: moment( '2026-08-06' ), + endOf: moment( '2026-08-06' ), + }; + const query = { quantity: 30 }; + + expect( + getStatsCsvFileName( { siteSlug, path: 'emails', period, query, includeDates: false } ) + ).toBe( 'mercantile.wordpress.org-emails.csv' ); + } ); + + it( 'falls back to period bounds when query is omitted', () => { + const period = { + period: 'week', + startOf: moment( '2026-01-05' ), + endOf: moment( '2026-01-11' ), + }; + + expect( getStatsCsvFileName( { siteSlug, path, period } ) ).toBe( + 'mercantile.wordpress.org-posts-week-01/05/2026-01/11/2026.csv' + ); + } ); +} ); diff --git a/client/my-sites/stats/stats-email-summary/index.jsx b/client/my-sites/stats/stats-email-summary/index.jsx index b4bbfb9c4a0f..c5ae09977107 100644 --- a/client/my-sites/stats/stats-email-summary/index.jsx +++ b/client/my-sites/stats/stats-email-summary/index.jsx @@ -79,6 +79,7 @@ const StatsEmailSummaryInner = ( { period, query, context, breadcrumbTrail } ) = path="emails" query={ query } period={ period } + includeDates={ false } headers={ [ 'title', 'opens_rate', 'unique_clicks', 'link' ] } rowModifierFn={ ( row, data ) => { if ( ! Array.isArray( row ) || row.length === 0 ) {