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:
parent
05dfaacd37
commit
71e8b699db
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Add new user preference to UserPreferences type.
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add isInSelectedTab context to tab blocks for use in nested blocks.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Update variation options block to auto create variations upon options update.
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -27,6 +27,9 @@
|
|||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
},
|
||||
"providesContext": {
|
||||
"isInSelectedTab": "isSelected"
|
||||
},
|
||||
"usesContext": [ "selectedTab" ],
|
||||
"editorStyle": "file:./editor.css",
|
||||
"templateLock": "contentOnly"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 } );
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -22,5 +22,6 @@
|
|||
"lock": false,
|
||||
"__experimentalToolbar": false
|
||||
},
|
||||
"usesContext": [ "isInSelectedTab" ],
|
||||
"editorStyle": "file:./editor.css"
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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, you’ll 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 } />;
|
||||
};
|
|
@ -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 );
|
||||
},
|
||||
} );
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 );
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './options';
|
|
@ -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' )
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
.woocommerce-product-options-section.woocommerce-form-section {
|
||||
.woocommerce-form-section__content {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Remove unused variation option components
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue