Convert simple to variable product and auto add variations (#39673)

* Auto create variations and move product to variable when adding variation options

* Delete unused components

* Add tour to variation options

* Remove unneeded options

* Add changelog

* Fix types

* Fix lint errors

* Fix broken tests

* FIlter out option tags when not included in attributes

* Don't invalidate variations data when no new variations are created
This commit is contained in:
louwie17 2023-08-11 11:15:05 -03:00 committed by GitHub
parent 05dfaacd37
commit 71e8b699db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 429 additions and 337 deletions

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Add new user preference to UserPreferences type.

View File

@ -26,6 +26,7 @@ export type UserPreferences = {
}; };
taxes_report_columns?: string; taxes_report_columns?: string;
variable_product_tour_shown?: string; variable_product_tour_shown?: string;
variable_product_block_tour_shown?: string;
variations_report_columns?: string; variations_report_columns?: string;
}; };

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add isInSelectedTab context to tab blocks for use in nested blocks.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Update variation options block to auto create variations upon options update.

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { CatalogVisibilityBlockAttributes } from './types'; import { CatalogVisibilityBlockAttributes } from './types';

View File

@ -1,11 +1,16 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import initBlock from '../../utils/init-block'; import { initBlock } from '../../utils/init-block';
import metadata from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
const { name } = metadata; const { name, ...metadata } = blockConfiguration as BlockConfiguration;
export { metadata, name }; export { metadata, name };

View File

@ -1,11 +1,16 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import initBlock from '../../utils/init-block'; import { initBlock } from '../../utils/init-block';
import metadata from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
const { name } = metadata; const { name, ...metadata } = blockConfiguration as BlockConfiguration;
export { metadata, name }; export { metadata, name };

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { InventoryEmailBlockAttributes } from './types'; import { InventoryEmailBlockAttributes } from './types';

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { TrackInventoryBlockAttributes } from './types'; import { TrackInventoryBlockAttributes } from './types';

View File

@ -1,11 +1,16 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import initBlock from '../../utils/init-block'; import { initBlock } from '../../utils/init-block';
import metadata from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
const { name } = metadata; const { name, ...metadata } = blockConfiguration as BlockConfiguration;
export { metadata, name }; export { metadata, name };

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { RequirePasswordBlockAttributes } from './types'; import { RequirePasswordBlockAttributes } from './types';

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { PricingBlockAttributes } from './types'; import { PricingBlockAttributes } from './types';

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { RadioBlockAttributes } from './types'; import { RadioBlockAttributes } from './types';

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { SalePriceBlockAttributes } from './types'; import { SalePriceBlockAttributes } from './types';

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { SalePriceBlockAttributes } from './types'; import { SalePriceBlockAttributes } from './types';

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { ScheduleSalePricingBlockAttributes } from './types'; import { ScheduleSalePricingBlockAttributes } from './types';

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { SectionBlockAttributes } from './types'; import { SectionBlockAttributes } from './types';

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { ShippingClassBlockAttributes } from './types'; import { ShippingClassBlockAttributes } from './types';

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { ShippingDimensionsBlockAttributes } from './types'; import { ShippingDimensionsBlockAttributes } from './types';

View File

@ -27,6 +27,9 @@
"lock": false, "lock": false,
"__experimentalToolbar": false "__experimentalToolbar": false
}, },
"providesContext": {
"isInSelectedTab": "isSelected"
},
"usesContext": [ "selectedTab" ], "usesContext": [ "selectedTab" ],
"editorStyle": "file:./editor.css", "editorStyle": "file:./editor.css",
"templateLock": "contentOnly" "templateLock": "contentOnly"

View File

