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;
variable_product_tour_shown?: string;
variable_product_block_tour_shown?: 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
*/
import { initBlock } from '../../utils/init-blocks';
import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json';
import { Edit } from './edit';
import { CatalogVisibilityBlockAttributes } from './types';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,19 +2,29 @@
* External dependencies
*/
import { useBlockProps } from '@wordpress/block-editor';
import { BlockEditProps } from '@wordpress/blocks';
import { createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
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();
return (
<div { ...blockProps }>
<VariationsTable />
{ context?.isInSelectedTab && <VariableProductTour /> }
</div>
);
}

View File

@ -1,2 +1,13 @@
.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
*/
import { initBlock } from '../../utils/init-blocks';
import { initBlock } from '../../utils/init-block';
import blockConfiguration from './block.json';
import { Edit } from './edit';
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,
} from '../../hooks/use-product-attributes';
import { AttributeControl } from '../../components/attribute-control';
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
return values.reduce< Product[ 'default_attributes' ] >(
@ -45,6 +46,7 @@ function manageDefaultAttributes( values: EnhancedProductAttribute[] ) {
export function Edit() {
const blockProps = useBlockProps();
const { generateProductVariations } = useProductVariationsHelper();
const [ entityAttributes, setEntityAttributes ] = useEntityProp<
ProductAttribute[]
@ -64,6 +66,7 @@ export function Edit() {
onChange( values ) {
setEntityAttributes( values );
setEntityDefaultAttributes( manageDefaultAttributes( values ) );
generateProductVariations( values );
},
} );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export type EnhancedProductAttribute = ProductAttribute & {
type useProductAttributesProps = {
allAttributes: ProductAttribute[];
isVariationAttributes?: boolean;
onChange: ( attributes: EnhancedProductAttribute[] ) => void;
onChange: ( attributes: ProductAttribute[] ) => void;
productId?: number;
};
@ -79,11 +79,11 @@ export function useProductAttributes( {
};
const getAugmentedAttributes = (
atts: ProductAttribute[],
atts: EnhancedProductAttribute[],
variation: boolean,
startPosition: number
) => {
return atts.map( ( attribute, index ) => ( {
): ProductAttribute[] => {
return atts.map( ( { isDefault, terms, ...attribute }, index ) => ( {
...attribute,
variation,
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 './sort-fills-by-order';
export * from './init-blocks';
export * from './init-block';
export * from './product-apifetch-middleware';
export * from './sift';

View File

@ -2,38 +2,30 @@
* External dependencies
*/
import {
Block,
BlockConfiguration,
BlockEditProps,
registerBlockType,
} from '@wordpress/blocks';
import { ComponentType } from 'react';
type BlockRepresentation = {
name: string;
metadata: BlockConfiguration;
settings: Partial< Omit< BlockConfiguration, 'edit' > > & {
readonly edit?:
| ComponentType<
BlockEditProps< object > & {
context?: Record< string, unknown >;
}
>
| undefined;
};
};
interface BlockRepresentation< T extends Record< string, object > > {
name?: string;
metadata: BlockConfiguration< T >;
settings: Partial< BlockConfiguration< T > >;
}
/**
* Function to register an individual block.
*
* @param {Object} block The block to be registered.
*
* @return {WPBlockType|void} The block, if it has been successfully registered;
* otherwise `undefined`.
* @param block The block to be registered.
* @return 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 ) {
return;
}
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 { AttributesSectionFills } from './attributes-section';
import { ImagesSectionFills } from './images-section';
import { OptionsSection } from '../sections/options-section';
import { ProductVariationsSection } from '../sections/product-variations-section';
import {
TAB_GENERAL_ID,
@ -113,7 +112,6 @@ const Tabs = () => {
tabProps={ tabPropData.options }
>
<>
<OptionsSection />
<ProductVariationsSection />
</>
</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( '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_filter( 'woocommerce_register_post_type_product', array( $this, 'add_product_template' ) );
@ -778,6 +779,21 @@ class Init {
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.
*/