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;
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
* 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';
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 } );
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -22,5 +22,6 @@
|
||||||
"lock": false,
|
"lock": false,
|
||||||
"__experimentalToolbar": false
|
"__experimentalToolbar": false
|
||||||
},
|
},
|
||||||
|
"usesContext": [ "isInSelectedTab" ],
|
||||||
"editorStyle": "file:./editor.css"
|
"editorStyle": "file:./editor.css"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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,
|
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 );
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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 );
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 './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';
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { 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>
|
||||||
|
|
|
@ -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( '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.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue