* Add initial Customize Appearance task

* Add options wc endpoint

* Add wc-api specs to manage options
This commit is contained in:
Joshua T Flowers 2019-09-02 11:45:56 +08:00 committed by GitHub
parent e156605bfa
commit 7bce0b710f
15 changed files with 586 additions and 16 deletions

View File

@ -4,7 +4,7 @@
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { filter, noop } from 'lodash';
import { filter } from 'lodash';
import { compose } from '@wordpress/compose';
/**
@ -17,6 +17,7 @@ import { updateQueryString } from '@woocommerce/navigation';
* Internal depdencies
*/
import './style.scss';
import Appearance from './tasks/appearance';
import Connect from './tasks/connect';
import Products from './tasks/products';
import Shipping from './tasks/shipping';
@ -72,12 +73,13 @@ class TaskDashboard extends Component {
visible: true,
},
{
key: 'personalize-store',
key: 'appearance',
title: __( 'Personalize your store', 'woocommerce-admin' ),
content: __( 'Create a custom homepage and upload your logo', 'wooocommerce-admin' ),
before: <i className="material-icons-outlined">palette</i>,
after: <i className="material-icons-outlined">chevron_right</i>,
onClick: noop,
onClick: () => updateQueryString( { task: 'appearance' } ),
container: <Appearance />,
visible: true,
},
{

View File

@ -225,3 +225,9 @@
}
}
}
.woocommerce-task-appearance {
.muriel-image-upload {
margin-bottom: $gap-smallest;
}
}

View File

