This commit is contained in:
Joshua T Flowers 2019-08-21 13:58:47 +08:00 committed by GitHub
parent dd9948aa1a
commit f4d7936b17
21 changed files with 957 additions and 292 deletions

View File

@ -0,0 +1,114 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import { SelectControl, TextControl } from 'newspack-components';
import { useMemo } from 'react';
/**
* Form validation.
*
* @param {Object} values Keyed values of all fields in the form.
* @return {Object} Key value of fields and error messages, { myField: 'This field is required' }
*/
export function validateStoreAddress( values ) {
const errors = {};
if ( ! values.addressLine1.length ) {
errors.addressLine1 = __( 'Please add an address', 'woocommerce-admin' );
}
if ( ! values.countryState.length ) {
errors.countryState = __( 'Please select a country and state', 'woocommerce-admin' );
}
if ( ! values.city.length ) {
errors.city = __( 'Please add a city', 'woocommerce-admin' );
}
if ( ! values.postCode.length ) {
errors.postCode = __( 'Please add a post code', 'woocommerce-admin' );
}
return errors;
}
/**
* Get all country and state combinations used for select dropdowns.
*
* @return {Object} Select options, { value: 'US:GA', label: 'United States - Georgia' }
*/
export function getCountryStateOptions() {
const countries = ( wcSettings.dataEndpoints && wcSettings.dataEndpoints.countries ) || [];
const countryStateOptions = countries.reduce( ( acc, country ) => {
if ( ! country.states.length ) {
acc.push( {
value: country.code,
label: decodeEntities( country.name ),
} );
return acc;
}
const countryStates = country.states.map( state => {
return {
value: country.code + ':' + state.code,
label: decodeEntities( country.name ) + ' -- ' + decodeEntities( state.name ),
};
} );
acc.push( ...countryStates );
return acc;
}, [] );
countryStateOptions.unshift( { value: '', label: '' } );
return countryStateOptions;
}
/**
* Store address fields.
*
* @param {Object} props Props for input components.
* @return {Object} -
*/
export function StoreAddress( props ) {
const { getInputProps } = props;
const countryStateOptions = useMemo( () => getCountryStateOptions(), [] );
return (
<div className="woocommerce-store-address-fields">
<TextControl
label={ __( 'Address line 1', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'addressLine1' ) }
/>
<TextControl
label={ __( 'Address line 2 (optional)', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'addressLine2' ) }
/>
<SelectControl
label={ __( 'Country / State', 'woocommerce-admin' ) }
required
options={ countryStateOptions }
{ ...getInputProps( 'countryState' ) }
/>
<TextControl
label={ __( 'City', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'city' ) }
/>
<TextControl
label={ __( 'Post code', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'postCode' ) }
/>
</div>
);
}

View File

