Skip to content

Commit

Permalink
Downloading media library archive from the export page. (#27581)
Browse files Browse the repository at this point in the history
* Add the action types and the action creators for the media export
substate.

* Implement the media exporter reducer.

* Add the data-layer handlers for interacting with
the `/sites/{id}/exports/media` endpoint.

* Implement a new component specifically for exporting media library:
ExportMediaCard.

* Implement the selector: get-media-export-data

* Implement <QueryMediaExport /> as a query component for sending
`requestMediaExport()` action.

* Dock <ExportMediaCard /> component into the export page when
`export-media` flag is on.

* CSS tweaks so <ExportCard /> and <ExportMediaCard /> looks consistent.

* Disable the action button if there is no `mediaExportUrl`

* Record a track event on clicking the media download button.

* Reword and add more detailed description.

* Add the test suite for the data-layer handler of
`/sites/{id}/exports/media` endpoint.

* Rename the selector as `get-media-export-url` and add the test suite.

* Trim the nested `mediaExportData` down as a plain `mediaExportUrl`

* <QueryMediaExport> should only send the request if the data is not
there.

* Add the missing action type.

* Fix the breaking test.

* Show and keep the old subtitle of <ExportCard> when `export-media` is
off.

* Reword according to editorial comments.

* For future proofing, do not show <ExportMedia /> component for Jetpack
sites.

* Rebase against the latest master.

* Add `propTypes` to `<QueryMediaExportData>` component.

* Remove `export-media` feature flag from `<ExportCard>` component

* Make the whole export card clickable, and reword accordingly.

* Add missing `isRequired` declarations to `recordMediaExportClicks` and
`translate`

* Remove redundant `true` value assignment for `buttonPrimary`

* Destructure out `recordMediaExportClick` and simplify the `react-redux`
`connect` call.

* Remove unnecessary mock.

* Remove `export-media` feature flag.

* Add two more missing proptypes.

* mediaExportUrl shouldn't be required, since it is valid to be null at
the beginning.

* Remove the `clickableHeader` prop and update the description.
  • Loading branch information
southp committed Oct 12, 2018
1 parent 3556afa commit e1b6243
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 3 deletions.
41 changes: 41 additions & 0 deletions client/components/data/query-media-export/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** @format */

/**
* External dependencies
*/
import { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

/**
* Internal dependencies
*/
import { requestMediaExport } from 'state/site-settings/exporter/actions';
import getMediaExportUrl from 'state/selectors/get-media-export-url';

class QueryMediaExport extends Component {
static propTypes = {
mediaExportUrl: PropTypes.string,
requestMediaExport: PropTypes.func.isRequired,
siteId: PropTypes.number.isRequired,
};

componentDidMount() {
if ( this.props.mediaExportUrl ) {
return;
}

this.props.requestMediaExport( this.props.siteId );
}

render() {
return null;
}
}

export default connect(
state => ( {
mediaExportUrl: getMediaExportUrl( state ),
} ),
{ requestMediaExport }
)( QueryMediaExport );
4 changes: 3 additions & 1 deletion client/my-sites/exporter/export-card/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class ExportCard extends Component {
<div>
<h1 className="export-card__title">{ translate( 'Export your content' ) }</h1>
<h2 className="export-card__subtitle">
{ translate( 'Or select specific content items to export' ) }
{ translate(
'Export all (or specific) text content (pages, posts, feedback) from your site.'
) }
</h2>
</div>
}
Expand Down
58 changes: 58 additions & 0 deletions client/my-sites/exporter/export-media-card/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @format */

/**
* External dependencies
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { localize } from 'i18n-calypso';

/**
* Internal dependencies
*/
import ActionCard from 'components/action-card';
import QueryMediaExport from 'components/data/query-media-export';
import getMediaExportUrl from 'state/selectors/get-media-export-url';
import { recordTracksEvent } from 'state/analytics/actions';

class ExportMediaCard extends Component {
static propTypes = {
siteId: PropTypes.number.isRequired,
mediaExportUrl: PropTypes.string,
recordMediaExportClick: PropTypes.func.isRequired,
translate: PropTypes.func.isRequired,
};

render() {
const { mediaExportUrl, siteId, translate, recordMediaExportClick } = this.props;

return (
<div className="export-media-card">
<QueryMediaExport siteId={ siteId } />
<ActionCard
className="export-media-card__content export-card"
headerText={ translate( 'Export media library' ) }
mainText={ translate(
'Download your entire media library (images, videos, audios …) of your site.'
) }
buttonText={ translate( 'Download' ) }
buttonHref={ mediaExportUrl }
buttonDisabled={ ! mediaExportUrl }
buttonOnClick={ recordMediaExportClick }
compact={ false }
buttonPrimary
/>
</div>
);
}
}

export default connect(
state => ( {
mediaExportUrl: getMediaExportUrl( state ),
} ),
{
recordMediaExportClick: () => recordTracksEvent( 'calypso_export_media_download_button_click' ),
}
)( localize( ExportMediaCard ) );
8 changes: 6 additions & 2 deletions client/my-sites/exporter/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import { connect } from 'react-redux';
import config from 'config';
import QuerySiteGuidedTransfer from 'components/data/query-site-guided-transfer';
import { getSelectedSiteId } from 'state/ui/selectors';
import { isJetpackSite } from 'state/sites/selectors';
import { isGuidedTransferInProgress } from 'state/sites/guided-transfer/selectors';
import Notices from './notices';
import ExportCard from './export-card';
import ExportMediaCard from './export-media-card';
import GuidedTransferCard from './guided-transfer-card';
import InProgressCard from './guided-transfer-card/in-progress';

class Exporter extends Component {
render() {
const { siteId, isTransferInProgress } = this.props;
const { siteId, isJetpack, isTransferInProgress } = this.props;
const showGuidedTransferOptions = config.isEnabled( 'manage/export/guided-transfer' );

return (
Expand All @@ -31,14 +33,16 @@ class Exporter extends Component {
<Notices />
{ showGuidedTransferOptions && isTransferInProgress && <InProgressCard /> }
<ExportCard siteId={ siteId } />
{ ! isJetpack && <ExportMediaCard siteId={ siteId } /> }
{ showGuidedTransferOptions && ! isTransferInProgress && <GuidedTransferCard /> }
</div>
);
}
}

const mapStateToProps = state => ( {
const mapStateToProps = ( state, { siteId } ) => ( {
siteId: getSelectedSiteId( state ),
isJetpack: isJetpackSite( state, siteId ),
isTransferInProgress: isGuidedTransferInProgress( state, getSelectedSiteId( state ) ),
} );

Expand Down
17 changes: 17 additions & 0 deletions client/my-sites/exporter/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,28 @@
clear: left;
}

.export-card .foldable-card__header {
padding: 24px;
}

.export-media-card .action-card__heading {
font-size: 21px;
font-weight: 300;
color: $gray-dark;
margin-bottom: 16px;
clear: left;
}

.export-card__subtitle {
font-size: 14px;
color: $gray-dark;
}

.export-media-card .action-card__main p {
font-size: 14px;
color: $gray-dark;
}

.export-card__spinner-button {
float: right;
padding: 8px;
Expand Down
2 changes: 2 additions & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export const EXPORT_ADVANCED_SETTINGS_FETCH_FAIL = 'EXPORT_ADVANCED_SETTINGS_FET
export const EXPORT_ADVANCED_SETTINGS_RECEIVE = 'EXPORT_ADVANCED_SETTINGS_RECEIVE';
export const EXPORT_CLEAR = 'EXPORT_CLEAR';
export const EXPORT_COMPLETE = 'EXPORT_COMPLETE';
export const EXPORT_MEDIA_REQUEST = 'EXPORT_MEDIA_REQUEST';
export const EXPORT_FAILURE = 'EXPORT_FAILURE';
export const EXPORT_POST_TYPE_FIELD_SET = 'EXPORT_POST_TYPE_FIELD_SET';
export const EXPORT_POST_TYPE_SET = 'EXPORT_POST_TYPE_SET';
Expand Down Expand Up @@ -854,6 +855,7 @@ export const SELECTED_SITE_SUBSCRIBE = 'SELECTED_SITE_SUBSCRIBE';
export const SELECTED_SITE_UNSUBSCRIBE = 'SELECTED_SITE_UNSUBSCRIBE';
export const SEO_TITLE_SET = 'SEO_TITLE_SET';
export const SERIALIZE = 'SERIALIZE';
export const SET_MEDIA_EXPORT_DATA = 'SET_MEDIA_EXPORT_DATA';
export const SHARING_BUTTONS_RECEIVE = 'SHARING_BUTTONS_RECEIVE';
export const SHARING_BUTTONS_REQUEST = 'SHARING_BUTTONS_REQUEST';
export const SHARING_BUTTONS_REQUEST_FAILURE = 'SHARING_BUTTONS_REQUEST_FAILURE';
Expand Down
51 changes: 51 additions & 0 deletions client/state/data-layer/wpcom/sites/exports/media/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/** @format */

/**
* External dependencies
*/
import { translate } from 'i18n-calypso';

/**
* Internal dependencies
*/
import { http } from 'state/data-layer/wpcom-http/actions';
import { dispatchRequestEx } from 'state/data-layer/wpcom-http/utils';
import { registerHandlers } from 'state/data-layer/handler-registry';
import { errorNotice } from 'state/notices/actions';
import { setMediaExportData } from 'state/site-settings/exporter/actions';
import { EXPORT_MEDIA_REQUEST } from 'state/action-types';

export const fetch = action =>
http(
{
method: 'GET',
path: `/sites/${ action.siteId }/exports/media`,
apiNamespace: 'rest/v1.1',
query: {
http_envelope: 1,
},
},
action
);

export const onSuccess = ( action, { mediaExportUrl } ) => setMediaExportData( mediaExportUrl );

export const onError = () =>
errorNotice(
translate( "We couldn't export your media library at the moment. Please try again later." )
);

export const fromApi = response => ( {
mediaExportUrl: response.media_export_url,
} );

registerHandlers( 'state/data-layer/wpcom/sites/exports/media/index.js', {
[ EXPORT_MEDIA_REQUEST ]: [
dispatchRequestEx( {
fetch,
onSuccess,
onError,
fromApi,
} ),
],
} );
60 changes: 60 additions & 0 deletions client/state/data-layer/wpcom/sites/exports/media/test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/** @format */

/**
* Internal dependencies
*/
import { http } from 'state/data-layer/wpcom-http/actions';
import { NOTICE_CREATE } from 'state/action-types';
import { setMediaExportData } from 'state/site-settings/exporter/actions';
import { fetch, onSuccess, onError, fromApi } from '../';

describe( 'fetch()', () => {
test( 'should dispatch the expected http action.', () => {
const action = {
siteId: 123,
};
expect( fetch( action ) ).toEqual(
http(
{
method: 'GET',
path: `/sites/${ action.siteId }/exports/media`,
apiNamespace: 'rest/v1.1',
query: {
http_envelope: 1,
},
},
action
)
);
} );
} );

describe( 'onSuccess()', () => {
test( 'should dispatch the action for storing the received data on success.', () => {
const action = {
siteId: 123,
};
const data = {
mediaExportUrl: 'aaa',
};
expect( onSuccess( action, data ) ).toEqual( setMediaExportData( data.mediaExportUrl ) );
} );
} );

describe( 'onError()', () => {
test( 'should dispatch an notice action on error.', () => {
expect( onError().type ).toEqual( NOTICE_CREATE );
} );
} );

describe( 'fromApi()', () => {
test( 'should convert the encapsulated data as expected.', () => {
expect(
fromApi( {
media_export_url: 'aaa',
} )
).toEqual( {
mediaExportUrl: 'aaa',
} );
} );
} );
8 changes: 8 additions & 0 deletions client/state/selectors/get-media-export-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @format */

/**
* External dependencies
*/
import { get } from 'lodash';

export default state => get( state, 'siteSettings.exporter.mediaExportUrl', null );
25 changes: 25 additions & 0 deletions client/state/selectors/test/get-media-export-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/** @format */

/**
* Internal dependencies
*/
import getMediaExportUrl from 'state/selectors/get-media-export-url';

describe( 'getMediaExportUrl()', () => {
test( 'should return the stored media export url field.', () => {
const mediaExportUrl = 'https://examples.com/profit';
expect(
getMediaExportUrl( {
siteSettings: {
exporter: {
mediaExportUrl,
},
},
} )
).toEqual( mediaExportUrl );
} );

test( 'should default to null.', () => {
expect( getMediaExportUrl( {} ) ).toBeNull();
} );
} );
18 changes: 18 additions & 0 deletions client/state/site-settings/exporter/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ import {
EXPORT_STATUS_FETCH,
EXPORT_POST_TYPE_SET,
EXPORT_POST_TYPE_FIELD_SET,
EXPORT_MEDIA_REQUEST,
SET_MEDIA_EXPORT_DATA,
} from 'state/action-types';

import { prepareExportRequest } from './selectors';

import 'state/data-layer/wpcom/sites/exports/media';

/**
* Sets the post type to export.
*
Expand Down Expand Up @@ -183,3 +187,17 @@ export function clearExport( siteId ) {
siteId,
};
}

export function requestMediaExport( siteId ) {
return {
type: EXPORT_MEDIA_REQUEST,
siteId,
};
}

export function setMediaExportData( mediaExportUrl ) {
return {
type: SET_MEDIA_EXPORT_DATA,
mediaExportUrl,
};
}
Loading

0 comments on commit e1b6243

Please sign in to comment.