@ -4,25 +4,35 @@
import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; import { InnerBlocks, useBlockProps } from '@wordpress/block-editor';
import classnames from 'classnames'; import classnames from 'classnames';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
import type { BlockAttributes } from '@wordpress/blocks'; import type { BlockAttributes, BlockEditProps } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { TabButton } from './tab-button'; import { TabButton } from './tab-button';
export interface TabBlockAttributes extends BlockAttributes {
id: string;
title: string;
order: number;
isSelected?: boolean;
}
export function Edit( { export function Edit( {
setAttributes,
attributes, attributes,
context, context,
}: { }: BlockEditProps< TabBlockAttributes > & {
attributes: BlockAttributes;
context?: { context?: {
selectedTab?: string | null; selectedTab?: string | null;
}; };
} ) { } ) {
const blockProps = useBlockProps(); const blockProps = useBlockProps();
const { id, title, order } = attributes; const { id, title, order, isSelected: contextIsSelected } = attributes;
const isSelected = context?.selectedTab === id; const isSelected = context?.selectedTab === id;
if ( isSelected !== contextIsSelected ) {
setAttributes( { isSelected } );
}
const classes = classnames( 'wp-block-woocommerce-product-tab__content', { const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
'is-selected': isSelected, 'is-selected': isSelected,

View File

@ -1,17 +1,25 @@
/**
* External dependencies
*/
import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import initBlock from '../../utils/init-block'; import { initBlock } from '../../utils/init-block';
import metadata from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit, TabBlockAttributes } from './edit';
const { name } = metadata; const { name, ...metadata } =
blockConfiguration as BlockConfiguration< TabBlockAttributes >;
export { metadata, name }; export { metadata, name };
export const settings = { export const settings: Partial< BlockConfiguration< TabBlockAttributes > > = {
example: {}, example: {},
edit: Edit, edit: Edit,
}; };
export const init = () => initBlock( { name, metadata, settings } ); export function init() {
initBlock( { name, metadata, settings } );
}

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { ToggleBlockAttributes } from './types'; import { ToggleBlockAttributes } from './types';

View File

@ -22,5 +22,6 @@
"lock": false, "lock": false,
"__experimentalToolbar": false "__experimentalToolbar": false
}, },
"usesContext": [ "isInSelectedTab" ],
"editorStyle": "file:./editor.css" "editorStyle": "file:./editor.css"
} }

View File

@ -2,19 +2,29 @@
* External dependencies * External dependencies
*/ */
import { useBlockProps } from '@wordpress/block-editor'; import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { VariationsTable } from '../../components/variations-table'; import { VariationsTable } from '../../components/variations-table';
import { VariationOptionsBlockAttributes } from './types';
import { VariableProductTour } from './variable-product-tour';
export function Edit() { export function Edit( {
context,
}: BlockEditProps< VariationOptionsBlockAttributes > & {
context?: {
isInSelectedTab?: boolean;
};
} ) {
const blockProps = useBlockProps(); const blockProps = useBlockProps();
return ( return (
<div { ...blockProps }> <div { ...blockProps }>
<VariationsTable /> <VariationsTable />
{ context?.isInSelectedTab && <VariableProductTour /> }
</div> </div>
); );
} }

View File

@ -1,2 +1,13 @@
.wp-block-woocommerce-product-variations-items-field { .wp-block-woocommerce-product-variations-items-field {
} }
.variation-items-product-tour {
.tour-kit-spotlight {
border-radius: $gap-smaller;
padding: $gap-large;
}
.tour-kit-frame__container,
.woocommerce-tour-kit-step {
border-radius: $gap-smaller;
}
}

View File

@ -6,7 +6,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { VariationOptionsBlockAttributes } from './types'; import { VariationOptionsBlockAttributes } from './types';

View File

