Skip to content

Commit 882d9ad

Browse files
halfodognose24
andauthored
STATS-63: Download CSV for email. (#103581)
* STATS-63: Add an option to modify export array. * STATS-63: Add an option to modify CSV data. * STATS-63: Add Email CSV download functionality. * STATS-63: Add headers support in CSV. * STATS-63: Add headers in email CSV. * STATS-63: Use proper array checking. Co-authored-by: Dognose <[email protected]> * STATS-63: Preserve the args order in buildExportArray. * STATS-63: Align CSV button in the middle. --------- Co-authored-by: Dognose <[email protected]>
1 parent e1ab630 commit 882d9ad

File tree

5 files changed

+78
-17
lines changed

5 files changed

+78
-17
lines changed

client/my-sites/stats/stats-download-csv/index.jsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class StatsDownloadCsv extends Component {
3030
siteId: PropTypes.number,
3131
isMobile: PropTypes.bool,
3232
hideIfNoData: PropTypes.bool,
33+
headers: PropTypes.array,
34+
rowModifierFn: PropTypes.func,
3335
};
3436

3537
processExportData = ( data ) => {
@@ -51,7 +53,7 @@ class StatsDownloadCsv extends Component {
5153

5254
downloadCsv = ( event ) => {
5355
event.preventDefault();
54-
const { siteSlug, path, period, data } = this.props;
56+
const { siteSlug, path, period, data, headers } = this.props;
5557

5658
const fileName =
5759
[
@@ -64,7 +66,11 @@ class StatsDownloadCsv extends Component {
6466

6567
this.props.recordGoogleEvent( 'Stats', 'CSV Download ' + titlecase( path ) );
6668

67-
const csvData = this.processExportData( data )
69+
const csvData = headers
70+
? [ headers, ...this.processExportData( data ) ]
71+
: this.processExportData( data );
72+
73+
const csvString = csvData
6874
.map( ( row ) => {
6975
if ( Array.isArray( row ) ) {
7076
return row.join( ',' );
@@ -76,7 +82,7 @@ class StatsDownloadCsv extends Component {
7682
} )
7783
.join( '\n' );
7884

79-
const blob = new Blob( [ csvData ], { type: 'text/csv;charset=utf-8' } );
85+
const blob = new Blob( [ csvString ], { type: 'text/csv;charset=utf-8' } );
8086

8187
saveAs( blob, fileName );
8288
};
@@ -137,11 +143,11 @@ const connectComponent = connect(
137143
return { data: ownProps.data, siteSlug, siteId, isLoading: false };
138144
}
139145

140-
const { statType, query } = ownProps;
146+
const { statType, query, rowModifierFn } = ownProps;
141147
const data =
142148
statType === 'statsVideoPlays'
143149
? getSiteStatsNormalizedData( state, siteId, statType, query )
144-
: getSiteStatsCSVData( state, siteId, statType, query );
150+
: getSiteStatsCSVData( state, siteId, statType, query, rowModifierFn );
145151
const isLoading = isRequestingSiteStatsForQuery( state, siteId, statType, query );
146152

147153
return { data, siteSlug, siteId, isLoading };

client/my-sites/stats/stats-email-summary/index.jsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@ import { useMemo, useEffect } from 'react';
66
import JetpackColophon from 'calypso/components/jetpack-colophon';
77
import NavigationHeader from 'calypso/components/navigation-header';
88
import Main from 'calypso/my-sites/stats/components/stats-main';
9+
import { useShouldGateStats } from 'calypso/my-sites/stats/hooks/use-should-gate-stats';
910
import { recordCurrentScreen } from 'calypso/my-sites/stats/hooks/use-stats-navigation-history';
11+
import DownloadCsv from 'calypso/my-sites/stats/stats-download-csv';
12+
import DownloadCsvUpsell from 'calypso/my-sites/stats/stats-download-csv-upsell';
1013
import { useSelector } from 'calypso/state';
1114
import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors';
1215
import PageHeader from '../components/headers/page-header';
16+
import { STATS_FEATURE_DOWNLOAD_CSV } from '../constants';
1317
import {
1418
TooltipWrapper,
1519
OpensTooltipContent,
@@ -28,6 +32,7 @@ const StatsEmailSummary = ( { period, query, context } ) => {
2832
const translate = useTranslate();
2933
const siteId = useSelector( getSelectedSiteId );
3034
const siteSlug = useSelector( ( state ) => getSelectedSiteSlug( state, siteId ) );
35+
const shouldGateCsvDownloads = useShouldGateStats( STATS_FEATURE_DOWNLOAD_CSV );
3136

3237
// TODO: align with all summary pages.
3338
const navigationItems = useMemo( () => {
@@ -79,6 +84,26 @@ const StatsEmailSummary = ( { period, query, context } ) => {
7984
titleLogo: null,
8085
};
8186

87+
const downloadCsvElement = shouldGateCsvDownloads ? (
88+
<DownloadCsvUpsell siteId={ siteId } borderless />
89+
) : (
90+
<DownloadCsv
91+
statType="statsEmailsSummary"
92+
path="emails"
93+
query={ query }
94+
period={ period }
95+
headers={ [ 'title', 'opens_rate', 'unique_clicks', 'link' ] }
96+
rowModifierFn={ ( row, data ) => {
97+
if ( ! Array.isArray( row ) || row.length === 0 ) {
98+
return row;
99+
}
100+
101+
const [ label, ...rest ] = row;
102+
return [ label, data.opens_rate, ...rest ];
103+
} }
104+
/>
105+
);
106+
82107
return (
83108
<Main
84109
className={ clsx( {
@@ -93,6 +118,9 @@ const StatsEmailSummary = ( { period, query, context } ) => {
93118
className="stats__section-header modernized-header"
94119
titleProps={ titleProps }
95120
backLinkProps={ backLinkProps }
121+
rightSection={
122+
<div className="stats-module__header-nav-button">{ downloadCsvElement }</div>
123+
}
96124
/>
97125
) }
98126

client/state/stats/lists/selectors.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,21 +160,23 @@ export const getSiteStatsNormalizedData = treeSelect(
160160

161161
/**
162162
* Returns an array of stats data ready for csv export
163-
* @param {Object} state Global state tree
164-
* @param {number} siteId Site ID
165-
* @param {string} statType Type of stat
166-
* @param {Object} query Stats query object
167-
* @returns {Array} Array of stats data ready for CSV export
163+
* @template T
164+
* @param {Object} state Global state tree
165+
* @param {number} siteId Site ID
166+
* @param {string} statType Type of stat
167+
* @param {Object} query Stats query object
168+
* @param {(value: unknown[], data?: Object) => unknown[]} modifierFn Modifies the export row.
169+
* @returns {Array} Array of stats data ready for CSV export
168170
*/
169-
export function getSiteStatsCSVData( state, siteId, statType, query ) {
171+
export function getSiteStatsCSVData( state, siteId, statType, query, modifierFn = null ) {
170172
const data = getSiteStatsNormalizedData( state, siteId, statType, query );
171173
if ( ! data || ! Array.isArray( data ) ) {
172174
return [];
173175
}
174176

175177
return flatten(
176178
map( data, ( item ) => {
177-
return buildExportArray( item );
179+
return buildExportArray( item, null, modifierFn );
178180
} )
179181
);
180182
}

client/state/stats/lists/test/utils.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,26 @@ describe( 'utils', () => {
199199
).toEqual( [ [ '"Chicken and ""Ribs""', 10, 'https://example.com/chicken' ] ] );
200200
} );
201201

202+
test( 'should modify each row in csv by a modifier function', () => {
203+
expect(
204+
buildExportArray(
205+
{
206+
actions: [
207+
{
208+
type: 'link',
209+
data: 'https://example.com/chicken',
210+
},
211+
],
212+
label: 'Chicken',
213+
value: 10,
214+
others: [ 1, 2, 3 ],
215+
},
216+
null,
217+
( row, data ) => [ ...row, data.others[ 2 ] ]
218+
)
219+
).toEqual( [ [ '"Chicken"', 10, 'https://example.com/chicken', 3 ] ] );
220+
} );
221+
202222
test( 'should recurse child data', () => {
203223
expect(
204224
buildExportArray( {

client/state/stats/lists/utils.js

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ export function parseAvatar( avatarUrl ) {
8484

8585
/**
8686
* Builds data into escaped array for CSV export
87-
* @param {Object} data Normalized stats data object
88-
* @param {string} parent Label of parent
89-
* @returns {Array} CSV Row
87+
* @param {Object} data Normalized stats data object
88+
* @param {string} parent Label of parent
89+
* @param {(value: unknown[], data?: Object) => unknown[]} modifierFn Modifies the export row.
90+
* @returns {Array} CSV Row
9091
*/
91-
export function buildExportArray( data, parent = null ) {
92+
export function buildExportArray( data, parent = null, modifierFn = null ) {
9293
if ( ! data || ! data.label || ! data.value ) {
9394
return [];
9495
}
@@ -102,9 +103,13 @@ export function buildExportArray( data, parent = null ) {
102103
exportData = [ [ '"' + escapedLabel + '"', data.value, data.actions[ 0 ].data ] ];
103104
}
104105

106+
if ( modifierFn ) {
107+
exportData = [ modifierFn( exportData[ 0 ], data ) ];
108+
}
109+
105110
if ( data.children ) {
106111
const childData = map( data.children, ( child ) => {
107-
return buildExportArray( child, label );
112+
return buildExportArray( child, label, modifierFn );
108113
} );
109114

110115
exportData = exportData.concat( flatten( childData ) );

0 commit comments

Comments
 (0)