* Initial product template modal

* Add custom product template for the new-post route

* Add test, and code for making use of the insert_post action

* Added ProductTemplates Datastore to create template with REST api

* Add back variation method, deleted by accident

* Move product from template endpoint to OnboardingTasks class

* Fix lint errors

* Added tracks and template hooks

* Rename product create permission check function, for less confusion

* Remove duplicate recommended option

* Fix stylelint errors

* PHP tests for the product_from_template endpoint

* Fix onboarding php unit tests

* Write tests for client product template options

* Refactored list component, to use it as a radio control list

* Fix lint errors

* REmove assertion as it fails on the ci

* Add changelog

* Updating tests to make sure all product attribute taxonomies are removed

* Add more specific assertions for importing sample products test

* Update the sample products test

* Deconstruct item object, from PR suggestion

* Fix PHP errors, by updating the createProductFromTemplate call
This commit is contained in:
louwie17 2021-01-25 12:52:42 -04:00 committed by GitHub
parent 3a792f66d9
commit 74b3eccbc9
17 changed files with 705 additions and 144 deletions

View File

@ -17,7 +17,7 @@ import { recordEvent } from '@woocommerce/tracks';
*/
import Appearance from './tasks/appearance';
import { getCategorizedOnboardingProducts } from '../dashboard/utils';
import Products from './tasks/products';
import { Products } from './tasks/products';
import Shipping from './tasks/shipping';
import Tax from './tasks/tax';
import Payments from './tasks/payments';

View File

@ -0,0 +1,7 @@
/**
* Internal dependencies
*/
import Products from './products';
import ProductTemplateModal from './product-template-modal';
export { Products, ProductTemplateModal };

View File

@ -0,0 +1,151 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button, Modal } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { applyFilters } from '@wordpress/hooks';
import { ITEMS_STORE_NAME } from '@woocommerce/data';
import { getAdminLink } from '@woocommerce/wc-admin-settings';
import { recordEvent } from '@woocommerce/tracks';
import { List } from '@woocommerce/components';
/**
* Internal dependencies
*/
import './product-template-modal.scss';
import { createNoticesFromResponse } from '../../../lib/notices';
export const ONBOARDING_PRODUCT_TEMPLATES_FILTER =
'woocommerce_admin_onboarding_product_templates';
const PRODUCT_TEMPLATES = [
{
key: 'physical',
title: __( 'Physical product', 'woocommerce-admin' ),
subtitle: __(
'Tangible items that get delivered to customers',
'woocommerce-admin'
),
},
{
key: 'digital',
title: __( 'Digital product', 'woocommerce-admin' ),
subtitle: __(
'Items that customers download or access through your website',
'woocommerce-admin'
),
},
{
key: 'variable',
title: __( 'Variable product', 'woocommerce-admin' ),
subtitle: __(
'Products with several versions that customers can choose from',
'woocommerce-admin'
),
},
];
export default function ProductTemplateModal( { onClose } ) {
const [ selectedTemplate, setSelectedTemplate ] = useState();
const [ isRedirecting, setIsRedirecting ] = useState( false );
const { createProductFromTemplate } = useDispatch( ITEMS_STORE_NAME );
const createTemplate = () => {
setIsRedirecting( true );
recordEvent( 'tasklist_product_template_selection', {
product_type: selectedTemplate,
} );
if ( selectedTemplate ) {
createProductFromTemplate(
{
template_name: selectedTemplate,
status: 'draft',
},
{ _fields: [ 'id' ] }
).then(
( data ) => {
if ( data && data.id ) {
const link = getAdminLink(
`post.php?post=${ data.id }&action=edit&wc_onboarding_active_task=products&tutorial=true`
);
window.location = link;
}
},
( error ) => {
// failed creating product with template
createNoticesFromResponse( error );
setIsRedirecting( false );
}
);
} else if ( onClose ) {
recordEvent( 'tasklist_product_template_dismiss' );
onClose();
}
};
const onSelectTemplateClick = ( event ) => {
const val = event.target && event.target.value;
setSelectedTemplate( val );
};
const templates = applyFilters(
ONBOARDING_PRODUCT_TEMPLATES_FILTER,
PRODUCT_TEMPLATES
);
return (
<Modal
title={ __( 'Start with a template' ) }
isDismissible={ true }
onRequestClose={ () => onClose() }
className="woocommerce-product-template-modal"
>
<div className="woocommerce-product-template-modal__wrapper">
<div className="woocommerce-product-template-modal__list">
<List items={ templates }>
{ ( item, index ) => (
<div className="woocommerce-list__item-inner">
<input
id={ `product-templates-${
item.key || index
}` }
className="components-radio-control__input"
type="radio"
name="product-template-options"
value={ item.key }
onChange={ onSelectTemplateClick }
checked={ item.key === selectedTemplate }
/>
<label
className="woocommerce-list__item-text"
htmlFor={ `product-templates-${
item.key || index
}` }
>
<div className="woocommerce-list__item-label">
{ item.title }
</div>
<div className="woocommerce-list__item-subtitle">
{ item.subtitle }
</div>
</label>
</div>
) }
</List>
</div>
<div className="woocommerce-product-template-modal__actions">
<Button
isPrimary
isBusy={ isRedirecting }
disabled={ ! selectedTemplate || isRedirecting }
onClick={ createTemplate }
>
{ __( 'Go' ) }
</Button>
</div>
</div>
</Modal>
);
}