@ -0,0 +1,138 @@
/**
* External dependencies
*/
import { createElement, useEffect, useRef, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { TourKit, TourKitTypes } from '@woocommerce/components';
import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
useUserPreferences,
} from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useSelect } from '@wordpress/data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group
import { useEntityId } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { DEFAULT_PER_PAGE_OPTION } from '../../constants';
export const VariableProductTour: React.FC = () => {
const [ isTourOpen, setIsTourOpen ] = useState( false );
const productId = useEntityId( 'postType', 'product' );
const prevTotalCount = useRef< undefined | number >();
const { totalCount } = useSelect(
( select ) => {
const { getProductVariationsTotalCount } = select(
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
);
const requestParams = {
product_id: productId,
page: 1,
per_page: DEFAULT_PER_PAGE_OPTION,
order: 'asc',
orderby: 'menu_order',
};
return {
totalCount:
getProductVariationsTotalCount< number >( requestParams ),
};
},
[ productId ]
);
const {
updateUserPreferences,
variable_product_block_tour_shown: hasShownTour,
} = useUserPreferences();
const config: TourKitTypes.WooConfig = {
placement: 'top',
steps: [
{
referenceElements: {
desktop:
'.wp-block-woocommerce-product-variation-items-field',
},
focusElement: {
desktop:
'.wp-block-woocommerce-product-variation-items-field',
},
meta: {
name: 'product-variations-2',
heading: __(
'⚡️ This product now has variations',
'woocommerce'
),
descriptions: {
desktop: __(
'From now on, youll manage pricing, shipping, and inventory for each variation individually—just like any other product in your store.',
'woocommerce'
),
},
primaryButton: {
text: __( 'Got it', 'woocommerce' ),
},
},
},
],
options: {
classNames: [ 'variation-items-product-tour' ],
// WooTourKit does not handle merging of default options properly,
// so we need to duplicate the effects options here.
effects: {
arrowIndicator: true,
spotlight: {
interactivity: {
enabled: true,
},
},
},
callbacks: {
onStepViewOnce: () => {
recordEvent( 'variable_product_block_tour_shown', {
variable_count: totalCount,
} );
},
},
popperModifiers: [
{
name: 'offset',
options: {
// 24px for additional padding and 8px for arrow.
offset: [ 0, 32 ],
},
},
],
},
closeHandler: () => {
updateUserPreferences( {
variable_product_block_tour_shown: 'yes',
} );
setIsTourOpen( false );
recordEvent( 'variable_product_block_tour_dismissed' );
},
};
useEffect( () => {
const isFirstVariation =
prevTotalCount.current !== totalCount &&
totalCount > 0 &&
prevTotalCount.current === 0;
prevTotalCount.current = totalCount;
if ( isFirstVariation && ! isTourOpen ) {
setIsTourOpen( true );
}
}, [ totalCount ] );
if ( hasShownTour === 'yes' || ! isTourOpen ) {
return null;
}
return <TourKit config={ config } />;
};

View File

@ -23,6 +23,7 @@ import {
useProductAttributes, useProductAttributes,
} from '../../hooks/use-product-attributes'; } from '../../hooks/use-product-attributes';
import { AttributeControl } from '../../components/attribute-control'; import { AttributeControl } from '../../components/attribute-control';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
function manageDefaultAttributes( values: EnhancedProductAttribute[] ) { function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
return values.reduce< Product[ 'default_attributes' ] >( return values.reduce< Product[ 'default_attributes' ] >(
@ -45,6 +46,7 @@ function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
export function Edit() { export function Edit() {
const blockProps = useBlockProps(); const blockProps = useBlockProps();
const { generateProductVariations } = useProductVariationsHelper();
const [ entityAttributes, setEntityAttributes ] = useEntityProp< const [ entityAttributes, setEntityAttributes ] = useEntityProp<
ProductAttribute[] ProductAttribute[]
@ -64,6 +66,7 @@ export function Edit() {
onChange( values ) { onChange( values ) {
setEntityAttributes( values ); setEntityAttributes( values );
setEntityDefaultAttributes( manageDefaultAttributes( values ) ); setEntityDefaultAttributes( manageDefaultAttributes( values ) );
generateProductVariations( values );
}, },
} ); } );

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { VariationOptionsBlockAttributes } from './types'; import { VariationOptionsBlockAttributes } from './types';

View File

@ -34,6 +34,7 @@ import {
useProductAttributes, useProductAttributes,
} from '../../hooks/use-product-attributes'; } from '../../hooks/use-product-attributes';
import { getAttributeId } from '../../components/attribute-control/utils'; import { getAttributeId } from '../../components/attribute-control/utils';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
function hasAttributesUsedForVariations( function hasAttributesUsedForVariations(
productAttributes: Product[ 'attributes' ] productAttributes: Product[ 'attributes' ]
@ -56,6 +57,7 @@ export function Edit( {
}: BlockEditProps< VariationsBlockAttributes > ) { }: BlockEditProps< VariationsBlockAttributes > ) {
const { description } = attributes; const { description } = attributes;
const { generateProductVariations } = useProductVariationsHelper();
const [ isNewModalVisible, setIsNewModalVisible ] = useState( false ); const [ isNewModalVisible, setIsNewModalVisible ] = useState( false );
const [ productAttributes, setProductAttributes ] = useEntityProp< const [ productAttributes, setProductAttributes ] = useEntityProp<
Product[ 'attributes' ] Product[ 'attributes' ]
@ -74,6 +76,7 @@ export function Edit( {
setDefaultProductAttributes( setDefaultProductAttributes(
getFirstOptionFromEachAttribute( values ) getFirstOptionFromEachAttribute( values )
); );
generateProductVariations( values );
}, },
} }
); );

