Onboarding: Add create homepage logic to "Customize Appearance" step (https://github.com/woocommerce/woocommerce-admin/pull/2889)

This commit is contained in:
Joshua T Flowers 2019-09-06 22:18:44 +08:00 committed by GitHub
parent 5b0af85472
commit f095466442
8 changed files with 157 additions and 46 deletions

View File

@ -37,7 +37,7 @@ class TaskDashboard extends Component {
} }
getTasks() { getTasks() {
const { shippingZonesCount, tasks } = wcSettings.onboarding; const { customLogo, hasHomepage, hasProducts, shippingZonesCount } = wcSettings.onboarding;
const { profileItems, query } = this.props; const { profileItems, query } = this.props;
return [ return [
@ -61,7 +61,7 @@ class TaskDashboard extends Component {
'Add products manually, import from a sheet or migrate from another platform', 'Add products manually, import from a sheet or migrate from another platform',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
before: tasks.products ? ( before: hasProducts ? (
<i className="material-icons-outlined">check_circle</i> <i className="material-icons-outlined">check_circle</i>
) : ( ) : (
<i className="material-icons-outlined">add_box</i> <i className="material-icons-outlined">add_box</i>
@ -69,7 +69,7 @@ class TaskDashboard extends Component {
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
onClick: () => updateQueryString( { task: 'products' } ), onClick: () => updateQueryString( { task: 'products' } ),
container: <Products />, container: <Products />,
className: tasks.products ? 'is-complete' : null, className: hasProducts ? 'is-complete' : null,
visible: true, visible: true,
}, },
{ {
@ -80,6 +80,7 @@ class TaskDashboard extends Component {
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
onClick: () => updateQueryString( { task: 'appearance' } ), onClick: () => updateQueryString( { task: 'appearance' } ),
container: <Appearance />, container: <Appearance />,
className: customLogo && hasHomepage ? 'is-complete' : null,
visible: true, visible: true,
}, },
{ {

View File

@ -19,6 +19,7 @@ import { getHistory, getNewPath } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { WC_ADMIN_NAMESPACE } from 'wc-api/constants';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
class Appearance extends Component { class Appearance extends Component {
@ -26,8 +27,8 @@ class Appearance extends Component {
super( props ); super( props );
this.stepVisibility = { this.stepVisibility = {
homepage: ! wcSettings.onboarding.hasHomepage,
import: ! wcSettings.onboarding.hasProducts, import: ! wcSettings.onboarding.hasProducts,
logo: ! wcSettings.onboarding.customLogo,
}; };
this.state = { this.state = {
@ -38,6 +39,7 @@ class Appearance extends Component {
}; };
this.completeStep = this.completeStep.bind( this ); this.completeStep = this.completeStep.bind( this );
this.createHomepage = this.createHomepage.bind( this );
this.importProducts = this.importProducts.bind( this ); this.importProducts = this.importProducts.bind( this );
this.updateLogo = this.updateLogo.bind( this ); this.updateLogo = this.updateLogo.bind( this );
this.updateNotice = this.updateNotice.bind( this ); this.updateNotice = this.updateNotice.bind( this );
@ -69,6 +71,7 @@ class Appearance extends Component {
if ( 'logo' === step && isRequestSuccessful ) { if ( 'logo' === step && isRequestSuccessful ) {
createNotice( 'success', __( 'Store logo updated sucessfully.', 'woocommerce-admin' ) ); createNotice( 'success', __( 'Store logo updated sucessfully.', 'woocommerce-admin' ) );
this.completeStep(); this.completeStep();
wcSettings.onboarding.customLogo = themeMods.custom_logo ? true : false;
} }
if ( 'notice' === step && isRequestSuccessful ) { if ( 'notice' === step && isRequestSuccessful ) {
@ -95,7 +98,10 @@ class Appearance extends Component {
const { createNotice } = this.props; const { createNotice } = this.props;
this.setState( { isPending: true } ); this.setState( { isPending: true } );
apiFetch( { path: '/wc-admin/v1/onboarding/tasks/import_sample_products', method: 'POST' } ) apiFetch( {
path: `${ WC_ADMIN_NAMESPACE }/onboarding/tasks/import_sample_products`,
method: 'POST',
} )
.then( result => { .then( result => {
if ( result.failed && result.failed.length ) { if ( result.failed && result.failed.length ) {
createNotice( createNotice(
@ -119,12 +125,32 @@ class Appearance extends Component {
} ); } );
} }
createHomepage() {
const { createNotice } = this.props;
this.setState( { isPending: true } );
apiFetch( { path: '/wc-admin/v1/onboarding/tasks/create_homepage', method: 'POST' } )
.then( response => {
createNotice( response.status, response.message );
this.setState( { isPending: false } );
if ( response.edit_post_link ) {
window.location = `${ response.edit_post_link }&wc_onboarding_active_task=homepage`;
}
} )
.catch( error => {
createNotice( 'error', error.message );
this.setState( { isPending: false } );
} );
}
updateLogo() { updateLogo() {
const { options, themeMods, updateOptions } = this.props; const { options, themeMods, updateOptions } = this.props;
const { logo } = this.state; const { logo } = this.state;
const updateThemeMods = logo ? { ...themeMods, custom_logo: logo.id } : themeMods;
updateOptions( { updateOptions( {
[ `theme_mods_${ options.stylesheet }` ]: { ...themeMods, custom_logo: logo.id }, [ `theme_mods_${ options.stylesheet }` ]: updateThemeMods,
} ); } );
} }
@ -171,13 +197,15 @@ class Appearance extends Component {
), ),
content: ( content: (
<Fragment> <Fragment>
<Button isPrimary>{ __( 'Create homepage', 'woocommerce-admin' ) }</Button> <Button isPrimary onClick={ this.createHomepage }>
{ __( 'Create homepage', 'woocommerce-admin' ) }
</Button>
<Button onClick={ () => this.completeStep() }> <Button onClick={ () => this.completeStep() }>
{ __( 'Skip', 'woocommerce-admin' ) } { __( 'Skip', 'woocommerce-admin' ) }
</Button> </Button>
</Fragment> </Fragment>
), ),
visible: true, visible: this.stepVisibility.homepage,
}, },
{ {
key: 'logo', key: 'logo',
@ -194,7 +222,7 @@ class Appearance extends Component {
</Button> </Button>
</Fragment> </Fragment>
), ),
visible: this.stepVisibility.logo, visible: true,
}, },
{ {
key: 'notice', key: 'notice',
@ -244,7 +272,7 @@ class Appearance extends Component {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getOptions, getOptionsError, isOptionsRequesting } = select( 'wc-api' ); const { getOptions, getOptionsError, isUpdateOptionsRequesting } = select( 'wc-api' );
const options = getOptions( [ const options = getOptions( [
'woocommerce_demo_store', 'woocommerce_demo_store',
@ -252,10 +280,7 @@ export default compose(
'stylesheet', 'stylesheet',
] ); ] );
const themeModsName = `theme_mods_${ options.stylesheet }`; const themeModsName = `theme_mods_${ options.stylesheet }`;
const themeOptions = const themeOptions = options.stylesheet ? getOptions( [ themeModsName ] ) : null;
options.stylesheet && ! wcSettings.onboarding.customLogo
? getOptions( [ themeModsName ] )
: null;
const themeMods = const themeMods =
themeOptions && themeOptions[ themeModsName ] ? themeOptions[ themeModsName ] : {}; themeOptions && themeOptions[ themeModsName ] ? themeOptions[ themeModsName ] : {};
@ -273,9 +298,9 @@ export default compose(
} }
const hasErrors = Boolean( errors.length ); const hasErrors = Boolean( errors.length );
const isRequesting = const isRequesting =
Boolean( isOptionsRequesting( [ themeModsName ] ) ) || Boolean( isUpdateOptionsRequesting( [ themeModsName ] ) ) ||
Boolean( Boolean(
isOptionsRequesting( [ 'woocommerce_demo_store', 'woocommerce_demo_store_notice' ] ) isUpdateOptionsRequesting( [ 'woocommerce_demo_store', 'woocommerce_demo_store_notice' ] )
); );
return { errors, getOptionsError, hasErrors, isRequesting, options, themeMods }; return { errors, getOptionsError, hasErrors, isRequesting, options, themeMods };

View File

@ -6,7 +6,7 @@
import { getResourceName } from '../utils'; import { getResourceName } from '../utils';
const updateOptions = operations => options => { const updateOptions = operations => options => {
const resourceName = getResourceName( 'options', Object.keys( options ) ); const resourceName = getResourceName( 'options-update', Object.keys( options ) );
operations.update( [ resourceName ], { [ resourceName ]: options } ); operations.update( [ resourceName ], { [ resourceName ]: options } );
}; };

View File

@ -40,21 +40,24 @@ function updateOptions( resourceNames, data, fetch ) {
const url = WC_ADMIN_NAMESPACE + '/options'; const url = WC_ADMIN_NAMESPACE + '/options';
const filteredNames = resourceNames.filter( name => { const filteredNames = resourceNames.filter( name => {
return name.startsWith( 'options' ); return name.startsWith( 'options-update' );
} ); } );
return filteredNames.map( async resourceName => { return filteredNames.map( async resourceName => {
return fetch( { path: url, method: 'POST', data: data[ resourceName ] } ) return fetch( { path: url, method: 'POST', data: data[ resourceName ] } )
.then( () => optionsToResource( data[ resourceName ] ) ) .then( () => optionsToResource( data[ resourceName ], true ) )
.catch( error => { .catch( error => {
return { [ resourceName ]: { error } }; return { [ resourceName ]: { error } };
} ); } );
} ); } );
} }
function optionsToResource( options ) { function optionsToResource( options, updateResource = false ) {
const optionNames = Object.keys( options ); const optionNames = Object.keys( options );
const resourceName = getResourceName( 'options', optionNames ); const resourceName = getResourceName(
updateResource ? 'options-update' : 'options',
optionNames
);
const resources = {}; const resources = {};
optionNames.forEach( optionNames.forEach(

View File

@ -31,7 +31,7 @@ const getOptionsError = getResource => optionNames => {
return getResource( getResourceName( 'options', optionNames ) ).error; return getResource( getResourceName( 'options', optionNames ) ).error;
}; };
const isOptionsRequesting = getResource => optionNames => { const isGetOptionsRequesting = getResource => optionNames => {
const { lastReceived, lastRequested } = getResource( getResourceName( 'options', optionNames ) ); const { lastReceived, lastRequested } = getResource( getResourceName( 'options', optionNames ) );
if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) { if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) {
@ -41,8 +41,21 @@ const isOptionsRequesting = getResource => optionNames => {
return lastRequested > lastReceived; return lastRequested > lastReceived;
}; };
const isUpdateOptionsRequesting = getResource => optionNames => {
const { lastReceived, lastRequested } = getResource(
getResourceName( 'options-update', optionNames )
);
if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default { export default {
getOptions, getOptions,
getOptionsError, getOptionsError,
isOptionsRequesting, isGetOptionsRequesting,
isUpdateOptionsRequesting,
}; };

View File

@ -49,6 +49,19 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
'schema' => array( $this, 'get_public_item_schema' ), 'schema' => array( $this, 'get_public_item_schema' ),
) )
); );
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/create_homepage',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_homepage' ),
'permission_callback' => array( $this, 'create_homepage_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
} }
/** /**
@ -65,6 +78,20 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
return true; return true;
} }
/**
* Check if a given request has access to create a product.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function create_homepage_permission_check( $request ) {
if ( ! wc_rest_check_post_permissions( 'page', 'create' ) || ! current_user_can( 'manage_options' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create a new homepage.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/** /**
* Import sample products from WooCommerce sample CSV. * Import sample products from WooCommerce sample CSV.
* *
@ -134,4 +161,32 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
public static function sanitize_special_column_name_regex( $value ) { public static function sanitize_special_column_name_regex( $value ) {
return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/'; return '/' . str_replace( array( '%d', '%s' ), '(.*)', trim( quotemeta( $value ) ) ) . '/';
} }
/**
* Create a homepage from a template.
*/
public static function create_homepage() {
$post_id = wp_insert_post(
array(
'post_title' => __( 'Homepage', 'woocommerce-admin' ),
'post_type' => 'page',
'post_status' => 'draft',
// @todo The images in this content should be replaced with working external links or imported.
'post_content' => "<!-- wp:cover {\"url\":\"https://local.wordpress.test/wp-content/uploads/2019/05/parallax.jpeg\",\"id\":3624} -->\n<div class=\"wp-block-cover has-background-dim\" style=\"background-image:url(https://local.wordpress.test/wp-content/uploads/2019/05/parallax.jpeg)\"><div class=\"wp-block-cover__inner-container\"><!-- wp:paragraph {\"align\":\"center\",\"placeholder\":\"Write title…\",\"fontSize\":\"large\"} -->\n<p style=\"text-align:center\" class=\"has-large-font-size\">Welcome to the store</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p style=\"text-align:center\">Write a short welcome message here</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:button {\"align\":\"center\"} -->\n<div class=\"wp-block-button aligncenter\"><a class=\"wp-block-button__link\">Go shopping</a></div>\n<!-- /wp:button --></div></div>\n<!-- /wp:cover -->\n\n<!-- wp:heading {\"align\":\"center\"} -->\n<h2 style=\"text-align:center\">New products</h2>\n<!-- /wp:heading -->\n\n<!-- wp:woocommerce/product-new /-->\n\n<!-- wp:media-text {\"align\":\"\",\"backgroundColor\":\"light-gray\",\"mediaPosition\":\"right\",\"mediaId\":1257,\"mediaType\":\"image\"} -->\n<div class=\"wp-block-media-text has-media-on-the-right has-light-gray-background-color\"><figure class=\"wp-block-media-text__media\"><img src=\"https://local.wordpress.test/wp-content/uploads/2017/05/brady-bellini-191086-1024x616.jpg\" alt=\"\" class=\"wp-image-1257\"/></figure><div class=\"wp-block-media-text__content\"><!-- wp:paragraph {\"align\":\"center\",\"placeholder\":\"Content…\",\"fontSize\":\"large\"} -->\n<p style=\"text-align:center\" class=\"has-large-font-size\">Here's a business goal</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p style=\"text-align:center\">Describe your business aspiration here.</p>\n<!-- /wp:paragraph --></div></div>\n<!-- /wp:media-text -->\n\n<!-- wp:media-text {\"align\":\"\",\"backgroundColor\":\"light-gray\",\"mediaId\":1257,\"mediaType\":\"image\"} -->\n<div class=\"wp-block-media-text has-light-gray-background-color\"><figure class=\"wp-block-media-text__media\"><img src=\"https://local.wordpress.test/wp-content/uploads/2017/05/brady-bellini-191086-1024x616.jpg\" alt=\"\" class=\"wp-image-1257\"/></figure><div class=\"wp-block-media-text__content\"><!-- wp:paragraph {\"align\":\"center\",\"placeholder\":\"Content…\",\"fontSize\":\"large\"} -->\n<p style=\"text-align:center\" class=\"has-large-font-size\">Another business goal</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p style=\"text-align:center\">Describe your business aspiration here.</p>\n<!-- /wp:paragraph --></div></div>\n<!-- /wp:media-text -->\n\n<!-- wp:media-text {\"align\":\"\",\"backgroundColor\":\"light-gray\",\"mediaPosition\":\"right\",\"mediaId\":1257,\"mediaType\":\"image\"} -->\n<div class=\"wp-block-media-text has-media-on-the-right has-light-gray-background-color\"><figure class=\"wp-block-media-text__media\"><img src=\"https://local.wordpress.test/wp-content/uploads/2017/05/brady-bellini-191086-1024x616.jpg\" alt=\"\" class=\"wp-image-1257\"/></figure><div class=\"wp-block-media-text__content\"><!-- wp:paragraph {\"align\":\"center\",\"placeholder\":\"Content…\",\"fontSize\":\"large\"} -->\n<p style=\"text-align:center\" class=\"has-large-font-size\">A final business goal</p>\n<!-- /wp:paragraph -->\n\n<!-- wp:paragraph {\"align\":\"center\"} -->\n<p style=\"text-align:center\">Describe your business aspiration here.</p>\n<!-- /wp:paragraph --></div></div>\n<!-- /wp:media-text -->\n\n<!-- wp:woocommerce/featured-product {\"editMode\":false,\"productId\":2567} -->\n<!-- wp:button {\"align\":\"center\"} -->\n<div class=\"wp-block-button aligncenter\"><a class=\"wp-block-button__link\" href=\"https://local.wordpress.test/shop/decor/wordpress-pennant\">Shop now</a></div>\n<!-- /wp:button -->\n<!-- /wp:woocommerce/featured-product -->"
)
);
if ( ! is_wp_error( $post_id ) ) {
update_option( 'woocommerce_onboarding_homepage_post_id', $post_id );
return array(
'status' => 'success',
'message' => __( 'Homepage created successfully.', 'woocommerce-admin' ),
'post_id' => $post_id,
'edit_post_link' => htmlspecialchars_decode( get_edit_post_link( $post_id ) ),
);
} else {
return $post_id;
}
}
} }

View File

@ -26,13 +26,6 @@ class OnboardingTasks {
*/ */
const ACTIVE_TASK_TRANSIENT = 'wc_onboarding_active_task'; const ACTIVE_TASK_TRANSIENT = 'wc_onboarding_active_task';
/**
* Name of the tasks transient.
*
* @var string
*/
const TASKS_TRANSIENT = 'wc_onboarding_tasks';
/** /**
* Get class instance. * Get class instance.
*/ */
@ -50,7 +43,7 @@ class OnboardingTasks {
add_action( 'admin_enqueue_scripts', array( $this, 'add_media_scripts' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'add_media_scripts' ) );
add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 30 ); // Run after Onboarding. add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 30 ); // Run after Onboarding.
add_action( 'admin_init', array( $this, 'set_active_task' ), 20 ); add_action( 'admin_init', array( $this, 'set_active_task' ), 20 );
add_action( 'admin_init', array( $this, 'check_active_task_completion' ), 1 ); add_action( 'current_screen', array( $this, 'check_active_task_completion' ), 1000 );
} }
/** /**
@ -66,26 +59,14 @@ class OnboardingTasks {
* @param array $settings Component settings. * @param array $settings Component settings.
*/ */
public function component_settings( $settings ) { public function component_settings( $settings ) {
$tasks = get_transient( self::TASKS_TRANSIENT );
$products = wp_count_posts( 'product' ); $products = wp_count_posts( 'product' );
if ( ! $tasks ) {
$tasks = array();
$task_list = array( 'products' );
foreach ( $task_list as $task ) {
$tasks[ $task ] = self::check_task_completion( $task );
}
set_transient( self::TASKS_TRANSIENT, $tasks, DAY_IN_SECONDS );
}
// @todo We may want to consider caching some of these and use to check against // @todo We may want to consider caching some of these and use to check against
// task completion along with cache busting for active tasks. // task completion along with cache busting for active tasks.
$settings['onboarding']['automatedTaxSupportedCountries'] = self::get_automated_tax_supported_countries(); $settings['onboarding']['automatedTaxSupportedCountries'] = self::get_automated_tax_supported_countries();
$settings['onboarding']['customLogo'] = get_theme_mod( 'custom_logo', false ); $settings['onboarding']['customLogo'] = get_theme_mod( 'custom_logo', false );
$settings['onboarding']['hasProducts'] = (int) $products->publish > 0 || (int) $products->draft > 0; $settings['onboarding']['hasHomepage'] = self::check_task_completion( 'homepage' );
$settings['onboarding']['tasks'] = $tasks; $settings['onboarding']['hasProducts'] = self::check_task_completion( 'products' );
$settings['onboarding']['shippingZonesCount'] = count( \WC_Shipping_Zones::get_zones() ); $settings['onboarding']['shippingZonesCount'] = count( \WC_Shipping_Zones::get_zones() );
return $settings; return $settings;
@ -122,7 +103,6 @@ class OnboardingTasks {
if ( self::check_task_completion( $active_task ) ) { if ( self::check_task_completion( $active_task ) ) {
delete_transient( self::ACTIVE_TASK_TRANSIENT ); delete_transient( self::ACTIVE_TASK_TRANSIENT );
delete_transient( self::TASKS_TRANSIENT );
wp_safe_redirect( wc_admin_url() ); wp_safe_redirect( wc_admin_url() );
exit; exit;
} }
@ -139,6 +119,24 @@ class OnboardingTasks {
case 'products': case 'products':
$products = wp_count_posts( 'product' ); $products = wp_count_posts( 'product' );
return (int) $products->publish > 0 || (int) $products->draft > 0; return (int) $products->publish > 0 || (int) $products->draft > 0;
case 'homepage':
// @todo This should be run client-side in a Gutenberg hook and add a notice
// to return to the task list if complete.
$homepage_id = get_option( 'woocommerce_onboarding_homepage_post_id', false );
if ( ! $homepage_id ) {
return false;
}
$post = get_post( $homepage_id );
$completed = $post && 'publish' === $post->post_status;
if ( $completed ) {
update_option( 'show_on_front', 'page' );
update_option( 'page_on_front', $homepage_id );
}
return $completed;
} }
return false; return false;

View File

@ -49,4 +49,20 @@ class WC_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
$this->assertArrayHasKey( 'skipped', $data ); $this->assertArrayHasKey( 'skipped', $data );
$this->assertArrayHasKey( 'updated', $data ); $this->assertArrayHasKey( 'updated', $data );
} }
/**
* Test that Tasks data is returned by the endpoint.
*/
public function test_create_homepage() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/create_homepage' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertEquals( 'success', $data['status'] );
$this->assertEquals( get_option( 'woocommerce_onboarding_homepage_post_id' ), $data['post_id'] );
$this->assertEquals( htmlspecialchars_decode( get_edit_post_link( get_option( 'woocommerce_onboarding_homepage_post_id' ) ) ), $data['edit_post_link'] );
}
} }