Add Options section to new product experience (#35910)
* Support passing in filter and new attribute properties to AttributeField * Changelog * Pass addButtonLabel as prop * Add OptionsSection to options tab * Refactor more to create Attributes and Options fields * Refactor a couple of things * Refactor globalAttributeHelperMessage * Remove `Used for filters` checkbox * Remove `hydrationComplete` * Add subtitle to empty state component * Fix 'Add option' button * Fix tests Co-authored-by: Fernando Marichal <contacto@fernandomarichal.com>
This commit is contained in:
parent
5b3b5dab59
commit
a9b46d51b5
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
ProductAttribute,
|
||||
|
@ -12,8 +12,11 @@ import { resolveSelect } from '@wordpress/data';
|
|||
import {
|
||||
Sortable,
|
||||
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -33,23 +36,24 @@ type AttributeFieldProps = {
|
|||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
// TODO: should we support an 'any' option to show all attributes?
|
||||
attributeType?: 'regular' | 'for-variations';
|
||||
};
|
||||
|
||||
export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & {
|
||||
options?: string[];
|
||||
terms?: ProductAttributeTerm[];
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
attributeType = 'regular',
|
||||
} ) => {
|
||||
const [ showAddAttributeModal, setShowAddAttributeModal ] =
|
||||
useState( false );
|
||||
const [ hydrationComplete, setHydrationComplete ] = useState< boolean >(
|
||||
value ? false : true
|
||||
);
|
||||
const [ hydratedAttributes, setHydratedAttributes ] = useState<
|
||||
HydratedAttributeType[]
|
||||
>( [] );
|
||||
|
@ -57,8 +61,13 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
null | string
|
||||
>( null );
|
||||
|
||||
const CANCEL_BUTTON_EVENT_NAME =
|
||||
'product_add_attributes_modal_cancel_button_click';
|
||||
const isOnlyForVariations = attributeType === 'for-variations';
|
||||
|
||||
const newAttributeProps = { variation: isOnlyForVariations };
|
||||
|
||||
const CANCEL_BUTTON_EVENT_NAME = isOnlyForVariations
|
||||
? 'product_add_options_modal_cancel_button_click'
|
||||
: 'product_add_attributes_modal_cancel_button_click';
|
||||
|
||||
const fetchTerms = useCallback(
|
||||
( attributeId: number ) => {
|
||||
|
@ -82,7 +91,19 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! value || hydrationComplete ) {
|
||||
// Temporarily always doing hydration, since otherwise new attributes
|
||||
// get removed from Options and Attributes when the other list is then
|
||||
// modified
|
||||
//
|
||||
// This is because the hydration is out of date -- the logic currently
|
||||
// assumes modifications are only made from within the component
|
||||
//
|
||||
// I think we'll need to move the hydration out of the individual component
|
||||
// instance. To where, I do not yet know... maybe in the form context
|
||||
// somewhere so that a single hydration source can be shared between multiple
|
||||
// instances? Something like a simple key-value store in the form context
|
||||
// would be handy.
|
||||
if ( ! value ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -94,19 +115,26 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
).then( ( allResults ) => {
|
||||
setHydratedAttributes( [
|
||||
...globalAttributes.map( ( attr, index ) => {
|
||||
const fetchedTerms = allResults[ index ];
|
||||
|
||||
const newAttr = {
|
||||
...attr,
|
||||
terms: allResults[ index ],
|
||||
options: undefined,
|
||||
// I'm not sure this is quite right for handling unpersisted terms,
|
||||
// but this gets things kinda working for now
|
||||
terms:
|
||||
fetchedTerms.length > 0 ? fetchedTerms : undefined,
|
||||
options:
|
||||
fetchedTerms.length === 0
|
||||
? attr.options
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return newAttr;
|
||||
} ),
|
||||
...customAttributes,
|
||||
] );
|
||||
setHydrationComplete( true );
|
||||
} );
|
||||
}, [ productId, value, hydrationComplete ] );
|
||||
}, [ fetchTerms, productId, value ] );
|
||||
|
||||
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
|
||||
`${ attribute.id }-${ attribute.name }`;
|
||||
|
@ -121,6 +149,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
? attr.terms.map( ( term ) => term.name )
|
||||
: ( attr.options as string[] ),
|
||||
terms: undefined,
|
||||
visible: attr.visible || false,
|
||||
};
|
||||
} )
|
||||
);
|
||||
|
@ -157,24 +186,48 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
)
|
||||
)
|
||||
.map( ( newAttr, index ) => {
|
||||
newAttr.position = ( value || [] ).length + index;
|
||||
return newAttr;
|
||||
return {
|
||||
...newAttributeProps,
|
||||
...newAttr,
|
||||
position: ( value || [] ).length + index,
|
||||
};
|
||||
} ),
|
||||
] );
|
||||
recordEvent( 'product_add_attributes_modal_add_button_click' );
|
||||
setShowAddAttributeModal( false );
|
||||
};
|
||||
|
||||
if ( ! value || value.length === 0 || hydratedAttributes.length === 0 ) {
|
||||
const filteredAttributes = value
|
||||
? value.filter(
|
||||
( attribute: ProductAttribute ) =>
|
||||
attribute.variation === isOnlyForVariations
|
||||
)
|
||||
: false;
|
||||
|
||||
if (
|
||||
! filteredAttributes ||
|
||||
filteredAttributes.length === 0 ||
|
||||
hydratedAttributes.length === 0
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<AttributeEmptyState
|
||||
addNewLabel={
|
||||
isOnlyForVariations
|
||||
? __( 'Add options', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onNewClick={ () => {
|
||||
recordEvent(
|
||||
'product_add_first_attribute_button_click'
|
||||
);
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
subtitle={
|
||||
isOnlyForVariations
|
||||
? __( 'No options yet', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{ showAddAttributeModal && (
|
||||
<AddAttributeModal
|
||||
|
@ -183,7 +236,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
setShowAddAttributeModal( false );
|
||||
} }
|
||||
onAdd={ onAddNewAttributes }
|
||||
selectedAttributeIds={ ( value || [] ).map(
|
||||
selectedAttributeIds={ ( filteredAttributes || [] ).map(
|
||||
( attr ) => attr.id
|
||||
) }
|
||||
/>
|
||||
|
@ -193,8 +246,10 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
}
|
||||
|
||||
const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
|
||||
const attributeKeyValues = value.reduce(
|
||||
const sortedAttributes = filteredAttributes.sort(
|
||||
( a, b ) => a.position - b.position
|
||||
);
|
||||
const attributeKeyValues = filteredAttributes.reduce(
|
||||
(
|
||||
keyValue: Record< number, ProductAttribute >,
|
||||
attribute: ProductAttribute
|
||||
|
@ -205,6 +260,20 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
{} as Record< number, ProductAttribute >
|
||||
);
|
||||
|
||||
const attribute = hydratedAttributes.find(
|
||||
( attr ) => fetchAttributeId( attr ) === editingAttributeId
|
||||
) as HydratedAttributeType;
|
||||
|
||||
const editAttributeCopy = isOnlyForVariations
|
||||
? __(
|
||||
`You can change the option's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
)
|
||||
: __(
|
||||
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="woocommerce-attribute-field">
|
||||
<Sortable
|
||||
|
@ -217,27 +286,39 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
} }
|
||||
>
|
||||
{ sortedAttributes.map( ( attribute ) => (
|
||||
{ sortedAttributes.map( ( attr ) => (
|
||||
<AttributeListItem
|
||||
attribute={ attribute }
|
||||
key={ fetchAttributeId( attribute ) }
|
||||
attribute={ attr }
|
||||
key={ fetchAttributeId( attr ) }
|
||||
onEditClick={ () =>
|
||||
setEditingAttributeId(
|
||||
fetchAttributeId( attribute )
|
||||
)
|
||||
setEditingAttributeId( fetchAttributeId( attr ) )
|
||||
}
|
||||
onRemoveClick={ () => onRemove( attribute ) }
|
||||
onRemoveClick={ () => onRemove( attr ) }
|
||||
/>
|
||||
) ) }
|
||||
</Sortable>
|
||||
<AddAttributeListItem
|
||||
label={
|
||||
isOnlyForVariations
|
||||
? __( 'Add option', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onAddClick={ () => {
|
||||
recordEvent( 'product_add_attribute_button' );
|
||||
recordEvent(
|
||||
isOnlyForVariations
|
||||
? 'product_add_option_button'
|
||||
: 'product_add_attribute_button'
|
||||
);
|
||||
setShowAddAttributeModal( true );
|
||||
} }
|
||||
/>
|
||||
{ showAddAttributeModal && (
|
||||
<AddAttributeModal
|
||||
title={
|
||||
isOnlyForVariations
|
||||
? __( 'Add options', 'woocommerce' )
|
||||
: undefined
|
||||
}
|
||||
onCancel={ () => {
|
||||
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
||||
setShowAddAttributeModal( false );
|
||||
|
@ -249,6 +330,29 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
<SelectControlMenuSlot />
|
||||
{ editingAttributeId && (
|
||||
<EditAttributeModal
|
||||
title={
|
||||
/* translators: %s is the attribute name */
|
||||
sprintf(
|
||||
__( 'Edit %s', 'woocommerce' ),
|
||||
attribute.name
|
||||
)
|
||||
}
|
||||
globalAttributeHelperMessage={ interpolateComponents( {
|
||||
mixedString: editAttributeCopy,
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ getAdminLink(
|
||||
'edit.php?post_type=product&page=product_attributes'
|
||||
) }
|
||||
target="_blank"
|
||||
type="wp-admin"
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
onCancel={ () => setEditingAttributeId( null ) }
|
||||
onEdit={ ( changedAttribute ) => {
|
||||
const newAttributesSet = [ ...hydratedAttributes ];
|
||||
|
@ -266,12 +370,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
updateAttributes( newAttributesSet );
|
||||
setEditingAttributeId( null );
|
||||
} }
|
||||
attribute={
|
||||
hydratedAttributes.find(
|
||||
( attr ) =>
|
||||
fetchAttributeId( attr ) === editingAttributeId
|
||||
) as HydratedAttributeType
|
||||
}
|
||||
attribute={ attribute }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
|
|
|
@ -9,12 +9,7 @@ import {
|
|||
TextControl,
|
||||
} from '@wordpress/components';
|
||||
import { useState } from '@wordpress/element';
|
||||
import {
|
||||
__experimentalTooltip as Tooltip,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { __experimentalTooltip as Tooltip } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -30,14 +25,12 @@ import './edit-attribute-modal.scss';
|
|||
type EditAttributeModalProps = {
|
||||
title?: string;
|
||||
nameLabel?: string;
|
||||
globalAttributeHelperMessage?: string;
|
||||
globalAttributeHelperMessage?: JSX.Element;
|
||||
customAttributeHelperMessage?: string;
|
||||
termsLabel?: string;
|
||||
termsPlaceholder?: string;
|
||||
visibleLabel?: string;
|
||||
visibleTooltip?: string;
|
||||
filtersLabel?: string;
|
||||
filtersTooltip?: string;
|
||||
cancelAccessibleLabel?: string;
|
||||
cancelLabel?: string;
|
||||
updateAccessibleLabel?: string;
|
||||
|
@ -50,25 +43,7 @@ type EditAttributeModalProps = {
|
|||
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||
title = __( 'Edit attribute', 'woocommerce' ),
|
||||
nameLabel = __( 'Name', 'woocommerce' ),
|
||||
globalAttributeHelperMessage = interpolateComponents( {
|
||||
mixedString: __(
|
||||
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ getAdminLink(
|
||||
'edit.php?post_type=product&page=product_attributes'
|
||||
) }
|
||||
target="_blank"
|
||||
type="wp-admin"
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ),
|
||||
globalAttributeHelperMessage,
|
||||
customAttributeHelperMessage = __(
|
||||
'Your customers will see this on the product page',
|
||||
'woocommerce'
|
||||
|
@ -80,11 +55,6 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
'Show or hide this attribute on the product page',
|
||||
'woocommerce'
|
||||
),
|
||||
filtersLabel = __( 'Used for filters', 'woocommerce' ),
|
||||
filtersTooltip = __(
|
||||
`Show or hide this attribute in the filters section on your store's category and shop pages`,
|
||||
'woocommerce'
|
||||
),
|
||||
cancelAccessibleLabel = __( 'Cancel', 'woocommerce' ),
|
||||
cancelLabel = __( 'Cancel', 'woocommerce' ),
|
||||
updateAccessibleLabel = __( 'Edit attribute', 'woocommerce' ),
|
||||
|
@ -165,19 +135,6 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
/>
|
||||
<Tooltip text={ visibleTooltip } />
|
||||
</div>
|
||||
<div className="woocommerce-edit-attribute-modal__option-container">
|
||||
<CheckboxControl
|
||||
onChange={ ( val ) =>
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
variation: val,
|
||||
} )
|
||||
}
|
||||
checked={ editableAttribute?.variation }
|
||||
label={ filtersLabel }
|
||||
/>
|
||||
<Tooltip text={ filtersTooltip } />
|
||||
</div>
|
||||
</div>
|
||||
<div className="woocommerce-add-attribute-modal__buttons">
|
||||
<Button
|
||||
|
|
|
@ -26,7 +26,7 @@ const attributeList: ProductAttribute[] = [
|
|||
visible: true,
|
||||
variation: true,
|
||||
options: [
|
||||
'Beige',
|
||||
'beige',
|
||||
'black',
|
||||
'Blue',
|
||||
'brown',
|
||||
|
@ -134,23 +134,24 @@ describe( 'AttributeField', () => {
|
|||
await screen.findByText( attributeList[ 0 ].name )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 1 ].name )
|
||||
).toBeInTheDocument();
|
||||
await screen.queryByText( attributeList[ 1 ].name )
|
||||
).not.toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render the first two terms of each attribute, and show "+ n more" for the rest', async () => {
|
||||
it( 'should render the first two terms of each option, and show "+ n more" for the rest', async () => {
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ () => {} }
|
||||
attributeType="for-variations"
|
||||
/>
|
||||
);
|
||||
} );
|
||||
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 0 ].options[ 0 ] )
|
||||
).toBeInTheDocument();
|
||||
await screen.queryByText( attributeList[ 0 ].options[ 0 ] )
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
|
||||
).toBeInTheDocument();
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
|
||||
type AttributesProps = {
|
||||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export const Attributes: React.FC< AttributesProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
} ) => {
|
||||
return (
|
||||
<AttributeField
|
||||
attributeType="regular"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
productId={ productId }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './attributes';
|
|
@ -0,0 +1 @@
|
|||
export * from './options';
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
|
||||
type OptionsProps = {
|
||||
value: ProductAttribute[];
|
||||
onChange: ( value: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export const Options: React.FC< OptionsProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
} ) => {
|
||||
return (
|
||||
<AttributeField
|
||||
attributeType="for-variations"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
productId={ productId }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -18,6 +18,7 @@ import { ProductVariationsSection } from './sections/product-variations-section'
|
|||
import { ImagesSection } from './sections/images-section';
|
||||
import { validate } from './product-validation';
|
||||
import { AttributesSection } from './sections/attributes-section';
|
||||
import { OptionsSection } from './sections/options-section';
|
||||
import { ProductFormFooter } from './layout/product-form-footer';
|
||||
import { ProductFormTab } from './product-form-tab';
|
||||
|
||||
|
@ -69,6 +70,7 @@ export const ProductForm: React.FC< {
|
|||
<ProductShippingSection product={ product } />
|
||||
</ProductFormTab>
|
||||
<ProductFormTab name="options" title="Options">
|
||||
<OptionsSection />
|
||||
<ProductVariationsSection />
|
||||
</ProductFormTab>
|
||||
</ProductFormLayout>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
*/
|
||||
import './attributes-section.scss';
|
||||
import { ProductSectionLayout } from '../layout/product-section-layout';
|
||||
import { AttributeField } from '../fields/attribute-field';
|
||||
import { Attributes } from '../fields/attributes';
|
||||
|
||||
export const AttributesSection: React.FC = () => {
|
||||
const {
|
||||
|
@ -45,8 +45,10 @@ export const AttributesSection: React.FC = () => {
|
|||
</>
|
||||
}
|
||||
>
|
||||
<AttributeField
|
||||
{ ...getInputProps( 'attributes', { productId } ) }
|
||||
<Attributes
|
||||
{ ...getInputProps( 'attributes', {
|
||||
productId,
|
||||
} ) }
|
||||
/>
|
||||
</ProductSectionLayout>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.woocommerce-product-options-section.woocommerce-form-section {
|
||||
.woocommerce-form-section__content {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* 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: add
|
||||
|
||||
Add Options section to new product experience form.
|
Loading…
Reference in New Issue