Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Importer: Add Site Importer Beta to Calypso #23930

Merged
merged 4 commits into from
Apr 13, 2018
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
29 changes: 20 additions & 9 deletions client/my-sites/importer/importer-header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Button from 'components/forms/form-button';
import { appStates } from 'state/imports/constants';
import { cancelImport, resetImport, startImport } from 'lib/importer/actions';
import { connectDispatcher } from './dispatcher-converter';
import SiteImporterPlaceholderLogo from './site-importer/placeholder-logo';

/**
* Module variables
Expand Down Expand Up @@ -82,22 +83,32 @@ class ImporterHeader extends React.PureComponent {
}
};

getLogo = icon => {
if ( includes( [ 'wordpress', 'medium' ], icon ) ) {
return <SocialLogo className="importer__service-icon" icon={ icon } size={ 48 } />;
}

if ( includes( [ 'site-importer' ], icon ) ) {
return <SiteImporterPlaceholderLogo />;
}
return (
<svg
className="importer__service-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
/>
);
};

render() {
const { importerStatus: { importerState }, icon, isEnabled, title, description } = this.props;
const canCancel =
isEnabled && ! includes( [ appStates.UPLOADING, ...stopStates ], importerState );
const isScary = includes( [ ...cancelStates ], importerState );

return (
<header className="importer-service">
{ includes( [ 'wordpress', 'medium' ], icon ) ? (
<SocialLogo className="importer__service-icon" icon={ icon } size={ 48 } />
) : (
<svg
className="importer__service-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
/>
) }
{ this.getLogo( icon ) }
<Button
className="importer__master-control"
disabled={ ! canCancel }
Expand Down
45 changes: 45 additions & 0 deletions client/my-sites/importer/importer-site-importer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/** @format */

/**
* External dependencies
*/

import PropTypes from 'prop-types';
import { localize } from 'i18n-calypso';
import React from 'react';

/**
* Internal dependencies
*/
import SiteImporter from './site-importer/site-importer';

class ImporterSiteImporter extends React.PureComponent {
static displayName = 'ImporterSiteImporter';

importerData = {
title: 'Site Importer (Beta)',
icon: 'site-importer',
description: this.props.translate( 'Import posts, pages, and media from your existing site!' ),
uploadDescription: this.props.translate( 'Type your existing site URL to start the import.' ),
};

static propTypes = {
importerStatus: PropTypes.shape( {
filename: PropTypes.string,
importerState: PropTypes.string.isRequired,
errorData: PropTypes.shape( {
type: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
} ),
percentComplete: PropTypes.number,
siteTitle: PropTypes.string.isRequired,
statusMessage: PropTypes.string,
} ),
};

render() {
return <SiteImporter importerData={ this.importerData } { ...this.props } />;
}
}

