diff --git a/plugins/woocommerce-admin/client/task-list/tasks.js b/plugins/woocommerce-admin/client/task-list/tasks.js index 01e56268b02..e142e2488e6 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks.js +++ b/plugins/woocommerce-admin/client/task-list/tasks.js @@ -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'; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/products/index.js b/plugins/woocommerce-admin/client/task-list/tasks/products/index.js new file mode 100644 index 00000000000..7b97442b8f3 --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/products/index.js @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import Products from './products'; +import ProductTemplateModal from './product-template-modal'; + +export { Products, ProductTemplateModal }; diff --git a/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.js b/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.js new file mode 100644 index 00000000000..39b9dee4be6 --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.js @@ -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 ( + onClose() } + className="woocommerce-product-template-modal" + > +
+
+ + { ( item, index ) => ( +
+ + +
+ ) } +
+
+
+ +
+
+
+ ); +} diff --git a/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.scss b/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.scss new file mode 100644 index 00000000000..c88d1c31d5b --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/tasks/products/product-template-modal.scss @@ -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; +} diff --git a/plugins/woocommerce-admin/client/task-list/tasks/products.js b/plugins/woocommerce-admin/client/task-list/tasks/products/products.js similarity index 58% rename from plugins/woocommerce-admin/client/task-list/tasks/products.js rename to plugins/woocommerce-admin/client/task-list/tasks/products/products.js index 0591f0b1644..7d0717503f0 100644 --- a/plugins/woocommerce-admin/client/task-list/tasks/products.js +++ b/plugins/woocommerce-admin/client/task-list/tasks/products/products.js @@ -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: add_box, + after: chevron_right, + 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 ( - - - - - - - - ); - } +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 ( + + + + + + + { selectTemplate ? ( + setSelectTemplate( null ) } + /> + ) : null } + + ); } diff --git a/plugins/woocommerce-admin/client/task-list/test/products.js b/plugins/woocommerce-admin/client/task-list/test/products.js new file mode 100644 index 00000000000..1a93fc775bd --- /dev/null +++ b/plugins/woocommerce-admin/client/task-list/test/products.js @@ -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( ); + + 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( ); + + expect( screen.queryByText( '[ProductTemplateModal]' ) ).toBeNull(); + } ); + + it( 'should render product template modal when start with template task is selected', () => { + render( ); + + 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( ); + + 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( ); + + expect( + screen.queryByText( 'Physical product' ) + ).toBeInTheDocument(); + expect( + screen.queryByText( 'Digital product' ) + ).toBeInTheDocument(); + expect( + screen.queryByText( 'Variable product' ) + ).toBeInTheDocument(); + } ); + } ); +} ); diff --git a/plugins/woocommerce-admin/packages/components/src/list/README.md b/plugins/woocommerce-admin/packages/components/src/list/README.md index 6005f5b9077..b80ebc67aab 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/README.md +++ b/plugins/woocommerce-admin/packages/components/src/list/README.md @@ -1,5 +1,4 @@ -List -=== +# List List component to display a list of items. @@ -29,23 +28,33 @@ const listItems = [ }, ]; - +; +``` + +If you wanted a different format for the individual list item you can pass in a functional child: + +``` + +{ + (item, index) =>
{item.title}
+} +
``` ### 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. \ No newline at end of file +- `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. diff --git a/plugins/woocommerce-admin/packages/components/src/list/index.js b/plugins/woocommerce-admin/packages/components/src/list/index.js index 777fbd15213..4503592a706 100644 --- a/plugins/woocommerce-admin/packages/components/src/list/index.js +++ b/plugins/woocommerce-admin/packages/components/src/list/index.js @@ -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 ( + + { 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 ( - - { 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 ( - -
  • - - { before && ( -
    - { before } -
    - ) } -
    - - { title } - - { content && ( - - { content } - - ) } -
    - { after && ( -
    - { after } -
    - ) } -
    -
  • -
    - ); - } ) } -
    - ); - } + return ( + +
  • + { children ? ( + children( item, index ) + ) : ( + + ) } +
  • +
    + ); + } ) } +
    + ); } 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, }; diff --git a/plugins/woocommerce-admin/packages/components/src/list/list-item.js b/plugins/woocommerce-admin/packages/components/src/list/list-item.js new file mode 100644 index 00000000000..d521682baba --- /dev/null +++ b/plugins/woocommerce-admin/packages/components/src/list/list-item.js @@ -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 ( + + { before && ( +
    { before }
    + ) } +
    + { title } + { content && ( + + { content } + + ) } +
    + { after && ( +
    { after }
    + ) } +
    + ); +} + +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; diff --git a/plugins/woocommerce-admin/packages/data/src/items/actions.js b/plugins/woocommerce-admin/packages/data/src/items/actions.js index 26ed1621d24..71662a367f9 100644 --- a/plugins/woocommerce-admin/packages/data/src/items/actions.js +++ b/plugins/woocommerce-admin/packages/data/src/items/actions.js @@ -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; + } +} diff --git a/plugins/woocommerce-admin/packages/data/src/items/reducer.js b/plugins/woocommerce-admin/packages/data/src/items/reducer.js index 3f6769cfadb..60707994250 100644 --- a/plugins/woocommerce-admin/packages/data/src/items/reducer.js +++ b/plugins/woocommerce-admin/packages/data/src/items/reducer.js @@ -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, }, }, diff --git a/plugins/woocommerce-admin/readme.txt b/plugins/woocommerce-admin/readme.txt index cd9780ae78e..62711d7fab8 100644 --- a/plugins/woocommerce-admin/readme.txt +++ b/plugins/woocommerce-admin/readme.txt @@ -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 == diff --git a/plugins/woocommerce-admin/src/API/OnboardingTasks.php b/plugins/woocommerce-admin/src/API/OnboardingTasks.php index fc193fb7f5b..4b1ddf07156 100644 --- a/plugins/woocommerce-admin/src/API/OnboardingTasks.php +++ b/plugins/woocommerce-admin/src/API/OnboardingTasks.php @@ -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. * diff --git a/plugins/woocommerce-admin/src/API/Templates/digital_product.csv b/plugins/woocommerce-admin/src/API/Templates/digital_product.csv new file mode 100644 index 00000000000..be171a918e4 --- /dev/null +++ b/plugins/woocommerce-admin/src/API/Templates/digital_product.csv @@ -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 diff --git a/plugins/woocommerce-admin/src/API/Templates/physical_product.csv b/plugins/woocommerce-admin/src/API/Templates/physical_product.csv new file mode 100644 index 00000000000..b33e258acbf --- /dev/null +++ b/plugins/woocommerce-admin/src/API/Templates/physical_product.csv @@ -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,,,, diff --git a/plugins/woocommerce-admin/src/API/Templates/variable_product.csv b/plugins/woocommerce-admin/src/API/Templates/variable_product.csv new file mode 100644 index 00000000000..9a221322aaa --- /dev/null +++ b/plugins/woocommerce-admin/src/API/Templates/variable_product.csv @@ -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,,,, diff --git a/plugins/woocommerce-admin/tests/api/onboarding-tasks.php b/plugins/woocommerce-admin/tests/api/onboarding-tasks.php index 3da7e321a3c..dec71b05c8c 100644 --- a/plugins/woocommerce-admin/tests/api/onboarding-tasks.php +++ b/plugins/woocommerce-admin/tests/api/onboarding-tasks.php @@ -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'] ) ); } + + }