View File

@ -0,0 +1,45 @@
$border-color: $gray-100;
.woocommerce-product-template-modal {
min-width: 565px;
.woocommerce-product-template-modal__actions {
padding-top: $gap-large;
}
}
.woocommerce-product-template-modal__list {
.woocommerce-list {
margin: 0 #{-1 * $gap-large};
border-bottom: 1px solid $border-color;
border-top: 1px solid $border-color;
}
.woocommerce-list__item-inner {
.components-radio-control__input {
width: 20px;
height: 20px;
margin-right: $gap;
&:checked::before {
margin: 1px 0 0 1px;
width: 10px;
height: 10px;
background: var(--wp-admin-theme-color);
}
&:focus {
box-shadow: none;
}
}
}
.woocommerce-list__item-label {
color: $gray-900;
}
.woocommerce-list__item-subtitle {
color: $gray-700;
margin-top: 4px;
}
.woocommerce-list__item:hover {
background: none;
}
}
.woocommerce-product-template-modal__actions {
text-align: right;
}

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component, Fragment } from '@wordpress/element';
import { Fragment, useState } from '@wordpress/element';
import { Card, CardBody } from '@wordpress/components';
import { List } from '@woocommerce/components';
import { getAdminLink } from '@woocommerce/wc-admin-settings';
@ -11,10 +11,26 @@ import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import ProductTemplateModal from './product-template-modal';
const subTasks = [
{
title: __( 'Add manually (recommended)', 'woocommerce-admin' ),
key: 'addProductTemplate',
title: __( 'Start with a template (recommended)', 'woocommerce-admin' ),
content: __(
'For small stores we recommend adding products manually',
'woocommerce-admin'
),
before: <i className="material-icons-outlined">add_box</i>,
after: <i className="material-icons-outlined">chevron_right</i>,
onClick: () =>
recordEvent( 'tasklist_add_product', {
method: 'product_template',
} ),
},
{
key: 'addProductManually',
title: __( 'Add manually', 'woocommerce-admin' ),
content: __(
'For small stores we recommend adding products manually',
'woocommerce-admin'
@ -28,6 +44,7 @@ const subTasks = [
),
},
{
key: 'importProducts',
title: __( 'Import', 'woocommerce-admin' ),
content: __(
'For larger stores we recommend importing all products at once via CSV file',
@ -42,6 +59,7 @@ const subTasks = [
),
},
{
key: 'migrateProducts',
title: __( 'Migrate', 'woocommerce-admin' ),
content: __(
'For stores currently selling elsewhere we suggest using a product migration service',
@ -57,16 +75,33 @@ const subTasks = [
},
];
export default class Products extends Component {
render() {
return (
<Fragment>
<Card className="woocommerce-task-card">
<CardBody size={ null }>
<List items={ subTasks } />
</CardBody>
</Card>
</Fragment>
);
}
export default function Products() {
const [ selectTemplate, setSelectTemplate ] = useState( null );
const onTaskClick = ( task ) => {
task.onClick();
if ( task.key === 'addProductTemplate' ) {
setSelectTemplate( true );
}
};
const listItems = subTasks.map( ( task ) => ( {
...task,
onClick: () => onTaskClick( task ),
} ) );
return (
<Fragment>
<Card className="woocommerce-task-card">
<CardBody size={ null }>
<List items={ listItems } />
</CardBody>
</Card>
{ selectTemplate ? (
<ProductTemplateModal
onClose={ () => setSelectTemplate( null ) }
/>
) : null }
</Fragment>
);
}

View File

@ -0,0 +1,90 @@
/**
* External dependencies
*/
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
/**
* Internal dependencies
*/
import { Products, ProductTemplateModal } from '../tasks/products';
describe( 'products', () => {
describe( 'Products', () => {
afterEach( () => jest.clearAllMocks() );
it( 'should render 4 different options to add products', () => {
render( <Products /> );
expect(
screen.queryByText( 'Start with a template (recommended)' )
).toBeInTheDocument();
expect( screen.queryByText( 'Add manually' ) ).toBeInTheDocument();
expect( screen.queryByText( 'Import' ) ).toBeInTheDocument();
expect( screen.queryByText( 'Migrate' ) ).toBeInTheDocument();
} );
it( 'should not render the product template modal right away', () => {
render( <Products /> );
expect( screen.queryByText( '[ProductTemplateModal]' ) ).toBeNull();
} );
it( 'should render product template modal when start with template task is selected', () => {
render( <Products /> );
fireEvent(
screen.queryByText( 'Start with a template (recommended)' ),
// eslint-disable-next-line no-undef
new MouseEvent( 'click', { bubbles: true } )
);
expect(
screen.queryByText( 'Physical product' )
).toBeInTheDocument();
expect(
screen.queryByText( 'Digital product' )
).toBeInTheDocument();
expect(
screen.queryByText( 'Variable product' )
).toBeInTheDocument();
} );
it( 'should allow the user to close the template modal', () => {
render( <Products /> );
fireEvent(
screen.queryByText( 'Start with a template (recommended)' ),
// eslint-disable-next-line no-undef
new MouseEvent( 'click', { bubbles: true } )
);
expect(
screen.queryByText( 'Physical product' )
).toBeInTheDocument();
const closeButton = screen.getByRole( 'button', {
name: 'Close dialog',
} );
fireEvent.click( closeButton );
expect(
screen.queryByText( 'Physical product' )
).not.toBeInTheDocument();
} );
} );
describe( 'ProductWithTemplate', () => {
afterEach( () => jest.clearAllMocks() );
it( 'should render 3 different product types', () => {
render( <ProductTemplateModal /> );
expect(
screen.queryByText( 'Physical product' )
).toBeInTheDocument();
expect(
screen.queryByText( 'Digital product' )
).toBeInTheDocument();
expect(
screen.queryByText( 'Variable product' )
).toBeInTheDocument();
} );
} );
} );

View File

@ -1,5 +1,4 @@
List
===
# List
List component to display a list of items.
@ -29,23 +28,33 @@ const listItems = [
},
];
<List items={ listItems } />
<List items={ listItems } />;
```
If you wanted a different format for the individual list item you can pass in a functional child:
```
<List items={ listItems } >
{
(item, index) => <div className="woocommerce-list__item-inner">{item.title}</div>
}
</List>
```
### Props
Name | Type | Default | Description
--- | --- | --- | ---
`className` | String | `null` | Additional class name to style the component
`items` | Array | `null` | (required) An array of list items
| Name | Type | Default | Description |
| ----------- | ------ | ------- | -------------------------------------------- |
| `className` | String | `null` | Additional class name to style the component |
| `items` | Array | `null` | (required) An array of list items |
`items` structure:
* `after`: ReactNode - Content displayed after the list item text.
* `before`: ReactNode - Content displayed before the list item text.
* `className`: String - Additional class name to style the list item.
* `description`: String - Description displayed beneath the list item title.
* `href`: String - Href attribute used in a Link wrapped around the item.
* `onClick`: Function - Called when the list item is clicked.
* `target`: String - Target attribute used for Link wrapper.
* `title`: String - Title displayed for the list item.
- `after`: ReactNode - Content displayed after the list item text.
- `before`: ReactNode - Content displayed before the list item text.
- `className`: String - Additional class name to style the list item.
- `description`: String - Description displayed beneath the list item title.
- `href`: String - Href attribute used in a Link wrapped around the item.
- `onClick`: Function - Called when the list item is clicked.
- `target`: String - Target attribute used for Link wrapper.
- `title`: String - Title displayed for the list item.

View File

@ -2,119 +2,54 @@
* External dependencies
*/
import classnames from 'classnames';
import { Component } from '@wordpress/element';
import { ENTER } from '@wordpress/keycodes';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
/**
* Internal dependencies
*/
import Link from '../link';
import ListItem from './list-item';
/**
* List component to display a list of items.
*
* @param {Object} props props for list
*/
class List extends Component {
handleKeyDown( event, onClick ) {
if ( typeof onClick === 'function' && event.keyCode === ENTER ) {
onClick();
}
}
function List( props ) {
const { className, items, children } = props;
const listClassName = classnames( 'woocommerce-list', className );
getItemLinkType( item ) {
const { href, linkType } = item;
return (
<TransitionGroup component="ul" className={ listClassName } role="menu">
{ items.map( ( item, index ) => {
const { className: itemClasses, href, key, onClick } = item;
const hasAction = typeof onClick === 'function' || href;
const itemClassName = classnames(
'woocommerce-list__item',
itemClasses,
{
'has-action': hasAction,
}
);
if ( linkType ) {
return linkType;
}
return href ? 'external' : null;
}
render() {
const { className, items } = this.props;
const listClassName = classnames( 'woocommerce-list', className );
return (
<TransitionGroup
component="ul"
className={ listClassName }
role="menu"
>
{ items.map( ( item, index ) => {
const {
after,
before,
className: itemClasses,
content,
href,
key,
listItemTag,
onClick,
target,
title,
} = item;
const hasAction = typeof onClick === 'function' || href;
const itemClassName = classnames(
'woocommerce-list__item',
itemClasses,
{
'has-action': hasAction,
}
);
const InnerTag = href ? Link : 'div';
const innerTagProps = {
className: 'woocommerce-list__item-inner',
onClick: typeof onClick === 'function' ? onClick : null,
'aria-disabled': hasAction ? 'false' : null,
tabIndex: hasAction ? '0' : null,
role: hasAction ? 'menuitem' : null,
onKeyDown: ( e ) =>
hasAction ? this.handleKeyDown( e, onClick ) : null,
target: href ? target : null,
type: this.getItemLinkType( item ),
href,
'data-list-item-tag': listItemTag,
};
return (
<CSSTransition
key={ key || index }
timeout={ 500 }
classNames="woocommerce-list__item"
>
<li className={ itemClassName }>
<InnerTag { ...innerTagProps }>
{ before && (
<div className="woocommerce-list__item-before">
{ before }
</div>
) }
<div className="woocommerce-list__item-text">
<span className="woocommerce-list__item-title">
{ title }
</span>
{ content && (
<span className="woocommerce-list__item-content">
{ content }
</span>
) }
</div>
{ after && (
<div className="woocommerce-list__item-after">
{ after }
</div>
) }
</InnerTag>
</li>
</CSSTransition>
);
} ) }
</TransitionGroup>
);
}
return (
<CSSTransition
key={ key || index }
timeout={ 500 }
classNames="woocommerce-list__item"
>
<li className={ itemClassName }>
{ children ? (
children( item, index )
) : (
<ListItem item={ item } />
) }
</li>
</CSSTransition>
);
} ) }
</TransitionGroup>
);
}
List.propTypes = {
@ -162,6 +97,10 @@ List.propTypes = {
* Title displayed for the list item.
*/
title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
/**
* Unique key for list item.
*/
key: PropTypes.string,
} )
).isRequired,
};

View File

@ -0,0 +1,121 @@
/**
* External dependencies
*/
import { ENTER } from '@wordpress/keycodes';
import PropTypes from 'prop-types';
/**
* Internal dependencies
*/
import Link from '../link';
function handleKeyDown( event, onClick ) {
if ( typeof onClick === 'function' && event.keyCode === ENTER ) {
onClick();
}
}
function getItemLinkType( item ) {
const { href, linkType } = item;
if ( linkType ) {
return linkType;
}
return href ? 'external' : null;
}
/**
* List component to display a list of items.
*
* @param {Object} props props for list item
*/
function ListItem( props ) {
const { item } = props;
const {
before,
title,
after,
content,
onClick,
href,
target,
listItemTag,
} = item;
const hasAction = typeof onClick === 'function' || href;
const InnerTag = href ? Link : 'div';
const innerTagProps = {
className: 'woocommerce-list__item-inner',
onClick: typeof onClick === 'function' ? onClick : null,
'aria-disabled': hasAction ? 'false' : null,
tabIndex: hasAction ? '0' : null,
role: hasAction ? 'menuitem' : null,
onKeyDown: ( e ) => ( hasAction ? handleKeyDown( e, onClick ) : null ),
target: href ? target : null,
type: getItemLinkType( item ),
href,
'data-list-item-tag': listItemTag,
};
return (
<InnerTag { ...innerTagProps }>
{ before && (
<div className="woocommerce-list__item-before">{ before }</div>
) }
<div className="woocommerce-list__item-text">
<span className="woocommerce-list__item-title">{ title }</span>
{ content && (
<span className="woocommerce-list__item-content">
{ content }
</span>
) }
</div>
{ after && (
<div className="woocommerce-list__item-after">{ after }</div>
) }
</InnerTag>
);
}
ListItem.propTypes = {
/**
* An array of list items.
*/
item: PropTypes.shape( {
/**
* Content displayed after the list item text.
*/
after: PropTypes.node,
/**
* Content displayed before the list item text.
*/
before: PropTypes.node,
/**
* Additional class name to style the list item.
*/
className: PropTypes.string,
/**
* Content displayed beneath the list item title.
*/
content: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
/**
* Href attribute used in a Link wrapped around the item.
*/
href: PropTypes.string,
/**
* Called when the list item is clicked.
*/
onClick: PropTypes.func,
/**
* Target attribute used for Link wrapper.
*/
target: PropTypes.string,
/**
* Title displayed for the list item.
*/
title: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
} ).isRequired,
};
export default ListItem;

View File

@ -2,12 +2,13 @@
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { NAMESPACE } from '../constants';
import { NAMESPACE, WC_ADMIN_NAMESPACE } from '../constants';
export function setItem( itemType, id, item ) {
return {
@ -64,7 +65,6 @@ export function* updateProductStock( product, quantity ) {
default:
url += `/products/${ id }`;
}
try {
yield apiFetch( {
path: url,
@ -79,3 +79,22 @@ export function* updateProductStock( product, quantity ) {
return false;
}
}
export function* createProductFromTemplate( itemFields, query ) {
try {
const url = addQueryArgs(
`${ WC_ADMIN_NAMESPACE }/onboarding/tasks/create_product_from_template`,
query || {}
);
const newItem = yield apiFetch( {
path: url,
method: 'POST',
data: itemFields,
} );
yield setItem( 'products', newItem.id, newItem );
return newItem;
} catch ( error ) {
yield setError( 'createProductFromTemplate', query, error );
throw error;
}
}

View File

@ -15,14 +15,15 @@ const reducer = (
) => {
switch ( type ) {
case TYPES.SET_ITEM:
const itemData = state.data[ itemType ] || {};
return {
...state,
data: {
...state.data,
[ itemType ]: {
...state.data[ itemType ],
...itemData,
[ id ]: {
...state.data[ itemType ][ id ],
...( itemData[ id ] || {} ),
...item,
},
},

View File

@ -80,6 +80,7 @@ Release and roadmap notes are available on the [WooCommerce Developers Blog](htt
- Add: new inbox message - Getting started in Ecommerce - watch this webinar. #6086
- Add: Remote inbox notifications contains comparison and fix product rule. #6073
- Update: store deprecation welcome modal support doc link #6094
- Enhancement: Allowing users to create products by selecting a template. #5892
== Changelog ==

View File

@ -43,7 +43,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'import_sample_products' ),
'permission_callback' => array( $this, 'import_products_permission_check' ),
'permission_callback' => array( $this, 'create_products_permission_check' ),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
@ -74,6 +74,29 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
'schema' => array( $this, 'get_status_item_schema' ),
)
);
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/create_product_from_template',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => array( $this, 'create_product_from_template' ),
'permission_callback' => array( $this, 'create_products_permission_check' ),
'args' => array_merge(
$this->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE ),
array(
'template_name' => array(
'required' => true,
'type' => 'string',
'description' => __( 'Product template name.', 'woocommerce-admin' ),
),
)
),
),
'schema' => array( $this, 'get_public_item_schema' ),
)
);
}
/**
@ -82,7 +105,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
* @param WP_REST_Request $request Full details about the request.
* @return WP_Error|boolean
*/
public function import_products_permission_check( $request ) {
public function create_products_permission_check( $request ) {
if ( ! wc_rest_check_post_permissions( 'product', 'create' ) ) {
return new \WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce-admin' ), array( 'status' => rest_authorization_required_code() ) );
}
@ -119,32 +142,78 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
}
/**
* Import sample products from WooCommerce sample CSV.
* Import sample products from given CSV path.
*
* @param string $csv_file CSV file path.
* @return WP_Error|WP_REST_Response
*/
public static function import_sample_products() {
public static function import_sample_products_from_csv( $csv_file ) {
include_once WC_ABSPATH . 'includes/import/class-wc-product-csv-importer.php';
$file = WC_ABSPATH . 'sample-data/sample_products.csv';
if ( file_exists( $file ) && class_exists( 'WC_Product_CSV_Importer' ) ) {
if ( file_exists( $csv_file ) && class_exists( 'WC_Product_CSV_Importer' ) ) {
// Override locale so we can return mappings from WooCommerce in English language stores.
add_filter( 'locale', '__return_false', 9999 );
$importer_class = apply_filters( 'woocommerce_product_csv_importer_class', 'WC_Product_CSV_Importer' );
$args = array(
'parse' => true,
'mapping' => self::get_header_mappings( $file ),
'mapping' => self::get_header_mappings( $csv_file ),
);
$args = apply_filters( 'woocommerce_product_csv_importer_args', $args, $importer_class );
$importer = new $importer_class( $file, $args );
$importer = new $importer_class( $csv_file, $args );
$import = $importer->import();
return rest_ensure_response( $import );
return $import;
} else {
return new \WP_Error( 'woocommerce_rest_import_error', __( 'Sorry, the sample products data file was not found.', 'woocommerce-admin' ) );
}
}
/**
* Import sample products from WooCommerce sample CSV.
*
* @return WP_Error|WP_REST_Response
*/
public static function import_sample_products() {
$sample_csv_file = WC_ABSPATH . 'sample-data/sample_products.csv';
$import = self::import_sample_products_from_csv( $sample_csv_file );
return rest_ensure_response( $import );
}
/**
* Creates a product from a template name passed in through the template_name param.
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response|WP_Error
*/
public static function create_product_from_template( $request ) {
$template_name = $request->get_param( 'template_name' );
$template_path = __DIR__ . '/Templates/' . $template_name . '_product.csv';
$template_path = apply_filters( 'woocommerce_product_template_csv_file_path', $template_path, $template_name );
$import = self::import_sample_products_from_csv( $template_path );
if ( is_wp_error( $import ) || 0 === count( $import['imported'] ) ) {
return new \WP_Error(
'woocommerce_rest_product_creation_error',
/* translators: %s is template name */
__( 'Sorry, creating the product with template failed.', 'woocommerce-admin' ),
array( 'status' => 500 )
);
}
$product = wc_get_product( $import['imported'][0] );
$product->set_status( 'auto-draft' );
$product->save();
return rest_ensure_response(
array(
'id' => $product->get_id(),
)
);
}
/**
* Get header mappings from CSV columns.
*

View File

@ -0,0 +1,2 @@
Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
"simple, downloadable, virtual",Album,0,0,visible,"This is a simple, virtual product.","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa.",,,taxable,,1,,0,0,,,,,1,,,15,Music,,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg,1,1,,,,,,,0,,,,,,,,,1,"Single 1",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg,"Single 2",https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg
1 Type Name Published Is featured? Visibility in catalog Short description Description Date sale price starts Date sale price ends Tax status Tax class In stock? Stock Backorders allowed? Sold individually? Weight (lbs) Length (in) Width (in) Height (in) Allow customer reviews? Purchase note Sale price Regular price Categories Tags Shipping class Images Download limit Download expiry days Parent Grouped products Upsells Cross-sells External URL Button text Position Attribute 1 name Attribute 1 value(s) Attribute 1 visible Attribute 1 global Attribute 2 name Attribute 2 value(s) Attribute 2 visible Attribute 2 global Meta: _wpcom_is_markdown Download 1 name Download 1 URL Download 2 name Download 2 URL
2 simple, downloadable, virtual Album 0 0 visible This is a simple, virtual product. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sagittis orci ac odio dictum tincidunt. Donec ut metus leo. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed luctus, dui eu sagittis sodales, nulla nibh sagittis augue, vel porttitor diam enim non metus. Vestibulum aliquam augue neque. Phasellus tincidunt odio eget ullamcorper efficitur. Cras placerat ut turpis pellentesque vulputate. Nam sed consequat tortor. Curabitur finibus sapien dolor. Ut eleifend tellus nec erat pulvinar dignissim. Nam non arcu purus. Vivamus et massa massa. taxable 1 0 0 1 15 Music https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/album-1.jpg 1 1 0 1 Single 1 https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/single.jpg Single 2 https://demo.woothemes.com/woocommerce/wp-content/uploads/sites/56/2017/08/album.jpg

View File

@ -0,0 +1,2 @@
Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
simple,"Hoodie with Logo",0,0,visible,"This is a simple product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,2,10,6,3,1,,,45,"Clothing > Hoodies",,,https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg,,,,,,,,,0,Color,Blue,1,1,,,,,1,,,,
1 Type Name Published Is featured? Visibility in catalog Short description Description Date sale price starts Date sale price ends Tax status Tax class In stock? Stock Backorders allowed? Sold individually? Weight (lbs) Length (in) Width (in) Height (in) Allow customer reviews? Purchase note Sale price Regular price Categories Tags Shipping class Images Download limit Download expiry days Parent Grouped products Upsells Cross-sells External URL Button text Position Attribute 1 name Attribute 1 value(s) Attribute 1 visible Attribute 1 global Attribute 2 name Attribute 2 value(s) Attribute 2 visible Attribute 2 global Meta: _wpcom_is_markdown Download 1 name Download 1 URL Download 2 name Download 2 URL
2 simple Hoodie with Logo 0 0 visible This is a simple product. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. taxable 1 0 0 2 10 6 3 1 45 Clothing > Hoodies https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/hoodie-with-logo-2.jpg 0 Color Blue 1 1 1

View File

@ -0,0 +1,2 @@
Type,Name,Published,"Is featured?","Visibility in catalog","Short description",Description,"Date sale price starts","Date sale price ends","Tax status","Tax class","In stock?",Stock,"Backorders allowed?","Sold individually?","Weight (lbs)","Length (in)","Width (in)","Height (in)","Allow customer reviews?","Purchase note","Sale price","Regular price",Categories,Tags,"Shipping class",Images,"Download limit","Download expiry days",Parent,"Grouped products",Upsells,Cross-sells,"External URL","Button text",Position,"Attribute 1 name","Attribute 1 value(s)","Attribute 1 visible","Attribute 1 global","Attribute 2 name","Attribute 2 value(s)","Attribute 2 visible","Attribute 2 global","Meta: _wpcom_is_markdown","Download 1 name","Download 1 URL","Download 2 name","Download 2 URL"
variable,"V-Neck T-Shirt",0,1,visible,"This is a variable product.","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo.",,,taxable,,1,,0,0,.5,24,1,2,1,,,,"Clothing > Tshirts",,,"https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg",,,,,,,,,0,Color,"Blue, Green, Red",1,1,Size,"Large, Medium, Small",1,1,1,,,,
1 Type Name Published Is featured? Visibility in catalog Short description Description Date sale price starts Date sale price ends Tax status Tax class In stock? Stock Backorders allowed? Sold individually? Weight (lbs) Length (in) Width (in) Height (in) Allow customer reviews? Purchase note Sale price Regular price Categories Tags Shipping class Images Download limit Download expiry days Parent Grouped products Upsells Cross-sells External URL Button text Position Attribute 1 name Attribute 1 value(s) Attribute 1 visible Attribute 1 global Attribute 2 name Attribute 2 value(s) Attribute 2 visible Attribute 2 global Meta: _wpcom_is_markdown Download 1 name Download 1 URL Download 2 name Download 2 URL
2 variable V-Neck T-Shirt 0 1 visible This is a variable product. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Mauris placerat eleifend leo. taxable 1 0 0 .5 24 1 2 1 Clothing > Tshirts https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vneck-tee-2.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-green-1.jpg, https://woocommercecore.mystagingwebsite.com/wp-content/uploads/2017/12/vnech-tee-blue-1.jpg 0 Color Blue, Green, Red 1 1 Size Large, Medium, Small 1 1 1

View File

@ -32,12 +32,27 @@ class WC_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
);
}
/**
* Remove product attributes that where created in previous tests.
*/
public function clear_product_attribute_taxonomies() {
$taxonomies = get_taxonomies();
foreach ( (array) $taxonomies as $taxonomy ) {
// pa - product attribute.
if ( 'pa_' === substr( $taxonomy, 0, 3 ) ) {
unregister_taxonomy( $taxonomy );
}
}
}
/**
* Test that sample product data is imported.
*/
public function test_import_sample_products() {
wp_set_current_user( $this->user );
$this->clear_product_attribute_taxonomies();
$request = new WP_REST_Request( 'POST', $this->endpoint . '/import_sample_products' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
@ -45,9 +60,60 @@ class WC_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'failed', $data );
$this->assertEquals( 0, count( $data['failed'] ) );
$this->assertArrayHasKey( 'imported', $data );
$this->assertArrayHasKey( 'skipped', $data );
// There might be previous products present.
if ( 0 === count( $data['skipped'] ) ) {
$this->assertGreaterThan( 10, count( $data['imported'] ) );
}
$this->assertArrayHasKey( 'updated', $data );
$this->assertEquals( 0, count( $data['updated'] ) );
}
/**
* Test creating a product from a template name.
*/
public function test_create_product_from_template() {
wp_set_current_user( $this->user );
$this->clear_product_attribute_taxonomies();
$request = new WP_REST_Request( 'POST', $this->endpoint . '/create_product_from_template' );
$request->set_param( 'template_name', 'physical' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'id', $data );
$product = wc_get_product( $data['id'] );
$this->assertEquals( 'auto-draft', $product->get_status() );
$this->assertEquals( 'simple', $product->get_type() );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/create_product_from_template' );
$request->set_param( 'template_name', 'digital' );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'id', $data );
$product = wc_get_product( $data['id'] );
$this->assertEquals( 'auto-draft', $product->get_status() );
$this->assertEquals( 'simple', $product->get_type() );
}
/**
* Test that we get an error when template_name does not exist.
*/
public function test_create_product_from_wrong_template_name() {
wp_set_current_user( $this->user );
$request = new WP_REST_Request( 'POST', $this->endpoint . '/create_product_from_template' );
$request->set_param( 'template_name', 'random' );
$response = $this->server->dispatch( $request );
$this->assertEquals( 500, $response->get_status() );
}
/**
@ -85,4 +151,6 @@ class WC_Tests_API_Onboarding_Tasks extends WC_REST_Unit_Test_Case {
$this->assertSame( 'Custom post content', get_the_content( null, null, $data['post_id'] ) );
}
}