View File

@ -7,7 +7,7 @@ import { BlockConfiguration } from '@wordpress/blocks';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { initBlock } from '../../utils/init-blocks'; import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json'; import blockConfiguration from './block.json';
import { Edit } from './edit'; import { Edit } from './edit';
import { VariationsBlockAttributes } from './types'; import { VariationsBlockAttributes } from './types';

View File

@ -24,6 +24,13 @@ jest.mock( '@woocommerce/navigation', () => ( {
getQuery: jest.fn().mockReturnValue( {} ), getQuery: jest.fn().mockReturnValue( {} ),
} ) ); } ) );
const blockProps = {
setAttributes: () => {},
className: '',
clientId: '',
isSelected: false,
};
function MockTabs( { onChange = jest.fn() } ) { function MockTabs( { onChange = jest.fn() } ) {
const [ selected, setSelected ] = useState< string | null >( null ); const [ selected, setSelected ] = useState< string | null >( null );
const mockContext = { const mockContext = {
@ -39,15 +46,18 @@ function MockTabs( { onChange = jest.fn() } ) {
} } } }
/> />
<Tab <Tab
attributes={ { id: 'test1', title: 'Test button 1' } } { ...blockProps }
attributes={ { id: 'test1', title: 'Test button 1', order: 1 } }
context={ mockContext } context={ mockContext }
/> />
<Tab <Tab
attributes={ { id: 'test2', title: 'Test button 2' } } { ...blockProps }
attributes={ { id: 'test2', title: 'Test button 2', order: 2 } }
context={ mockContext } context={ mockContext }
/> />
<Tab <Tab
attributes={ { id: 'test3', title: 'Test button 3' } } { ...blockProps }
attributes={ { id: 'test3', title: 'Test button 3', order: 3 } }
context={ mockContext } context={ mockContext }
/> />
</SlotFillProvider> </SlotFillProvider>

View File

@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n';
import { Button, Spinner, Tooltip } from '@wordpress/components'; import { Button, Spinner, Tooltip } from '@wordpress/components';
import { import {
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME, EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
ProductAttribute,
ProductVariation, ProductVariation,
} from '@woocommerce/data'; } from '@woocommerce/data';
import { import {
@ -23,7 +24,7 @@ import { CurrencyContext } from '@woocommerce/currency';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet. // @ts-ignore No types for this exist yet.
// eslint-disable-next-line @woocommerce/dependency-group // eslint-disable-next-line @woocommerce/dependency-group
import { useEntityId } from '@wordpress/core-data'; import { useEntityId, useEntityProp } from '@wordpress/core-data';
/** /**
* Internal dependencies * Internal dependencies
@ -46,6 +47,16 @@ export function VariationsTable() {
const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >( const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >(
{} {}
); );
const [ entityAttributes ] = useEntityProp< ProductAttribute[] >(
'postType',
'product',
'attributes'
);
const variableAttributeTags = entityAttributes
.filter( ( attr ) => attr.variation )
.map( ( attr ) => attr.options )
.flat();
const productId = useEntityId( 'postType', 'product' ); const productId = useEntityId( 'postType', 'product' );
const context = useContext( CurrencyContext ); const context = useContext( CurrencyContext );
const { formatAmount } = context; const { formatAmount } = context;
@ -114,34 +125,45 @@ export function VariationsTable() {
{ variations.map( ( variation ) => ( { variations.map( ( variation ) => (
<ListItem key={ `${ variation.id }` }> <ListItem key={ `${ variation.id }` }>
<div className="woocommerce-product-variations__attributes"> <div className="woocommerce-product-variations__attributes">
{ variation.attributes.map( ( attribute ) => { { variation.attributes
const tag = ( .filter( ( attribute ) =>
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ variableAttributeTags.includes(
/* @ts-ignore Additional props are not required. */ attribute.option
<Tag )
id={ attribute.id } )
className="woocommerce-product-variations__attribute" .map( ( attribute ) => {
key={ attribute.id } const tag = (
label={ truncate( attribute.option, { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
length: PRODUCT_VARIATION_TITLE_LIMIT, /* @ts-ignore Additional props are not required. */
} ) } <Tag
screenReaderLabel={ attribute.option } id={ attribute.id }
/> className="woocommerce-product-variations__attribute"
); key={ attribute.id }
label={ truncate(
attribute.option,
{
length: PRODUCT_VARIATION_TITLE_LIMIT,
}
) }
screenReaderLabel={
attribute.option
}
/>
);
return attribute.option.length <= return attribute.option.length <=
PRODUCT_VARIATION_TITLE_LIMIT ? ( PRODUCT_VARIATION_TITLE_LIMIT ? (
tag tag
) : ( ) : (
<Tooltip <Tooltip
key={ attribute.id } key={ attribute.id }
text={ attribute.option } text={ attribute.option }
position="top center" position="top center"
> >
<span>{ tag }</span> <span>{ tag }</span>
</Tooltip> </Tooltip>
); );
} ) } } ) }
</div> </div>
<div <div
className={ classnames( className={ classnames(

View File

@ -23,7 +23,7 @@ export type EnhancedProductAttribute = ProductAttribute & {
type useProductAttributesProps = { type useProductAttributesProps = {
allAttributes: ProductAttribute[]; allAttributes: ProductAttribute[];
isVariationAttributes?: boolean; isVariationAttributes?: boolean;
onChange: ( attributes: EnhancedProductAttribute[] ) => void; onChange: ( attributes: ProductAttribute[] ) => void;
productId?: number; productId?: number;
}; };
@ -79,11 +79,11 @@ export function useProductAttributes( {
}; };
const getAugmentedAttributes = ( const getAugmentedAttributes = (
atts: ProductAttribute[], atts: EnhancedProductAttribute[],
variation: boolean, variation: boolean,
startPosition: number startPosition: number
) => { ): ProductAttribute[] => {
return atts.map( ( attribute, index ) => ( { return atts.map( ( { isDefault, terms, ...attribute }, index ) => ( {
...attribute, ...attribute,
variation, variation,
position: startPosition + index, position: startPosition + index,

View File

@ -0,0 +1,77 @@
/**
* External dependencies
*/
import { useDispatch } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
import { useCallback, useState } from '@wordpress/element';
import {
Product,
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
} from '@woocommerce/data';
/**
* Internal dependencies
*/
import { EnhancedProductAttribute } from './use-product-attributes';
export function useProductVariationsHelper() {
const [ productId ] = useEntityProp< number >(
'postType',
'product',
'id'
);
const { saveEntityRecord } = useDispatch( 'core' );
const {
generateProductVariations: _generateProductVariations,
invalidateResolutionForStoreSelector,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const [ isGenerating, setIsGenerating ] = useState( false );
const generateProductVariations = useCallback(
async ( attributes: EnhancedProductAttribute[] ) => {
setIsGenerating( true );
const updateProductAttributes = async () => {
const hasVariableAttribute = attributes.some(
( attr ) => attr.variation
);
await saveEntityRecord< Promise< Product > >(
'postType',
'product',
{
id: productId,
type: hasVariableAttribute ? 'variable' : 'simple',
attributes,
}
);
};
return updateProductAttributes()
.then( () => {
return _generateProductVariations< { count: number } >( {
product_id: productId,
} );
} )
.then( ( data ) => {
if ( data.count > 0 ) {
invalidateResolutionForStoreSelector(
'getProductVariations'
);
return invalidateResolutionForStoreSelector(
'getProductVariationsTotalCount'
);
}
} )
.finally( () => {
setIsGenerating( false );
} );
},
[]
);
return {
generateProductVariations,
isGenerating,
};
}

View File

@ -22,7 +22,7 @@ import { isValidEmail } from './validate-email';
export * from './create-ordered-children'; export * from './create-ordered-children';
export * from './sort-fills-by-order'; export * from './sort-fills-by-order';
export * from './init-blocks'; export * from './init-block';
export * from './product-apifetch-middleware'; export * from './product-apifetch-middleware';
export * from './sift'; export * from './sift';

View File

@ -2,38 +2,30 @@
* External dependencies * External dependencies
*/ */
import { import {
Block,
BlockConfiguration, BlockConfiguration,
BlockEditProps,
registerBlockType, registerBlockType,
} from '@wordpress/blocks'; } from '@wordpress/blocks';
import { ComponentType } from 'react';
type BlockRepresentation = { interface BlockRepresentation< T extends Record< string, object > > {
name: string; name?: string;
metadata: BlockConfiguration; metadata: BlockConfiguration< T >;
settings: Partial< Omit< BlockConfiguration, 'edit' > > & { settings: Partial< BlockConfiguration< T > >;
readonly edit?: }
| ComponentType<
BlockEditProps< object > & {
context?: Record< string, unknown >;
}
>
| undefined;
};
};
/** /**
* Function to register an individual block. * Function to register an individual block.
* *
* @param {Object} block The block to be registered. * @param block The block to be registered.
* * @return The block, if it has been successfully registered; otherwise `undefined`.
* @return {WPBlockType|void} The block, if it has been successfully registered;
* otherwise `undefined`.
*/ */
export default function initBlock( block: BlockRepresentation ) { export function initBlock<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record< string, any > = Record< string, any >
>( block: BlockRepresentation< T > ): Block< T > | undefined {
if ( ! block ) { if ( ! block ) {
return; return;
} }
const { metadata, settings, name } = block; const { metadata, settings, name } = block;
return registerBlockType( { name, ...metadata }, settings ); return registerBlockType< T >( { name, ...metadata }, settings );
} }

View File

@ -1,31 +0,0 @@
/**
* External dependencies
*/
import {
Block,
BlockConfiguration,
registerBlockType,
} from '@wordpress/blocks';
interface BlockRepresentation< T extends Record< string, object > > {
name?: string;
metadata: BlockConfiguration< T >;
settings: Partial< BlockConfiguration< T > >;
}
/**
* Function to register an individual block.
*
* @param block The block to be registered.
* @return The block, if it has been successfully registered; otherwise `undefined`.
*/
export function initBlock<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
T extends Record< string, any > = Record< string, any >
>( block: BlockRepresentation< T > ): Block< T > | undefined {
if ( ! block ) {
return;
}
const { metadata, settings, name } = block;
return registerBlockType< T >( { name, ...metadata }, settings );
}

View File

@ -1 +0,0 @@
export * from './options';

View File

@ -1,79 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Product, ProductAttribute } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useFormContext } from '@woocommerce/components';
import { AttributeControl } from '@woocommerce/product-editor/src/components/attribute-control';
import { useProductAttributes } from '@woocommerce/product-editor/src/hooks/use-product-attributes';
/**
* Internal dependencies
*/
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
type OptionsProps = {
value: ProductAttribute[];
onChange: ( value: ProductAttribute[] ) => void;
productId?: number;
};
export const Options: React.FC< OptionsProps > = ( {
value,
onChange,
productId,
} ) => {
const { values } = useFormContext< Product >();
const { generateProductVariations } = useProductVariationsHelper();
const { attributes, handleChange } = useProductAttributes( {
allAttributes: value,
isVariationAttributes: true,
onChange: ( newAttributes ) => {
onChange( newAttributes );
generateProductVariations( {
...values,
attributes: newAttributes,
} );
},
productId,
} );
return (
<AttributeControl
value={ attributes }
onAdd={ () => {
recordEvent( 'product_add_options_modal_add_button_click' );
} }
onChange={ handleChange }
onNewModalCancel={ () => {
recordEvent( 'product_add_options_modal_cancel_button_click' );
} }
onNewModalOpen={ () => {
if ( ! attributes.length ) {
recordEvent( 'product_add_first_option_button_click' );
return;
}
recordEvent( 'product_add_option_button' );
} }
uiStrings={ {
emptyStateSubtitle: __( 'No options yet', 'woocommerce' ),
newAttributeListItemLabel: __( 'Add option', 'woocommerce' ),
newAttributeModalTitle: __( 'Add options', 'woocommerce' ),
globalAttributeHelperMessage: __(
`You can change the option's name in {{link}}Attributes{{/link}}.`,
'woocommerce'
),
} }
onRemove={ () =>
recordEvent(
'product_remove_option_confirmation_confirm_click'
)
}
onRemoveCancel={ () =>
recordEvent( 'product_remove_option_confirmation_cancel_click' )
}
/>
);
};

View File

@ -20,7 +20,6 @@ import { PricingSectionFills } from './pricing-section';
import { InventorySectionFills } from './inventory-section'; import { InventorySectionFills } from './inventory-section';
import { AttributesSectionFills } from './attributes-section'; import { AttributesSectionFills } from './attributes-section';
import { ImagesSectionFills } from './images-section'; import { ImagesSectionFills } from './images-section';
import { OptionsSection } from '../sections/options-section';
import { ProductVariationsSection } from '../sections/product-variations-section'; import { ProductVariationsSection } from '../sections/product-variations-section';
import { import {
TAB_GENERAL_ID, TAB_GENERAL_ID,
@ -113,7 +112,6 @@ const Tabs = () => {
tabProps={ tabPropData.options } tabProps={ tabPropData.options }
> >
<> <>
<OptionsSection />
<ProductVariationsSection /> <ProductVariationsSection />
</> </>
</WooProductTabItem> </WooProductTabItem>

View File

@ -1,70 +0,0 @@
/**
* External dependencies
*/
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
import { useDispatch } from '@wordpress/data';
import { useCallback, useState } from '@wordpress/element';
import {
Product,
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
PRODUCTS_STORE_NAME,
} from '@woocommerce/data';
import { useFormContext } from '@woocommerce/components';
export function useProductVariationsHelper() {
const {
generateProductVariations: _generateProductVariations,
invalidateResolutionForStoreSelector,
} = useDispatch( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
const { createProduct, updateProduct } = useDispatch( PRODUCTS_STORE_NAME );
const { resetForm } = useFormContext< Product >();
const [ isGenerating, setIsGenerating ] = useState( false );
const generateProductVariations = useCallback(
async ( product: Partial< Product > ) => {
setIsGenerating( true );
const createOrUpdateProduct = product.id
? () =>
updateProduct< Promise< Product > >(
product.id,
product
)
: () => {
return createProduct< Promise< Product > >( {
...product,
status: 'auto-draft',
name: product.name || AUTO_DRAFT_NAME,
} );
};
return createOrUpdateProduct()
.then( ( createdOrUpdatedProduct ) => {
if ( ! product.id ) {
resetForm( {
...createdOrUpdatedProduct,
name: product.name || '',
} );
}
return _generateProductVariations( {
product_id: createdOrUpdatedProduct.id,
} );
} )
.then( () => {
return invalidateResolutionForStoreSelector(
'getProductVariations'
);
} )
.finally( () => {
setIsGenerating( false );
} );
},
[]
);
return {
generateProductVariations,
isGenerating,
};
}

View File

@ -1,6 +0,0 @@
.woocommerce-product-options-section.woocommerce-form-section {
.woocommerce-form-section__content {
padding: 0;
border: 0;
}
}

View File

@ -1,55 +0,0 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Link, useFormContext } from '@woocommerce/components';
import { Product } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
/**
* Internal dependencies
*/
import './options-section.scss';
import { ProductSectionLayout } from '../layout/product-section-layout';
import { Options } from '../fields/options';
export const OptionsSection: React.FC = () => {
const {
getInputProps,
values: { id: productId },
} = useFormContext< Product >();
return (
<ProductSectionLayout
title={ __( 'Options', 'woocommerce' ) }
className="woocommerce-product-options-section"
description={
<>
<span>
{ __(
'Add and manage options, such as size and color, for customers to choose on the product page.',
'woocommerce'
) }
</span>
<Link
className="woocommerce-form-section__header-link"
href="https://woocommerce.com/document/managing-product-taxonomies/#product-attributes"
target="_blank"
type="external"
onClick={ () => {
recordEvent( 'learn_more_about_options_help' );
} }
>
{ __( 'Learn more about options', 'woocommerce' ) }
</Link>
</>
}
>
<Options
{ ...getInputProps( 'attributes', {
productId,
} ) }
/>
</ProductSectionLayout>
);
};

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Remove unused variation option components

View File

@ -46,6 +46,7 @@ class Init {
add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 ); add_action( 'admin_enqueue_scripts', array( $this, 'dequeue_conflicting_styles' ), 100 );
add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 ); add_action( 'get_edit_post_link', array( $this, 'update_edit_product_link' ), 10, 2 );
} }
add_filter( 'woocommerce_admin_get_user_data_fields', array( $this, 'add_user_data_fields' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_product_template' ) ); add_filter( 'woocommerce_register_post_type_product', array( $this, 'add_product_template' ) );
@ -778,6 +779,21 @@ class Init {
return $args; return $args;
} }
/**
* Adds fields so that we can store user preferences for the variations block.
*
* @param array $user_data_fields User data fields.
* @return array
*/
public function add_user_data_fields( $user_data_fields ) {
return array_merge(
$user_data_fields,
array(
'variable_product_block_tour_shown',
)
);
}
/** /**
* Sets the current screen to the block editor if a wc-admin page. * Sets the current screen to the block editor if a wc-admin page.
*/ */