@ -73,12 +73,14 @@ class ProfileWizard extends Component {
componentDidMount() { componentDidMount() {
document.documentElement.classList.remove( 'wp-toolbar' ); document.documentElement.classList.remove( 'wp-toolbar' );
document.body.classList.add( 'woocommerce-onboarding' );
document.body.classList.add( 'woocommerce-profile-wizard__body' ); document.body.classList.add( 'woocommerce-profile-wizard__body' );
document.body.classList.add( 'woocommerce-admin-full-screen' ); document.body.classList.add( 'woocommerce-admin-full-screen' );
} }
componentWillUnmount() { componentWillUnmount() {
document.documentElement.classList.add( 'wp-toolbar' ); document.documentElement.classList.add( 'wp-toolbar' );
document.body.classList.remove( 'woocommerce-onboarding' );
document.body.classList.remove( 'woocommerce-profile-wizard__body' ); document.body.classList.remove( 'woocommerce-profile-wizard__body' );
document.body.classList.remove( 'woocommerce-admin-full-screen' ); document.body.classList.remove( 'woocommerce-admin-full-screen' );
} }

View File

@ -188,7 +188,6 @@ class BusinessDetails extends Component {
<div className="woocommerce-profile-wizard__benefit-toggle"> <div className="woocommerce-profile-wizard__benefit-toggle">
<FormToggle <FormToggle
checked={ values[ benefit.slug ] } checked={ values[ benefit.slug ] }
className="woocommerce-profile-wizard__toggle"
{ ...getInputProps( benefit.slug ) } { ...getInputProps( benefit.slug ) }
/> />
</div> </div>

View File

@ -181,7 +181,6 @@ class Start extends Component {
onChange={ this.onTrackingChange } onChange={ this.onTrackingChange }
onClick={ e => e.stopPropagation() } onClick={ e => e.stopPropagation() }
tabIndex="-1" tabIndex="-1"
className="woocommerce-profile-wizard__toggle"
/> />
</div> </div>

View File

@ -3,10 +3,9 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { Button, SelectControl, TextControl, CheckboxControl } from 'newspack-components'; import { Button, CheckboxControl } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element'; import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose'; import { compose } from '@wordpress/compose';
import { decodeEntities } from '@wordpress/html-entities';
import { withDispatch } from '@wordpress/data'; import { withDispatch } from '@wordpress/data';
import { recordEvent } from 'lib/tracks'; import { recordEvent } from 'lib/tracks';
@ -15,15 +14,15 @@ import { recordEvent } from 'lib/tracks';
*/ */
import { H, Card, Form } from '@woocommerce/components'; import { H, Card, Form } from '@woocommerce/components';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import {
StoreAddress,
validateStoreAddress,
} from '../../components/settings/general/store-address';
class StoreDetails extends Component { class StoreDetails extends Component {
constructor() { constructor() {
super( ...arguments ); super( ...arguments );
this.state = {
countryStateOptions: [],
};
this.initialValues = { this.initialValues = {
addressLine1: '', addressLine1: '',
addressLine2: '', addressLine2: '',
@ -36,30 +35,6 @@ class StoreDetails extends Component {
this.onContinue = this.onContinue.bind( this ); this.onContinue = this.onContinue.bind( this );
} }
componentWillMount() {
const countryStateOptions = this.getCountryStateOptions();
this.setState( { countryStateOptions } );
}
validate( values ) {
const errors = {};
if ( ! values.addressLine1.length ) {
errors.addressLine1 = __( 'Please add an address', 'woocommerce-admin' );
}
if ( ! values.countryState.length ) {
errors.countryState = __( 'Please select a country and state', 'woocommerce-admin' );
}
if ( ! values.city.length ) {
errors.city = __( 'Please add a city', 'woocommerce-admin' );
}
if ( ! values.postCode.length ) {
errors.postCode = __( 'Please add a post code', 'woocommerce-admin' );
}
return errors;
}
async onContinue( values ) { async onContinue( values ) {
const { const {
createNotice, createNotice,
@ -97,39 +72,7 @@ class StoreDetails extends Component {
} }
} }
getCountryStateOptions() {
const countries = ( wcSettings.dataEndpoints && wcSettings.dataEndpoints.countries ) || [];
const countryStateOptions = countries.reduce( ( acc, country ) => {
if ( ! country.states.length ) {
acc.push( {
value: country.code,
label: decodeEntities( country.name ),
} );
return acc;
}
const countryStates = country.states.map( state => {
return {
value: country.code + ':' + state.code,
label: decodeEntities( country.name ) + ' -- ' + decodeEntities( state.name ),
};
} );
acc.push( ...countryStates );
return acc;
}, [] );
countryStateOptions.unshift( { value: '', label: '' } );
return countryStateOptions;
}
render() { render() {
const { countryStateOptions } = this.state;
return ( return (
<Fragment> <Fragment>
<H className="woocommerce-profile-wizard__header-title"> <H className="woocommerce-profile-wizard__header-title">
@ -146,41 +89,11 @@ class StoreDetails extends Component {
<Form <Form
initialValues={ this.initialValues } initialValues={ this.initialValues }
onSubmitCallback={ this.onContinue } onSubmitCallback={ this.onContinue }
validate={ this.validate } validate={ validateStoreAddress }
> >
{ ( { getInputProps, handleSubmit } ) => ( { ( { getInputProps, handleSubmit } ) => (
<Fragment> <Fragment>
<TextControl <StoreAddress getInputProps={ getInputProps } />
label={ __( 'Address line 1', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'addressLine1' ) }
/>
<TextControl
label={ __( 'Address line 2 (optional)', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'addressLine2' ) }
/>
<SelectControl
label={ __( 'Country / State', 'woocommerce-admin' ) }
required
options={ countryStateOptions }
{ ...getInputProps( 'countryState' ) }
/>
<TextControl
label={ __( 'City', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'city' ) }
/>
<TextControl
label={ __( 'Post code', 'woocommerce-admin' ) }
required
{ ...getInputProps( 'postCode' ) }
/>
<CheckboxControl <CheckboxControl
label={ __( "I'm setting up a store for a client", 'woocommerce-admin' ) } label={ __( "I'm setting up a store for a client", 'woocommerce-admin' ) }
{ ...getInputProps( 'isClient' ) } { ...getInputProps( 'isClient' ) }

View File

@ -211,7 +211,7 @@
.woocommerce-theme-preview__theme-name { .woocommerce-theme-preview__theme-name {
padding-left: $gap; padding-left: $gap;
color: #1a1a1a; color: $muriel-gray-900;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -36,28 +36,6 @@
} }
} }
.woocommerce-stepper .woocommerce-stepper__step {
.woocommerce-stepper__step-label {
color: $muriel-gray-800;
}
&.is-active,
&.is-complete {
.woocommerce-stepper__step-icon {
background: $muriel-hot-purple-600;
color: $muriel-white;
}
.woocommerce-stepper__step-label {
color: $muriel-gray-900;
}
}
.woocommerce-spinner {
background: $muriel-hot-purple-600;
}
}
.woocommerce-profile-wizard__header { .woocommerce-profile-wizard__header {
height: 56px; height: 56px;
border-bottom: 1px solid $muriel-gray-50; border-bottom: 1px solid $muriel-gray-50;
@ -137,26 +115,6 @@
padding-right: 0; padding-right: 0;
} }
/* Muriel style overrides */
.muriel-component {
margin-top: $gap;
margin-bottom: $gap;
}
.components-base-control.has-error {
margin-bottom: $gap * 2 !important;
border-color: $muriel-red-500;
.components-base-control__help {
top: 100%;
position: absolute;
margin-top: $gap-smallest;
font-size: 12px;
font-style: normal;
color: $muriel-red-500;
}
}
.woocommerce-profile-wizard__error { .woocommerce-profile-wizard__error {
display: block; display: block;
margin-top: $gap; margin-top: $gap;
@ -165,88 +123,6 @@
color: $muriel-red-500; color: $muriel-red-500;
} }
.muriel-input-text {
&.empty {
.components-base-control__label {
display: none;
}
input {
top: 14px;
}
}
input[type='text']:focus {
box-shadow: none;
border: 0;
}
}
.muriel-input-text label,
.muriel-select label {
display: initial;
width: auto;
right: auto;
left: $gap;
}
.muriel-select select {
left: 15px;
}
.muriel-select.empty label {
top: 15px;
}
.muriel-select::after {
margin-top: -$gap-smallest;
}
.muriel-input-text,
.muriel-select {
&.with-value,
&.active {
label {
top: 8px;
}
input,
select {
top: 24px;
}
}
}
.muriel-input-text.active,
.muriel-select.active {
box-shadow: 0 0 0 2px $muriel-hot-purple-600;
border-color: transparent;
}
.muriel-checkbox label.components-checkbox-control__label {
margin-left: $gap-smaller;
}
.muriel-checkbox input[type='checkbox'] {
width: 18px;
height: 18px;
}
.muriel-checkbox input[type='checkbox']:checked::before {
font-size: 18px;
margin-left: -3px;
margin-top: -1px;
}
.muriel-button.is-button {
height: 48px;
}
.muriel-checkbox input:checked {
background-color: $muriel-hot-purple-600;
border-color: $muriel-hot-purple-600;
}
.woocommerce-profile-wizard__benefit { .woocommerce-profile-wizard__benefit {
display: flex; display: flex;
@ -294,42 +170,6 @@
} }
} }
.woocommerce-profile-wizard__toggle {
label {
font-size: 14px;
}
.components-base-control {
display: inline-block;
}
&.components-form-toggle {
display: inline-block;
}
&.components-form-toggle .components-form-toggle__track {
width: 36px;
max-width: 36px;
height: 18px;
max-height: 18px;
}
.components-base-control__field {
margin-bottom: 0;
}
.muriel-checkbox label.components-checkbox-control__label {
margin-left: $gap-large;
}
&.components-form-toggle.is-checked {
.components-form-toggle__track {
background-color: $muriel-hot-purple-600;
border-color: $muriel-hot-purple-600;
}
}
}
.woocommerce-profile-wizard__tracking { .woocommerce-profile-wizard__tracking {
.components-form-toggle { .components-form-toggle {
display: none; display: none;

View File

@ -75,28 +75,163 @@
} }
} }
.woocommerce-dashboard__body { .woocommerce-onboarding {
background: $muriel-gray-0; .woocommerce-stepper .woocommerce-stepper__step {
color: $muriel-gray-600; .woocommerce-stepper__step-label {
font-family: $default-font; color: $muriel-gray-800;
#wpbody-content {
min-height: 100vh;
} }
/* Hide wp-admin and WooCommerce elements when the dashboard body class is present */ &.is-active,
#adminmenumain, &.is-complete {
.woocommerce-layout__header, .woocommerce-stepper__step-icon {
.update-nag, background: $muriel-hot-purple-600;
.woocommerce-store-alerts, color: $muriel-white;
.woocommerce-message, }
.notice,
.error, .woocommerce-stepper__step-label {
.updated { color: $muriel-gray-900;
}
}
.woocommerce-spinner {
background: $muriel-hot-purple-600;
}
}
/* Muriel style overrides */
.muriel-component {
margin-top: $gap;
margin-bottom: $gap;
}
.components-base-control.has-error {
margin-bottom: $gap * 2 !important;
border-color: $muriel-red-500;
.components-base-control__help {
top: 100%;
position: absolute;
margin-top: $gap-smallest;
font-size: 12px;
font-style: normal;
color: $muriel-red-500;
}
}
.muriel-input-text {
&.empty {
.components-base-control__label {
display: none; display: none;
} }
#wpcontent { input {
margin-left: 0; top: 14px;
}
}
input[type='text']:focus {
box-shadow: none;
border: 0;
}
}
.muriel-input-text label,
.muriel-select label {
display: initial;
width: auto;
right: auto;
left: $gap;
}
.muriel-select select {
left: 15px;
}
.muriel-select.empty label {
top: 15px;
}
.muriel-select::after {
margin-top: -$gap-smallest;
}
.muriel-input-text,
.muriel-select {
&.with-value,
&.active {
label {
display: block;
top: 8px;
}
input,
select {
top: 24px;
}
}
}
.muriel-input-text.active,
.muriel-select.active {
box-shadow: 0 0 0 2px $muriel-hot-purple-600;
border-color: transparent;
}
.muriel-checkbox label.components-checkbox-control__label {
margin-left: $gap-smaller;
}
.muriel-checkbox input[type='checkbox'] {
width: 18px;
height: 18px;
}
.muriel-checkbox input[type='checkbox']:checked::before {
font-size: 18px;
margin-left: -3px;
margin-top: -1px;
}
.muriel-button.is-button {
height: 48px;
}
.muriel-checkbox input[type='checkbox']:checked {
background-color: $muriel-hot-purple-600;
border-color: $muriel-hot-purple-600;
}
.components-form-toggle {
display: inline-block;
label {
font-size: 14px;
}
.components-base-control {
display: inline-block;
}
.components-form-toggle__track {
width: 36px;
max-width: 36px;
height: 18px;
max-height: 18px;
}
.components-base-control__field {
margin-bottom: 0;
}
.muriel-checkbox label.components-checkbox-control__label {
margin-left: $gap-large;
}
&.is-checked {
.components-form-toggle__track {
background-color: $muriel-hot-purple-600;
border-color: $muriel-hot-purple-600;
}
}
} }
} }

View File

@ -19,19 +19,22 @@ import { updateQueryString } from '@woocommerce/navigation';
import './style.scss'; import './style.scss';
import Connect from './tasks/connect'; import Connect from './tasks/connect';
import Products from './tasks/products'; import Products from './tasks/products';
import Shipping from './tasks/shipping';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
class TaskDashboard extends Component { class TaskDashboard extends Component {
componentDidMount() { componentDidMount() {
document.body.classList.add( 'woocommerce-onboarding' );
document.body.classList.add( 'woocommerce-task-dashboard__body' ); document.body.classList.add( 'woocommerce-task-dashboard__body' );
} }
componentWillUnmount() { componentWillUnmount() {
document.body.classList.remove( 'woocommerce-onboarding' );
document.body.classList.remove( 'woocommerce-task-dashboard__body' ); document.body.classList.remove( 'woocommerce-task-dashboard__body' );
} }
getTasks() { getTasks() {
const { tasks } = wcSettings.onboarding; const { shippingZonesCount, tasks } = wcSettings.onboarding;
const { profileItems, query } = this.props; const { profileItems, query } = this.props;
return [ return [
@ -82,9 +85,16 @@ class TaskDashboard extends Component {
'Configure some basic shipping rates to get started', 'Configure some basic shipping rates to get started',
'wooocommerce-admin' 'wooocommerce-admin'
), ),
before: <i className="material-icons-outlined">local_shipping</i>, before:
shippingZonesCount > 0 ? (
<i className="material-icons-outlined">check_circle</i>
) : (
<i className="material-icons-outlined">local_shipping</i>
),
after: <i className="material-icons-outlined">chevron_right</i>, after: <i className="material-icons-outlined">chevron_right</i>,
onClick: noop, onClick: () => updateQueryString( { task: 'shipping' } ),
container: <Shipping />,
className: shippingZonesCount > 0 ? 'is-complete' : null,
visible: true, visible: true,
}, },
{ {
@ -136,6 +146,7 @@ class TaskDashboard extends Component {
currentTask.container currentTask.container
) : ( ) : (
<Card <Card
className="woocommerce-task-card"
title={ __( 'Set up your store and start selling', 'woocommerce-admin' ) } title={ __( 'Set up your store and start selling', 'woocommerce-admin' ) }
description={ __( description={ __(
'Below youll find a list of the most important steps to get your store up and running.', 'Below youll find a list of the most important steps to get your store up and running.',

View File

@ -5,11 +5,22 @@
color: $muriel-gray-500; color: $muriel-gray-500;
} }
.woocommerce-card__body { .woocommerce-task-card .woocommerce-card__body {
border-top: 1px solid $muriel-gray-50; border-top: 1px solid $muriel-gray-50;
padding: 0; padding: 0;
} }
.woocommerce-card.is-narrow {
max-width: 680px;
margin-left: auto;
margin-right: auto;
}
.muriel-button.is-button {
height: 40px;
margin: 0;
}
.woocommerce-list { .woocommerce-list {
margin: 0; margin: 0;
@ -53,3 +64,74 @@
position: relative; position: relative;
} }
} }
.woocommerce-shipping-rates {
margin-bottom: $gap;
}
.woocommerce-shipping-rate {
display: flex;
padding-top: $gap-small;
padding-bottom: $gap-small;
.woocommerce-shipping-rate__main {
width: 100%;
}
.woocommerce-shipping-rate__icon {
padding-top: $gap;
margin-right: $gap-large;
}
.woocommerce-shipping-rate__name {
align-items: center;
display: flex;
padding-top: $gap;
font-size: 16px;
line-height: 22px;
color: $muriel-gray-900;
margin-bottom: $gap-small;
border-top: 1px solid $new-muriel-gray-5;
.components-form-toggle {
margin-left: auto;
height: 18px;
}
}
.woocommerce-shipping-rate__control-wrapper {
position: relative;
.muriel-input-text input {
left: $gap * 2;
}
.components-base-control {
margin-bottom: 0;
}
.woocommerce-shipping-rate__control-prefix,
.woocommerce-shipping-rate__control-suffix {
display: none;
position: absolute;
left: $gap;
top: 26px;
z-index: 1;
font-size: 16px;
line-height: 24px;
color: $new-muriel-gray-50;
}
&.has-value {
.woocommerce-shipping-rate__control-prefix,
.woocommerce-shipping-rate__control-suffix {
display: block;
}
}
.woocommerce-shipping-rate__control-suffix {
left: auto;
right: $gap;
}
}
}

View File

@ -52,7 +52,7 @@ export default class Products extends Component {
render() { render() {
return ( return (
<Fragment> <Fragment>
<Card> <Card className="woocommerce-task-card">
<List items={ subTasks } /> <List items={ subTasks } />
</Card> </Card>
</Fragment> </Fragment>

View File

@ -0,0 +1,214 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { Component } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { 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 StoreLocation from './location';
import ShippingRates from './rates';
import withSelect from 'wc-api/with-select';
class Shipping extends Component {
constructor() {
super( ...arguments );
this.initialState = {
isPending: false,
step: 'store_location',
shippingZones: [],
};
this.state = this.initialState;
this.completeStep = this.completeStep.bind( this );
}
componentDidMount() {
this.reset();
}
reset() {
this.setState( this.initialState );
}
async fetchShippingZones() {
this.setState( { isPending: true } );
const { countryCode, countryName } = this.props;
// @todo The following fetches for shipping information should be moved into
// the wc-api to make these methods and states more readily available.
const shippingZones = [];
const zones = await apiFetch( { path: '/wc/v3/shipping/zones' } );
let hasCountryZone = false;
await Promise.all(
zones.map( async zone => {
// "Rest of the world zone"
if ( 0 === zone.id ) {
zone.methods = await apiFetch( { path: `/wc/v3/shipping/zones/${ zone.id }/methods` } );
zone.name = __( 'Rest of the world', 'woocommerce-admin' );
zone.toggleEnabled = true;
shippingZones.push( zone );
return;
}
// Return any zone with a single location matching the country zone.
zone.locations = await apiFetch( { path: `/wc/v3/shipping/zones/${ zone.id }/locations` } );
const countryLocation = zone.locations.find( location => countryCode === location.code );
if ( countryLocation ) {
zone.methods = await apiFetch( { path: `/wc/v3/shipping/zones/${ zone.id }/methods` } );
shippingZones.push( zone );
hasCountryZone = true;
}
} )
);
// Create the default store country zone if it doesn't exist.
if ( ! hasCountryZone ) {
const zone = await apiFetch( {
method: 'POST',
path: '/wc/v3/shipping/zones',
data: { name: countryName },
} );
zone.locations = await apiFetch( {
method: 'POST',
path: `/wc/v3/shipping/zones/${ zone.id }/locations`,
data: [ { code: countryCode, type: 'country' } ],
} );
shippingZones.push( zone );
}
shippingZones.reverse();
this.setState( { isPending: false, shippingZones } );
}
componentDidUpdate( prevProps, prevState ) {
const { countryCode, settings } = this.props;
const {
woocommerce_store_address,
woocommerce_default_country,
woocommerce_store_postcode,
} = settings;
const { step } = this.state;
if (
'store_location' === step &&
woocommerce_store_address &&
woocommerce_default_country &&
woocommerce_store_postcode
) {
this.completeStep();
}
if (
'rates' === step &&
( prevProps.countryCode !== countryCode || 'rates' !== prevState.step )
) {
this.fetchShippingZones();
}
}
completeStep() {
const { step } = this.state;
const steps = this.getSteps();
const currentStepIndex = steps.findIndex( s => s.key === step );
const nextStep = steps[ currentStepIndex + 1 ];
if ( nextStep ) {
this.setState( { step: nextStep.key } );
} else {
getHistory().push( getNewPath( {}, '/', {} ) );
}
}
getSteps() {
const steps = [
{
key: 'store_location',
label: __( 'Set store location', 'woocommerce-admin' ),
description: __( 'The address from which your business operates', 'woocommerce-admin' ),
content: <StoreLocation completeStep={ this.completeStep } { ...this.props } />,
visible: true,
},
{
key: 'rates',
label: __( 'Set shipping costs', 'woocommerce-admin' ),
description: __(
'Define how much customers pay to ship to different destinations',
'woocommerce-admin'
),
content: (
<ShippingRates
shippingZones={ this.state.shippingZones }
completeStep={ this.completeStep }
{ ...this.props }
/>
),
visible: true,
},
];
return filter( steps, step => step.visible );
}
render() {
const { isPending, step } = this.state;
const { isSettingsRequesting } = this.props;
return (
<div className="woocommerce-task-shipping">
<Card className="is-narrow">
<Stepper
isPending={ isPending || isSettingsRequesting }
isVertical
currentStep={ step }
steps={ this.getSteps() }
/>
</Card>
</div>
);
}
}
export default compose(
withSelect( select => {
const { getSettings, getSettingsError, isGetSettingsRequesting } = select( 'wc-api' );
const settings = getSettings( 'general' );
const isSettingsError = Boolean( getSettingsError( 'general' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'general' );
const countryCode = settings.woocommerce_default_country
? settings.woocommerce_default_country.split( ':' )[ 0 ]
: null;
const countries = ( wcSettings.dataEndpoints && wcSettings.dataEndpoints.countries ) || [];
const country = countryCode ? countries.find( c => c.code === countryCode ) : null;
const countryName = country ? country.name : null;
return { countryCode, countryName, isSettingsError, isSettingsRequesting, settings };
} ),
withDispatch( dispatch => {
const { createNotice } = dispatch( 'core/notices' );
const { updateSettings } = dispatch( 'wc-api' );
return {
createNotice,
updateSettings,
};
} )
)( Shipping );

View File

@ -0,0 +1,95 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from 'newspack-components';
import { Component, Fragment } from '@wordpress/element';
/**
* WooCommerce dependencies
*/
import { Form } from '@woocommerce/components';
/**
* Internal dependencies
*/
import {
StoreAddress,
validateStoreAddress,
} from 'dashboard/components/settings/general/store-address';
export default class StoreLocation extends Component {
constructor() {
super( ...arguments );
this.onSubmit = this.onSubmit.bind( this );
}
async onSubmit( values ) {
const { completeStep, createNotice, isSettingsError, updateSettings } = this.props;
await updateSettings( {
general: {
woocommerce_store_address: values.addressLine1,
woocommerce_store_address_2: values.addressLine2,
woocommerce_default_country: values.countryState,
woocommerce_store_city: values.city,
woocommerce_store_postcode: values.postCode,
},
} );
if ( ! isSettingsError ) {
completeStep();
} else {
createNotice(
'error',
__( 'There was a problem saving your store location.', 'woocommerce-admin' )
);
}
}
getInitialValues() {
const { settings } = this.props;
const {
woocommerce_store_address,
woocommerce_store_address_2,
woocommerce_store_city,
woocommerce_default_country,
woocommerce_store_postcode,
} = settings;
return {
addressLine1: woocommerce_store_address || '',
addressLine2: woocommerce_store_address_2 || '',
city: woocommerce_store_city || '',
countryState: woocommerce_default_country || '',
postCode: woocommerce_store_postcode || '',
};
}
render() {
const { isSettingsRequesting } = this.props;
if ( isSettingsRequesting ) {
return null;
}
return (
<Form
initialValues={ this.getInitialValues() }
onSubmitCallback={ this.onSubmit }
validate={ validateStoreAddress }
>
{ ( { getInputProps, handleSubmit } ) => (
<Fragment>
<StoreAddress getInputProps={ getInputProps } />
<Button isPrimary onClick={ handleSubmit }>
{ __( 'Continue', 'woocommerce-admin' ) }
</Button>
</Fragment>
) }
</Form>
);
}
}

View File

@ -0,0 +1,244 @@
/** @format */
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { Button, TextControl } from 'newspack-components';
import classnames from 'classnames';
import { Component, Fragment } from '@wordpress/element';
import { FormToggle } from '@wordpress/components';
import { get } from 'lodash';
import PropTypes from 'prop-types';
/**
* WooCommerce dependencies
*/
import { Flag, Form } from '@woocommerce/components';
import { getCurrencyFormatString } from '@woocommerce/currency';
class ShippingRates extends Component {
constructor() {
super( ...arguments );
this.updateShippingZones = this.updateShippingZones.bind( this );
}
async updateShippingZones( values ) {
const { createNotice, shippingZones } = this.props;
shippingZones.map( zone => {
const flatRateMethods = zone.methods
? zone.methods.filter( method => 'flat_rate' === method.method_id )
: [];
if ( zone.toggleEnabled && ! values[ `${ zone.id }_enabled` ] ) {
// Disable any flat rate methods that exist if toggled off.
if ( flatRateMethods.length ) {
flatRateMethods.map( method => {
apiFetch( {
method: 'POST',
path: `/wc/v3/shipping/zones/${ zone.id }/methods/${ method.instance_id }`,
data: {
enabled: false,
},
} );
} );
}
return;
}
if ( flatRateMethods.length ) {
// Update the existing method.
apiFetch( {
method: 'POST',
path: `/wc/v3/shipping/zones/${ zone.id }/methods/${ flatRateMethods[ 0 ].instance_id }`,
data: {
enabled: true,
settings: { cost: values[ `${ zone.id }_rate` ] },
},
} );
} else {
// Add new method if one doesn't exist.
apiFetch( {
method: 'POST',
path: `/wc/v3/shipping/zones/${ zone.id }/methods`,
data: {
method_id: 'flat_rate',
settings: { cost: values[ `${ zone.id }_rate` ] },
},
} );
}
} );
// @todo This is a workaround to force the task to mark as complete.
// This should probably be updated to use wc-api so we can fetch shipping methods.
wcSettings.onboarding.shippingZonesCount = 1;
createNotice( 'success', __( 'Your shipping rates have been updated.', 'woocommerce-admin' ) );
this.props.completeStep();
}
renderInputPrefix() {
const symbolPosition = get( wcSettings, [ 'currency', 'position' ] );
if ( 0 === symbolPosition.indexOf( 'right' ) ) {
return;
}
return (
<span className="woocommerce-shipping-rate__control-prefix">
{ get( wcSettings, [ 'currency', 'symbol' ], '$' ) }
</span>
);
}
renderInputSuffix( rate ) {
const symbolPosition = get( wcSettings, [ 'currency', 'position' ] );
if ( 0 === symbolPosition.indexOf( 'right' ) ) {
return (
<span className="woocommerce-shipping-rate__control-suffix">
{ get( wcSettings, [ 'currency', 'symbol' ], '$' ) }
</span>
);
}
return parseFloat( rate ) === parseFloat( 0 ) ? (
<span className="woocommerce-shipping-rate__control-suffix">
{ __( 'Free shipping', 'woocommerce-admin' ) }
</span>
) : null;
}
getInitialValues() {
const values = {};
this.props.shippingZones.forEach( zone => {
const flatRateMethods =
zone.methods && zone.methods.length
? zone.methods.filter( method => 'flat_rate' === method.method_id )
: [];
const rate = flatRateMethods.length
? flatRateMethods[ 0 ].settings.cost.value
: getCurrencyFormatString( 0 );
values[ `${ zone.id }_rate` ] = rate;
if ( flatRateMethods.length && flatRateMethods[ 0 ].enabled ) {
values[ `${ zone.id }_enabled` ] = true;
} else {
values[ `${ zone.id }_enabled` ] = false;
}
} );
return values;
}
validate( values ) {
const errors = {};
const rates = Object.keys( values ).filter( field => field.endsWith( '_rate' ) );
rates.forEach( rate => {
if ( values[ rate ] < 0 ) {
errors[ rate ] = __( 'Shipping rates can not be negative numbers.', 'woocommerce-admin' );
}
} );
return errors;
}
render() {
const { shippingZones } = this.props;
if ( ! shippingZones.length ) {
return null;
}
return (
<Form
initialValues={ this.getInitialValues() }
onSubmitCallback={ this.updateShippingZones }
validate={ this.validate }
>
{ ( { getInputProps, handleSubmit, setTouched, setValue, values } ) => {
return (
<Fragment>
<div className="woocommerce-shipping-rates">
{ shippingZones.map( zone => (
<div className="woocommerce-shipping-rate" key={ zone.id }>
<div className="woocommerce-shipping-rate__icon">
{ zone.locations ? (
zone.locations.map( location => (
<Flag size={ 24 } code={ location.code } key={ location.code } />
) )
) : (
// Icon used for zones without locations or "Rest of the world".
<i className="material-icons-outlined">public</i>
) }
</div>
<div className="woocommerce-shipping-rate__main">
<div className="woocommerce-shipping-rate__name">
{ zone.name }
{ zone.toggleEnabled && (
<FormToggle { ...getInputProps( `${ zone.id }_enabled` ) } />
) }
</div>
{ ( ! zone.toggleEnabled || values[ `${ zone.id }_enabled` ] ) && (
<div
className={ classnames( 'woocommerce-shipping-rate__control-wrapper', {
'has-value': values[ `${ zone.id }_rate` ],
} ) }
>
{ this.renderInputPrefix() }
<TextControl
label={ __( 'Shipping cost', 'woocommerce-admin' ) }
required
{ ...getInputProps( `${ zone.id }_rate` ) }
onBlur={ () => {
setTouched( `${ zone.id }_rate` );
setValue(
`${ zone.id }_rate`,
getCurrencyFormatString( values[ `${ zone.id }_rate` ] )
);
} }
/>
{ this.renderInputSuffix( values[ `${ zone.id }_rate` ] ) }
</div>
) }
</div>
</div>
) ) }
</div>
<Button isPrimary onClick={ handleSubmit }>
{ __( 'Complete task', 'woocommerce-admin' ) }
</Button>
</Fragment>
);
} }
</Form>
);
}
}
ShippingRates.propTypes = {
/**
* Function used to mark the step complete.
*/
completeStep: PropTypes.func.isRequired,
/**
* Function to create a transient notice in the store.
*/
createNotice: PropTypes.func.isRequired,
/**
* Array of shipping zones returned from the WC REST API with added
* `methods` and `locations` properties appended from separate API calls.
*/
shippingZones: PropTypes.array,
};
ShippingRates.defaultProps = {
shippingZones: [],
};
export default ShippingRates;

View File

@ -82,4 +82,7 @@ $muriel-box-shadow-1dp: 0 2px 1px -1px rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0
0 1px 3px 0 rgba(0, 0, 0, 0.12); 0 1px 3px 0 rgba(0, 0, 0, 0.12);
$muriel-box-shadow-8dp: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), $muriel-box-shadow-8dp: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12); 0 3px 14px 2px rgba(0, 0, 0, 0.12);
$muriel-primary-500: #005fb7; // @todo These can be removed once color-studio is updated to >= 2.0.0.
$new-muriel-gray-50: #676a74;
$new-muriel-gray-5: #e3dfe2;
$new-muriel-primary-500: #005fb7;

View File

@ -80,7 +80,8 @@ function settingToSettingsResource( resourceName, data ) {
return ''; return '';
} }
const resources = {}; // Override lastReceived time for group when batch updating.
const resources = { [ resourceName ]: { lastReceived: Date.now() } };
data.update.forEach( data.update.forEach(
setting => setting =>
( resources[ getResourceName( resourceName, setting.id ) ] = { data: setting.value } ) ( resources[ getResourceName( resourceName, setting.id ) ] = { data: setting.value } )

View File

@ -20,6 +20,7 @@ class Form extends Component {
this.getInputProps = this.getInputProps.bind( this ); this.getInputProps = this.getInputProps.bind( this );
this.handleSubmit = this.handleSubmit.bind( this ); this.handleSubmit = this.handleSubmit.bind( this );
this.setTouched = this.setTouched.bind( this );
this.setValue = this.setValue.bind( this ); this.setValue = this.setValue.bind( this );
} }
@ -44,6 +45,12 @@ class Form extends Component {
} ), this.validate ); } ), this.validate );
} }
setTouched( name, touched = true ) {
this.setState( prevState => ( {
touched: { ...prevState.touched, [ name ]: touched },
} ) );
}
handleChange( name, value ) { handleChange( name, value ) {
const { values } = this.state; const { values } = this.state;
@ -60,9 +67,7 @@ class Form extends Component {
} }
handleBlur( name ) { handleBlur( name ) {
this.setState( prevState => ( { this.setTouched( name );
touched: { ...prevState.touched, [ name ]: true },
} ) );
} }
async handleSubmit() { async handleSubmit() {
@ -97,6 +102,7 @@ class Form extends Component {
values, values,
errors, errors,
touched, touched,
setTouched: this.setTouched,
setValue: this.setValue, setValue: this.setValue,
handleSubmit: this.handleSubmit, handleSubmit: this.handleSubmit,
getInputProps: this.getInputProps, getInputProps: this.getInputProps,
@ -137,8 +143,9 @@ Form.propTypes = {
Form.defaultProps = { Form.defaultProps = {
errors: {}, errors: {},
initialValues: {}, initialValues: {},
touched: {},
onSubmitCallback: noop, onSubmitCallback: noop,
touched: {},
validate: noop,
}; };
export default Form; export default Form;

View File

@ -15,7 +15,7 @@
padding: $gap; padding: $gap;
&:focus { &:focus {
box-shadow: inset 0 0 0 1px $muriel-primary-500, inset 0 0 0 2px #fff; box-shadow: inset 0 0 0 1px $new-muriel-primary-500, inset 0 0 0 2px #fff;
} }
} }
@ -23,7 +23,7 @@
display: block; display: block;
font-size: 16px; font-size: 16px;
line-height: 22px; line-height: 22px;
color: #1a1a1a; color: $muriel-gray-900;
} }
.woocommerce-list__item-description { .woocommerce-list__item-description {

View File

@ -13,6 +13,10 @@
font-weight: 400; font-weight: 400;
position: relative; position: relative;
.woocommerce-stepper__step-text {
width: 100%;
}
.woocommerce-stepper__step-label { .woocommerce-stepper__step-label {
color: $muriel-gray-900; color: $muriel-gray-900;
line-height: $step-icon-size; line-height: $step-icon-size;
@ -112,10 +116,11 @@
.woocommerce-stepper__steps { .woocommerce-stepper__steps {
align-items: initial; align-items: initial;
flex-direction: column; flex-direction: column;
margin-bottom: 0;
} }
.woocommerce-stepper__step { .woocommerce-stepper__step {
min-height: 90px; padding-bottom: $gap-larger;
} }
.woocommerce-stepper__step::after { .woocommerce-stepper__step::after {
@ -128,7 +133,7 @@
} }
.woocommerce-stepper__step:last-child { .woocommerce-stepper__step:last-child {
min-height: auto; padding-bottom: $gap-smaller;
&::after { &::after {
display: none; display: none;
} }

View File

@ -10,7 +10,7 @@ module.exports = {
outlines: '#007cba', outlines: '#007cba',
}, },
themes: { themes: {
'woocommerce-profile-wizard__body': { 'woocommerce-onboarding': {
primary: '#d52c82', primary: '#d52c82',
secondary: '#d52c82', secondary: '#d52c82',
toggle: '#d52c82', toggle: '#d52c82',

View File

@ -72,6 +72,7 @@ class OnboardingTasks {
} }
$settings['onboarding']['tasks'] = $tasks; $settings['onboarding']['tasks'] = $tasks;
$settings['onboarding']['shippingZonesCount'] = count( \WC_Shipping_Zones::get_zones() );
return $settings; return $settings;
} }