export default localize( ImporterSiteImporter );
4 changes: 3 additions & 1 deletion client/my-sites/importer/importing-pane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,9 @@ class ImportingPane extends React.PureComponent {

const mapDispatchToProps = dispatch => ( {
mapAuthorFor: importerId => ( source, target ) =>
dispatch( mapAuthor( importerId, source, target ) ),
setTimeout( () => {
dispatch( mapAuthor( importerId, source, target ) );
}, 0 ),
} );

export default connectDispatcher( null, mapDispatchToProps )( localize( ImportingPane ) );
12 changes: 12 additions & 0 deletions client/my-sites/importer/site-importer/placeholder-logo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* External dependencies
*/
import React from 'react';

export default () => (
<svg
className="site-importer__placeholder-logo social-logo importer__service-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
/>
);
243 changes: 243 additions & 0 deletions client/my-sites/importer/site-importer/site-importer-input-pane.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/** @format */

/**
* External dependencies
*/
import Dispatcher from 'dispatcher';
import PropTypes from 'prop-types';
import { localize } from 'i18n-calypso';
import React from 'react';
import { noop, every, has, defer, get } from 'lodash';

/**
* Internal dependencies
*/
import wpLib from 'lib/wp';
const wpcom = wpLib.undocumented();

import { toApi, fromApi } from 'lib/importer/common';

import { startMappingAuthors, startImporting, mapAuthor, finishUpload } from 'lib/importer/actions';
import user from 'lib/user';

import { appStates } from 'state/imports/constants';
import Button from 'components/forms/form-button';
import ErrorPane from '../error-pane';
import TextInput from 'components/forms/form-text-input';

import SiteImporterSitePreview from './site-importer-site-preview';
import { connectDispatcher } from '../dispatcher-converter';

class SiteImporterInputPane extends React.Component {
static displayName = 'SiteImporterSitePreview';

static propTypes = {
description: PropTypes.oneOfType( [ PropTypes.node, PropTypes.string ] ),
importerStatus: PropTypes.shape( {
importerState: PropTypes.string.isRequired,
percentComplete: PropTypes.number,
} ),
onStartImport: PropTypes.func,
disabled: PropTypes.bool,
};

static defaultProps = { description: null, onStartImport: noop };

state = {
// TODO this is bad, make it better. Both appStates and state should be unified in one.
importStage: 'idle',
error: false,
errorMessage: '',
siteURLInput: '',
};

// TODO This can be improved if we move to Redux.
Copy link
Contributor

Choose a reason for hiding this comment

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

This fn looks awful with those defers but agreed that we can handle it better once switching to redux.

componentWillReceiveProps = nextProps => {
// TODO test on a site without posts
const newImporterState = nextProps.importerStatus.importerState;
const oldImporterState = this.props.importerStatus.importerState;
const singleAuthorSite = get( this.props.site, 'single_user_site', true );

if ( newImporterState !== oldImporterState && newImporterState === appStates.UPLOAD_SUCCESS ) {
// WXR was uploaded, map the authors
if ( singleAuthorSite ) {
defer( props => {
const currentUserData = user().get();
const currentUser = {
...currentUserData,
name: currentUserData.display_name,
};

const mappingFunction = this.props.mapAuthorFor( props.importerStatus.importerId );

// map all the authors to the current user
props.importerStatus.customData.sourceAuthors.forEach( author => {
mappingFunction( author, currentUser );
} );
}, nextProps );
} else {
defer( props => startMappingAuthors( props.importerStatus.importerId ), nextProps );
}

// Do not continue execution of the function as the rest should be executed on the next update.
return;
}

if ( singleAuthorSite && has( this.props, 'importerStatus.customData.sourceAuthors' ) ) {
// Authors have been mapped, start the import
const oldAuthors = every( this.props.importerStatus.customData.sourceAuthors, 'mappedTo' );
const newAuthors = every( nextProps.importerStatus.customData.sourceAuthors, 'mappedTo' );

if ( oldAuthors === false && newAuthors === true ) {
defer( props => {
startImporting( props.importerStatus );
}, nextProps );
}
}
};

resetErrors = () => {
this.setState( {
error: false,
errorMessage: '',
} );
};

setUrl = event => {
this.setState( { siteURLInput: event.target.value } );
};

validateSite = () => {
const siteURL = this.state.siteURLInput;

this.setState( { loading: true }, this.resetErrors );

wpcom.wpcom.req
.get( {
path: `/sites/${
this.props.site.ID
}/site-importer/is-site-importable?site_url=${ siteURL }`,
apiNamespace: 'wpcom/v2',
} )
.then( resp => {
this.setState( {
importStage: 'importable',
importData: {
title: resp.site_title,
supported: resp.supported_content,
unsupported: resp.unsupported_content,
favicon: resp.favicon,
engine: resp.engine,
},
loading: false,
importSiteURL: siteURL,
} );
} )
.catch( err => {
this.setState( {
loading: false,
error: true,
errorMessage: `${ err.message }`,
} );
} );
};

importSite = () => {
this.setState( { loading: true }, this.resetErrors );

wpcom.wpcom.req
.post( {
path: `/sites/${ this.props.site.ID }/site-importer/import-site`,
apiNamespace: 'wpcom/v2',
formData: [
[ 'import_status', JSON.stringify( toApi( this.props.importerStatus ) ) ],
[ 'site_url', this.state.importSiteURL ],
],
} )
.then( resp => {
this.setState( { loading: false } );

const data = fromApi( resp );
const action = finishUpload( this.props.importerStatus.importerId )( data );
defer( () => {
Dispatcher.handleViewAction( action );
} );
} )
.catch( err => {
this.setState( {
loading: false,
error: true,
errorMessage: `${ err.message } (${ err.code })`,
} );
} );
};

resetImport = () => {
this.setState(
{
loading: false,
importStage: 'idle',
},
this.resetErrors
);
};

render() {
return (
<div className="site-importer__site-importer-pane">
{ this.state.importStage === 'idle' && (
Copy link
Contributor

Choose a reason for hiding this comment

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

I think a slight improvement here would be to make these state labels constants, so we can compare like:
this.state.importStage === IDLE_STATE && etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be awesome to extract that while refactoring the import section, so we can unify it between all the importers.

<div>
<p>{ this.props.description }</p>
<div className="site-importer__site-importer-url-input">
<TextInput
disabled={ this.state.loading }
onChange={ this.setUrl }
value={ this.state.siteURLInput }
/>
<Button
disabled={ this.state.loading }
isPrimary={ true }
onClick={ this.validateSite }
>
{ this.props.translate( 'Continue' ) }
</Button>
</div>
</div>
) }
{ this.state.importStage === 'importable' && (
<div className="site-importer__site-importer-confirm-site-pane">
<SiteImporterSitePreview
siteURL={ this.state.importSiteURL }
importData={ this.state.importData }
isLoading={ this.state.loading }
/>
<div className="site-importer__site-importer-confirm-site-pane-container">
<Button disabled={ this.state.loading } onClick={ this.importSite }>
{ this.props.translate( 'Yes! Start import' ) }
</Button>
<Button
disabled={ this.state.loading }
isPrimary={ false }
onClick={ this.resetImport }
>
{ this.props.translate( 'No' ) }
</Button>
</div>
</div>
) }
{ this.state.error && (
<ErrorPane type="importError" description={ this.state.errorMessage } />
) }
</div>
);
}
}

const mapDispatchToProps = dispatch => ( {
mapAuthorFor: importerId => ( source, target ) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

I may be wrong, but would it not be worth creating a new action for this? It seems like a lot of logic for the mapDispatchToProps cb

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was the easiest way to achieve the functionality. I agree it would make more sense to extract is a separate action, but with the underlying Flux code, I don't think it makes sense to add more things there instead of focusing on migrating to Redux and improving the code here after that.

Copy link
Contributor

Choose a reason for hiding this comment

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

That makes sense :)

defer( () => {
dispatch( mapAuthor( importerId, source, target ) );
} ),
} );

export default connectDispatcher( null, mapDispatchToProps )( localize( SiteImporterInputPane ) );
Loading