@ -0,0 +1,254 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, ImageUpload, TextControl } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { difference, filter } from 'lodash';
import { withDispatch } from '@wordpress/data';
/**
* WooCommerce dependencies
*/
import { Card, Stepper } from '@woocommerce/components';
import { getHistory, getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import withSelect from 'wc-api/with-select';
class Appearance extends Component {
constructor( props ) {
super( props );
this.state = {
logo: null,
stepIndex: 0,
storeNoticeText: props.options.woocommerce_demo_store_notice || '',
};
this.completeStep = this.completeStep.bind( this );
this.updateLogo = this.updateLogo.bind( this );
this.updateNotice = this.updateNotice.bind( this );
}
async componentDidUpdate( prevProps ) {
const { stepIndex } = this.state;
const { createNotice, errors, hasErrors, isRequesting, options, themeMods } = this.props;
const step = this.getSteps()[ stepIndex ].key;
const isRequestSuccessful = ! isRequesting && prevProps.isRequesting && ! hasErrors;
if ( themeMods && prevProps.themeMods.custom_logo !== themeMods.custom_logo ) {
await wp.media.attachment( themeMods.custom_logo ).fetch();
const logoUrl = wp.media.attachment( themeMods.custom_logo ).get( 'url' );
/* eslint-disable react/no-did-update-set-state */
this.setState( { logo: { id: themeMods.custom_logo, url: logoUrl } } );
/* eslint-enable react/no-did-update-set-state */
}
if (
options.woocommerce_demo_store_notice &&
prevProps.options.woocommerce_demo_store_notice !== options.woocommerce_demo_store_notice
) {
/* eslint-disable react/no-did-update-set-state */
this.setState( { storeNoticeText: options.woocommerce_demo_store_notice } );
/* eslint-enable react/no-did-update-set-state */
}
if ( 'logo' === step && isRequestSuccessful ) {
createNotice( 'success', __( 'Store logo updated sucessfully.', 'woocommerce-admin' ) );
this.completeStep();
}
if ( 'notice' === step && isRequestSuccessful ) {
createNotice( 'success', __( 'Store notice updated sucessfully.', 'woocommerce-admin' ) );
this.completeStep();
}
const newErrors = difference( errors, prevProps.errors );
newErrors.map( error => createNotice( 'error', error ) );
}
completeStep() {
const { stepIndex } = this.state;
const nextStep = this.getSteps()[ stepIndex + 1 ];
if ( nextStep ) {
this.setState( { stepIndex: stepIndex + 1 } );
} else {
getHistory().push( getNewPath( {}, '/', {} ) );
}
}
updateLogo() {
const { options, themeMods, updateOptions } = this.props;
const { logo } = this.state;
updateOptions( {
[ `theme_mods_${ options.stylesheet }` ]: { ...themeMods, custom_logo: logo.id },
} );
}
updateNotice() {
const { updateOptions } = this.props;
const { storeNoticeText } = this.state;
updateOptions( {
woocommerce_demo_store: storeNoticeText.length ? 'yes' : 'no',
woocommerce_demo_store_notice: storeNoticeText,
} );
}
getSteps() {
const { logo, storeNoticeText } = this.state;
const { isRequesting } = this.props;
const steps = [
{
key: 'import',
label: __( 'Import demo products', 'woocommerce-admin' ),
description: __(
'Well add some products that it will make it easier to see what your store looks like.',
'woocommerce-admin'
),
content: (
<Fragment>
<Button isPrimary>{ __( 'Import products', 'woocommerce-admin' ) }</Button>
<Button onClick={ () => this.completeStep() }>
{ __( 'Skip', 'woocommerce-admin' ) }
</Button>
</Fragment>
),
visible: true,
},
{
key: 'homepage',
label: __( 'Create a custom homepage', 'woocommerce-admin' ),
description: __(
'Create a new homepage and customize it to suit your needs',
'woocommerce-admin'
),
content: (
<Fragment>
<Button isPrimary>{ __( 'Create homepage', 'woocommerce-admin' ) }</Button>
<Button onClick={ () => this.completeStep() }>
{ __( 'Skip', 'woocommerce-admin' ) }
</Button>
</Fragment>
),
visible: true,
},
{
key: 'logo',
label: __( 'Upload a logo', 'woocommerce-admin' ),
description: __( 'Ensure your store is on-brand by adding your logo', 'woocommerce-admin' ),
content: (
<Fragment>
<ImageUpload image={ logo } onChange={ image => this.setState( { logo: image } ) } />
<Button onClick={ this.updateLogo } isBusy={ isRequesting } isPrimary>
{ __( 'Proceed', 'woocommerce-admin' ) }
</Button>
<Button onClick={ () => this.completeStep() }>
{ __( 'Skip', 'woocommerce-admin' ) }
</Button>
</Fragment>
),
visible: ! wcSettings.onboarding.customLogo,
},
{
key: 'notice',
label: __( 'Set a store notice', 'woocommerce-admin' ),
description: __(
'Optionally display a prominent notice across all pages of your store',
'woocommerce-admin'
),
content: (
<Fragment>
<TextControl
label={ __( 'Store notice text', 'woocommerce-admin' ) }
placeholder={ __( 'Store notice text', 'woocommerce-admin' ) }
value={ storeNoticeText }
onChange={ value => this.setState( { storeNoticeText: value } ) }
/>
<Button onClick={ this.updateNotice } isPrimary>
{ __( 'Complete task', 'woocommerce-admin' ) }
</Button>
</Fragment>
),
visible: true,
},
];
return filter( steps, step => step.visible );
}
render() {
const { stepIndex } = this.state;
const { isRequesting, hasErrors } = this.props;
return (
<div className="woocommerce-task-appearance">
<Card className="is-narrow">
<Stepper
isPending={ isRequesting && ! hasErrors }
isVertical
currentStep={ this.getSteps()[ stepIndex ].key }
steps={ this.getSteps() }
/>
</Card>
</div>
);
}
}
export default compose(
withSelect( select => {
const { getOptions, getOptionsError, isOptionsRequesting } = select( 'wc-api' );
const options = getOptions( [
'woocommerce_demo_store',
'woocommerce_demo_store_notice',
'stylesheet',
] );
const themeModsName = `theme_mods_${ options.stylesheet }`;
const themeOptions =
options.stylesheet && ! wcSettings.onboarding.customLogo
? getOptions( [ themeModsName ] )
: null;
const themeMods =
themeOptions && themeOptions[ themeModsName ] ? themeOptions[ themeModsName ] : {};
const errors = [];
const uploadLogoError = getOptionsError( [ themeModsName ] );
const storeNoticeError = getOptionsError( [
'woocommerce_demo_store',
'woocommerce_demo_store_notice',
] );
if ( uploadLogoError ) {
errors.push( uploadLogoError.message );
}
if ( storeNoticeError ) {
errors.push( storeNoticeError.message );
}
const hasErrors = Boolean( errors.length );
const isRequesting =
Boolean( isOptionsRequesting( [ themeModsName ] ) ) ||
Boolean(
isOptionsRequesting( [ 'woocommerce_demo_store', 'woocommerce_demo_store_notice' ] )
);
return { errors, getOptionsError, hasErrors, isRequesting, options, themeMods };
} ),
withDispatch( dispatch => {
const { createNotice } = dispatch( 'core/notices' );
const { updateOptions } = dispatch( 'wc-api' );
return {
createNotice,
updateOptions,
};
} )
)( Appearance );

View File

@ -4,7 +4,9 @@
*/
import { MINUTE } from '@fresh-data/framework';
export const JETPACK_NAMESPACE = '/jetpack/v4';
export const NAMESPACE = '/wc/v4';
export const WC_ADMIN_NAMESPACE = '/wc-admin/v1';
export const DEFAULT_REQUIREMENT = {
timeout: 1 * MINUTE,

View File

@ -4,12 +4,6 @@
*/
import { __ } from '@wordpress/i18n';
/**
* Onboarding namespace.
*/
export const NAMESPACE = '/wc-admin/v1';
export const JETPACK_NAMESPACE = '/jetpack/v4';
/**
* Plugin slugs and names as key/value pairs.
*/

View File

@ -10,7 +10,8 @@ import apiFetch from '@wordpress/api-fetch';
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { JETPACK_NAMESPACE, NAMESPACE, pluginNames } from './constants';
import { JETPACK_NAMESPACE, WC_ADMIN_NAMESPACE } from '../constants';
import { pluginNames } from './constants';
function read( resourceNames, fetch = apiFetch ) {
return [
@ -33,7 +34,7 @@ function readProfileItems( resourceNames, fetch ) {
const resourceName = 'onboarding-profile';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/profile';
const url = WC_ADMIN_NAMESPACE + '/onboarding/profile';
return [
fetch( { path: url } )
@ -51,7 +52,7 @@ function updateProfileItems( resourceNames, data, fetch ) {
const resourceName = 'onboarding-profile';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/profile';
const url = WC_ADMIN_NAMESPACE + '/onboarding/profile';
return [
fetch( {
@ -103,7 +104,7 @@ function profileItemToResource( items ) {
function readActivePlugins( resourceNames, fetch ) {
const resourceName = 'active-plugins';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/plugins/active';
const url = WC_ADMIN_NAMESPACE + '/onboarding/plugins/active';
return [
fetch( { path: url } )
@ -131,7 +132,7 @@ function activatePlugins( resourceNames, data, fetch ) {
const resourceName = 'plugin-activate';
if ( resourceNames.includes( resourceName ) ) {
const plugins = data[ resourceName ];
const url = NAMESPACE + '/onboarding/plugins/activate';
const url = WC_ADMIN_NAMESPACE + '/onboarding/plugins/activate';
return [
fetch( {
path: url,
@ -197,7 +198,7 @@ function readJetpackConnectUrl( resourceNames, fetch ) {
const resourceName = 'jetpack-connect-url';
if ( resourceNames.includes( resourceName ) ) {
const url = NAMESPACE + '/onboarding/plugins/connect-jetpack';
const url = WC_ADMIN_NAMESPACE + '/onboarding/plugins/connect-jetpack';
return [
fetch( {
@ -245,7 +246,7 @@ function installPlugins( resourceNames, data, fetch ) {
return plugins.map( async plugin => {
return fetch( {
path: `${ NAMESPACE }/onboarding/plugins/install`,
path: `${ WC_ADMIN_NAMESPACE }/onboarding/plugins/install`,
method: 'POST',
data: {
plugin,

View File

@ -0,0 +1,13 @@
/** @format */
/**
* Internal dependencies
*/
import operations from './operations';
import selectors from './selectors';
import mutations from './mutations';
export default {
operations,
selectors,
mutations,
};

View File

@ -0,0 +1,15 @@
/** @format */
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
const updateOptions = operations => options => {
const resourceName = getResourceName( 'options', Object.keys( options ) );
operations.update( [ resourceName ], { [ resourceName ]: options } );
};
export default {
updateOptions,
};

View File

@ -0,0 +1,76 @@
/** @format */
/**
* External dependencies
*/
import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { getResourceIdentifier, getResourceName } from '../utils';
import { WC_ADMIN_NAMESPACE } from '../constants';
function read( resourceNames, fetch = apiFetch ) {
return [ ...readOptions( resourceNames, fetch ) ];
}
function update( resourceNames, data, fetch = apiFetch ) {
return [ ...updateOptions( resourceNames, data, fetch ) ];
}
function readOptions( resourceNames, fetch ) {
const filteredNames = resourceNames.filter( name => {
return name.startsWith( 'options' );
} );
return filteredNames.map( async resourceName => {
const optionNames = getResourceIdentifier( resourceName );
const url = WC_ADMIN_NAMESPACE + '/options?options=' + optionNames.join( ',' );
return fetch( { path: url } )
.then( optionsToResource )
.catch( error => {
return { [ resourceName ]: { error: String( error.message ) } };
} );
} );
}
function updateOptions( resourceNames, data, fetch ) {
const url = WC_ADMIN_NAMESPACE + '/options';
const filteredNames = resourceNames.filter( name => {
return name.startsWith( 'options' );
} );
return filteredNames.map( async resourceName => {
return fetch( { path: url, method: 'POST', data: data[ resourceName ] } )
.then( () => optionsToResource( data[ resourceName ] ) )
.catch( error => {
return { [ resourceName ]: { error } };
} );
} );
}
function optionsToResource( options ) {
const optionNames = Object.keys( options );
const resourceName = getResourceName( 'options', optionNames );
const resources = {};
optionNames.forEach(
optionName =>
( resources[ getResourceName( 'options', optionName ) ] = { data: options[ optionName ] } )
);
return {
[ resourceName ]: {
data: optionNames,
},
...resources,
};
}
export default {
read,
update,
};

View File

@ -0,0 +1,48 @@
/** @format */
/**
* External dependencies
*/
import { isNil } from 'lodash';
/**
* Internal dependencies
*/
import { DEFAULT_REQUIREMENT } from '../constants';
import { getResourceName } from '../utils';
const getOptions = ( getResource, requireResource ) => (
optionNames,
requirement = DEFAULT_REQUIREMENT
) => {
const resourceName = getResourceName( 'options', optionNames );
const options = {};
const names = requireResource( requirement, resourceName ).data || [];
names.forEach( name => {
options[ name ] = getResource( getResourceName( 'options', name ) ).data;
} );
return options;
};
const getOptionsError = getResource => optionNames => {
return getResource( getResourceName( 'options', optionNames ) ).error;
};
const isOptionsRequesting = getResource => optionNames => {
const { lastReceived, lastRequested } = getResource( getResourceName( 'options', optionNames ) );
if ( ! isNil( lastRequested ) && isNil( lastReceived ) ) {
return true;
}
return lastRequested > lastReceived;
};
export default {
getOptions,
getOptionsError,
isOptionsRequesting,
};

View File

@ -7,6 +7,7 @@ import items from './items';
import imports from './imports';
import notes from './notes';
import onboarding from './onboarding';
import options from './options';
import reportItems from './reports/items';
import reportStats from './reports/stats';
import reviews from './reviews';
@ -20,6 +21,7 @@ function createWcApiSpec() {
...items.mutations,
...notes.mutations,
...onboarding.mutations,
...options.mutations,
...settings.mutations,
...user.mutations,
},
@ -28,6 +30,7 @@ function createWcApiSpec() {
...items.selectors,
...notes.selectors,
...onboarding.selectors,
...options.selectors,
...reportItems.selectors,
...reportStats.selectors,
...reviews.selectors,
@ -46,6 +49,7 @@ function createWcApiSpec() {
...items.operations.read( resourceNames ),
...notes.operations.read( resourceNames ),
...onboarding.operations.read( resourceNames ),
...options.operations.read( resourceNames ),
...reportItems.operations.read( resourceNames ),
...reportStats.operations.read( resourceNames ),
...reviews.operations.read( resourceNames ),
@ -58,6 +62,7 @@ function createWcApiSpec() {
...items.operations.update( resourceNames, data ),
...notes.operations.update( resourceNames, data ),
...onboarding.operations.update( resourceNames, data ),
...options.operations.update( resourceNames, data ),
...settings.operations.update( resourceNames, data ),
...user.operations.update( resourceNames, data ),
];

View File

@ -43,6 +43,7 @@ class Init {
'Automattic\WooCommerce\Admin\API\DataCountries',
'Automattic\WooCommerce\Admin\API\DataDownloadIPs',
'Automattic\WooCommerce\Admin\API\Leaderboards',
'Automattic\WooCommerce\Admin\API\Options',
'Automattic\WooCommerce\Admin\API\Orders',
'Automattic\WooCommerce\Admin\API\Products',
'Automattic\WooCommerce\Admin\API\ProductCategories',

View File

@ -0,0 +1,143 @@
<?php
/**
* REST API Options Controller
*
* Handles requests to get and update options in the wp_options table.
*
* @package WooCommerce Admin/API
*/
namespace Automattic\WooCommerce\Admin\API;
defined( 'ABSPATH' ) || exit;
/**
* Options Controller.
*
* @package WooCommerce Admin/API
* @extends WC_REST_Data_Controller
*/
class Options extends \WC_REST_Data_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'wc-admin/v1';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'options';
/**
* Register routes.
*/
public function register_routes() {
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => array( $this, 'get_options' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base,
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( $this, 'update_options' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
),
'schema' => array( $this, 'get_item_schema' ),
)
);
}
/**
* Check if a given request has access to manage options.
*
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function update_item_permissions_check( $request ) {
if ( ! current_user_can( 'manage_options' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot manage options.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}
/**
* Gets an array of options and respective values.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Options object with option values.
*/
public function get_options( $request ) {
$params = explode( ',', $request[ 'options' ] );
$options = array();
if ( ! is_array( $params ) ) {
return array();
}
foreach ( $params as $option ) {
$options[ $option ] = get_option( $option );
}
return $options;
}
/**
* Updates an array of objects.
*
* @param WP_REST_Request $request Full details about the request.
* @return array Options object with a boolean if the option was updated.
*/
public function update_options( $request ) {
$params = $request->get_json_params();
$updated = array();
if ( ! is_array( $params ) ) {
return array();
}
foreach ( $params as $key => $value ) {
$updated[ $key ] = update_option( $key, $value );
}
return $updated;
}
/**
* Get the schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'options',
'type' => 'object',
'properties' => array(
'options' => array(
'type' => 'array',
'description' => __( 'Array of options with associated values.', 'woocommerce-admin' ),
'context' => array( 'view' ),
'readonly' => true,
),
),
);
return $this->add_additional_fields_schema( $schema );
}
}

View File

@ -54,6 +54,7 @@ class Onboarding {
}
// Include WC Admin Onboarding classes.
// @todo We should return early if should_show_profiler and a new method should_show_tasks are both false.
OnboardingTasks::get_instance();
add_action( 'woocommerce_components_settings', array( $this, 'component_settings' ), 20 ); // Run after Automattic\WooCommerce\Admin\Loader.

View File

@ -47,11 +47,19 @@ class OnboardingTasks {
* Constructor
*/
public function __construct() {
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( 'admin_init', array( $this, 'set_active_task' ), 20 );
add_action( 'admin_init', array( $this, 'check_active_task_completion' ), 1 );
}
/**
* Enqueue scripts and styles.
*/
public function add_media_scripts() {
wp_enqueue_media();
}
/**
* Add task items to component settings.
*
@ -72,6 +80,7 @@ class OnboardingTasks {
}
$settings['onboarding']['automatedTaxSupportedCountries'] = self::get_automated_tax_supported_countries();
$settings['onboarding']['customLogo'] = get_theme_mod( 'custom_logo', false );
$settings['onboarding']['tasks'] = $tasks;
$settings['onboarding']['shippingZonesCount'] = count( \WC_Shipping_Zones::get_zones() );