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
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||||
import {
|
import {
|
||||||
ProductAttribute,
|
ProductAttribute,
|
||||||
|
@ -12,8 +12,11 @@ import { resolveSelect } from '@wordpress/data';
|
||||||
import {
|
import {
|
||||||
Sortable,
|
Sortable,
|
||||||
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
||||||
|
Link,
|
||||||
} from '@woocommerce/components';
|
} from '@woocommerce/components';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
|
import interpolateComponents from '@automattic/interpolate-components';
|
||||||
|
import { getAdminLink } from '@woocommerce/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -33,23 +36,24 @@ type AttributeFieldProps = {
|
||||||
value: ProductAttribute[];
|
value: ProductAttribute[];
|
||||||
onChange: ( value: ProductAttribute[] ) => void;
|
onChange: ( value: ProductAttribute[] ) => void;
|
||||||
productId?: number;
|
productId?: number;
|
||||||
|
// TODO: should we support an 'any' option to show all attributes?
|
||||||
|
attributeType?: 'regular' | 'for-variations';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & {
|
export type HydratedAttributeType = Omit< ProductAttribute, 'options' > & {
|
||||||
options?: string[];
|
options?: string[];
|
||||||
terms?: ProductAttributeTerm[];
|
terms?: ProductAttributeTerm[];
|
||||||
|
visible?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
productId,
|
productId,
|
||||||
|
attributeType = 'regular',
|
||||||
} ) => {
|
} ) => {
|
||||||
const [ showAddAttributeModal, setShowAddAttributeModal ] =
|
const [ showAddAttributeModal, setShowAddAttributeModal ] =
|
||||||
useState( false );
|
useState( false );
|
||||||
const [ hydrationComplete, setHydrationComplete ] = useState< boolean >(
|
|
||||||
value ? false : true
|
|
||||||
);
|
|
||||||
const [ hydratedAttributes, setHydratedAttributes ] = useState<
|
const [ hydratedAttributes, setHydratedAttributes ] = useState<
|
||||||
HydratedAttributeType[]
|
HydratedAttributeType[]
|
||||||
>( [] );
|
>( [] );
|
||||||
|
@ -57,8 +61,13 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
null | string
|
null | string
|
||||||
>( null );
|
>( null );
|
||||||
|
|
||||||
const CANCEL_BUTTON_EVENT_NAME =
|
const isOnlyForVariations = attributeType === 'for-variations';
|
||||||
'product_add_attributes_modal_cancel_button_click';
|
|
||||||
|
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(
|
const fetchTerms = useCallback(
|
||||||
( attributeId: number ) => {
|
( attributeId: number ) => {
|
||||||
|
@ -82,7 +91,19 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect( () => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,19 +115,26 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
).then( ( allResults ) => {
|
).then( ( allResults ) => {
|
||||||
setHydratedAttributes( [
|
setHydratedAttributes( [
|
||||||
...globalAttributes.map( ( attr, index ) => {
|
...globalAttributes.map( ( attr, index ) => {
|
||||||
|
const fetchedTerms = allResults[ index ];
|
||||||
|
|
||||||
const newAttr = {
|
const newAttr = {
|
||||||
...attr,
|
...attr,
|
||||||
terms: allResults[ index ],
|
// I'm not sure this is quite right for handling unpersisted terms,
|
||||||
options: undefined,
|
// but this gets things kinda working for now
|
||||||
|
terms:
|
||||||
|
fetchedTerms.length > 0 ? fetchedTerms : undefined,
|
||||||
|
options:
|
||||||
|
fetchedTerms.length === 0
|
||||||
|
? attr.options
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return newAttr;
|
return newAttr;
|
||||||
} ),
|
} ),
|
||||||
...customAttributes,
|
...customAttributes,
|
||||||
] );
|
] );
|
||||||
setHydrationComplete( true );
|
|
||||||
} );
|
} );
|
||||||
}, [ productId, value, hydrationComplete ] );
|
}, [ fetchTerms, productId, value ] );
|
||||||
|
|
||||||
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
|
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
|
||||||
`${ attribute.id }-${ attribute.name }`;
|
`${ attribute.id }-${ attribute.name }`;
|
||||||
|
@ -121,6 +149,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
? attr.terms.map( ( term ) => term.name )
|
? attr.terms.map( ( term ) => term.name )
|
||||||
: ( attr.options as string[] ),
|
: ( attr.options as string[] ),
|
||||||
terms: undefined,
|
terms: undefined,
|
||||||
|
visible: attr.visible || false,
|
||||||
};
|
};
|
||||||
} )
|
} )
|
||||||
);
|
);
|
||||||
|
@ -157,24 +186,48 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map( ( newAttr, index ) => {
|
.map( ( newAttr, index ) => {
|
||||||
newAttr.position = ( value || [] ).length + index;
|
return {
|
||||||
return newAttr;
|
...newAttributeProps,
|
||||||
|
...newAttr,
|
||||||
|
position: ( value || [] ).length + index,
|
||||||
|
};
|
||||||
} ),
|
} ),
|
||||||
] );
|
] );
|
||||||
recordEvent( 'product_add_attributes_modal_add_button_click' );
|
recordEvent( 'product_add_attributes_modal_add_button_click' );
|
||||||
setShowAddAttributeModal( false );
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<AttributeEmptyState
|
<AttributeEmptyState
|
||||||
|
addNewLabel={
|
||||||
|
isOnlyForVariations
|
||||||
|
? __( 'Add options', 'woocommerce' )
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onNewClick={ () => {
|
onNewClick={ () => {
|
||||||
recordEvent(
|
recordEvent(
|
||||||
'product_add_first_attribute_button_click'
|
'product_add_first_attribute_button_click'
|
||||||
);
|
);
|
||||||
setShowAddAttributeModal( true );
|
setShowAddAttributeModal( true );
|
||||||
} }
|
} }
|
||||||
|
subtitle={
|
||||||
|
isOnlyForVariations
|
||||||
|
? __( 'No options yet', 'woocommerce' )
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{ showAddAttributeModal && (
|
{ showAddAttributeModal && (
|
||||||
<AddAttributeModal
|
<AddAttributeModal
|
||||||
|
@ -183,7 +236,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
setShowAddAttributeModal( false );
|
setShowAddAttributeModal( false );
|
||||||
} }
|
} }
|
||||||
onAdd={ onAddNewAttributes }
|
onAdd={ onAddNewAttributes }
|
||||||
selectedAttributeIds={ ( value || [] ).map(
|
selectedAttributeIds={ ( filteredAttributes || [] ).map(
|
||||||
( attr ) => attr.id
|
( attr ) => attr.id
|
||||||
) }
|
) }
|
||||||
/>
|
/>
|
||||||
|
@ -193,8 +246,10 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
|
const sortedAttributes = filteredAttributes.sort(
|
||||||
const attributeKeyValues = value.reduce(
|
( a, b ) => a.position - b.position
|
||||||
|
);
|
||||||
|
const attributeKeyValues = filteredAttributes.reduce(
|
||||||
(
|
(
|
||||||
keyValue: Record< number, ProductAttribute >,
|
keyValue: Record< number, ProductAttribute >,
|
||||||
attribute: ProductAttribute
|
attribute: ProductAttribute
|
||||||
|
@ -205,6 +260,20 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
{} as Record< number, ProductAttribute >
|
{} 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 (
|
return (
|
||||||
<div className="woocommerce-attribute-field">
|
<div className="woocommerce-attribute-field">
|
||||||
<Sortable
|
<Sortable
|
||||||
|
@ -217,27 +286,39 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
);
|
);
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
{ sortedAttributes.map( ( attribute ) => (
|
{ sortedAttributes.map( ( attr ) => (
|
||||||
<AttributeListItem
|
<AttributeListItem
|
||||||
attribute={ attribute }
|
attribute={ attr }
|
||||||
key={ fetchAttributeId( attribute ) }
|
key={ fetchAttributeId( attr ) }
|
||||||
onEditClick={ () =>
|
onEditClick={ () =>
|
||||||
setEditingAttributeId(
|
setEditingAttributeId( fetchAttributeId( attr ) )
|
||||||
fetchAttributeId( attribute )
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
onRemoveClick={ () => onRemove( attribute ) }
|
onRemoveClick={ () => onRemove( attr ) }
|
||||||
/>
|
/>
|
||||||
) ) }
|
) ) }
|
||||||
</Sortable>
|
</Sortable>
|
||||||
<AddAttributeListItem
|
<AddAttributeListItem
|
||||||
|
label={
|
||||||
|
isOnlyForVariations
|
||||||
|
? __( 'Add option', 'woocommerce' )
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onAddClick={ () => {
|
onAddClick={ () => {
|
||||||
recordEvent( 'product_add_attribute_button' );
|
recordEvent(
|
||||||
|
isOnlyForVariations
|
||||||
|
? 'product_add_option_button'
|
||||||
|
: 'product_add_attribute_button'
|
||||||
|
);
|
||||||
setShowAddAttributeModal( true );
|
setShowAddAttributeModal( true );
|
||||||
} }
|
} }
|
||||||
/>
|
/>
|
||||||
{ showAddAttributeModal && (
|
{ showAddAttributeModal && (
|
||||||
<AddAttributeModal
|
<AddAttributeModal
|
||||||
|
title={
|
||||||
|
isOnlyForVariations
|
||||||
|
? __( 'Add options', 'woocommerce' )
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onCancel={ () => {
|
onCancel={ () => {
|
||||||
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
recordEvent( CANCEL_BUTTON_EVENT_NAME );
|
||||||
setShowAddAttributeModal( false );
|
setShowAddAttributeModal( false );
|
||||||
|
@ -249,6 +330,29 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
<SelectControlMenuSlot />
|
<SelectControlMenuSlot />
|
||||||
{ editingAttributeId && (
|
{ editingAttributeId && (
|
||||||
<EditAttributeModal
|
<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 ) }
|
onCancel={ () => setEditingAttributeId( null ) }
|
||||||
onEdit={ ( changedAttribute ) => {
|
onEdit={ ( changedAttribute ) => {
|
||||||
const newAttributesSet = [ ...hydratedAttributes ];
|
const newAttributesSet = [ ...hydratedAttributes ];
|
||||||
|
@ -266,12 +370,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
||||||
updateAttributes( newAttributesSet );
|
updateAttributes( newAttributesSet );
|
||||||
setEditingAttributeId( null );
|
setEditingAttributeId( null );
|
||||||
} }
|
} }
|
||||||
attribute={
|
attribute={ attribute }
|
||||||
hydratedAttributes.find(
|
|
||||||
( attr ) =>
|
|
||||||
fetchAttributeId( attr ) === editingAttributeId
|
|
||||||
) as HydratedAttributeType
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
) }
|
) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,12 +9,7 @@ import {
|
||||||
TextControl,
|
TextControl,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState } from '@wordpress/element';
|
||||||
import {
|
import { __experimentalTooltip as Tooltip } from '@woocommerce/components';
|
||||||
__experimentalTooltip as Tooltip,
|
|
||||||
Link,
|
|
||||||
} from '@woocommerce/components';
|
|
||||||
import interpolateComponents from '@automattic/interpolate-components';
|
|
||||||
import { getAdminLink } from '@woocommerce/settings';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -30,14 +25,12 @@ import './edit-attribute-modal.scss';
|
||||||
type EditAttributeModalProps = {
|
type EditAttributeModalProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
nameLabel?: string;
|
nameLabel?: string;
|
||||||
globalAttributeHelperMessage?: string;
|
globalAttributeHelperMessage?: JSX.Element;
|
||||||
customAttributeHelperMessage?: string;
|
customAttributeHelperMessage?: string;
|
||||||
termsLabel?: string;
|
termsLabel?: string;
|
||||||
termsPlaceholder?: string;
|
termsPlaceholder?: string;
|
||||||
visibleLabel?: string;
|
visibleLabel?: string;
|
||||||
visibleTooltip?: string;
|
visibleTooltip?: string;
|
||||||
filtersLabel?: string;
|
|
||||||
filtersTooltip?: string;
|
|
||||||
cancelAccessibleLabel?: string;
|
cancelAccessibleLabel?: string;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
updateAccessibleLabel?: string;
|
updateAccessibleLabel?: string;
|
||||||
|
@ -50,25 +43,7 @@ type EditAttributeModalProps = {
|
||||||
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||||
title = __( 'Edit attribute', 'woocommerce' ),
|
title = __( 'Edit attribute', 'woocommerce' ),
|
||||||
nameLabel = __( 'Name', 'woocommerce' ),
|
nameLabel = __( 'Name', 'woocommerce' ),
|
||||||
globalAttributeHelperMessage = interpolateComponents( {
|
globalAttributeHelperMessage,
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
} ),
|
|
||||||
customAttributeHelperMessage = __(
|
customAttributeHelperMessage = __(
|
||||||
'Your customers will see this on the product page',
|
'Your customers will see this on the product page',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
|
@ -80,11 +55,6 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||||
'Show or hide this attribute on the product page',
|
'Show or hide this attribute on the product page',
|
||||||
'woocommerce'
|
'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' ),
|
cancelAccessibleLabel = __( 'Cancel', 'woocommerce' ),
|
||||||
cancelLabel = __( 'Cancel', 'woocommerce' ),
|
cancelLabel = __( 'Cancel', 'woocommerce' ),
|
||||||
updateAccessibleLabel = __( 'Edit attribute', 'woocommerce' ),
|
updateAccessibleLabel = __( 'Edit attribute', 'woocommerce' ),
|
||||||
|
@ -165,19 +135,6 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||||
/>
|
/>
|
||||||
<Tooltip text={ visibleTooltip } />
|
<Tooltip text={ visibleTooltip } />
|
||||||
</div>
|
</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>
|
||||||
<div className="woocommerce-add-attribute-modal__buttons">
|
<div className="woocommerce-add-attribute-modal__buttons">
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -26,7 +26,7 @@ const attributeList: ProductAttribute[] = [
|
||||||
visible: true,
|
visible: true,
|
||||||
variation: true,
|
variation: true,
|
||||||
options: [
|
options: [
|
||||||
'Beige',
|
'beige',
|
||||||
'black',
|
'black',
|
||||||
'Blue',
|
'Blue',
|
||||||
'brown',
|
'brown',
|
||||||
|
@ -134,23 +134,24 @@ describe( 'AttributeField', () => {
|
||||||
await screen.findByText( attributeList[ 0 ].name )
|
await screen.findByText( attributeList[ 0 ].name )
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText( attributeList[ 1 ].name )
|
await screen.queryByText( attributeList[ 1 ].name )
|
||||||
).toBeInTheDocument();
|
).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( () => {
|
act( () => {
|
||||||
render(
|
render(
|
||||||
<AttributeField
|
<AttributeField
|
||||||
value={ [ ...attributeList ] }
|
value={ [ ...attributeList ] }
|
||||||
onChange={ () => {} }
|
onChange={ () => {} }
|
||||||
|
attributeType="for-variations"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText( attributeList[ 0 ].options[ 0 ] )
|
await screen.queryByText( attributeList[ 0 ].options[ 0 ] )
|
||||||
).toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
|
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
|
||||||
).toBeInTheDocument();
|
).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 { ImagesSection } from './sections/images-section';
|
||||||
import { validate } from './product-validation';
|
import { validate } from './product-validation';
|
||||||
import { AttributesSection } from './sections/attributes-section';
|
import { AttributesSection } from './sections/attributes-section';
|
||||||
|
import { OptionsSection } from './sections/options-section';
|
||||||
import { ProductFormFooter } from './layout/product-form-footer';
|
import { ProductFormFooter } from './layout/product-form-footer';
|
||||||
import { ProductFormTab } from './product-form-tab';
|
import { ProductFormTab } from './product-form-tab';
|
||||||
|
|
||||||
|
@ -69,6 +70,7 @@ export const ProductForm: React.FC< {
|
||||||
<ProductShippingSection product={ product } />
|
<ProductShippingSection product={ product } />
|
||||||
</ProductFormTab>
|
</ProductFormTab>
|
||||||
<ProductFormTab name="options" title="Options">
|
<ProductFormTab name="options" title="Options">
|
||||||
|
<OptionsSection />
|
||||||
<ProductVariationsSection />
|
<ProductVariationsSection />
|
||||||
</ProductFormTab>
|
</ProductFormTab>
|
||||||
</ProductFormLayout>
|
</ProductFormLayout>
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
||||||
*/
|
*/
|
||||||
import './attributes-section.scss';
|
import './attributes-section.scss';
|
||||||
import { ProductSectionLayout } from '../layout/product-section-layout';
|
import { ProductSectionLayout } from '../layout/product-section-layout';
|
||||||
import { AttributeField } from '../fields/attribute-field';
|
import { Attributes } from '../fields/attributes';
|
||||||
|
|
||||||
export const AttributesSection: React.FC = () => {
|
export const AttributesSection: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
|
@ -45,8 +45,10 @@ export const AttributesSection: React.FC = () => {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AttributeField
|
<Attributes
|
||||||
{ ...getInputProps( 'attributes', { productId } ) }
|
{ ...getInputProps( 'attributes', {
|
||||||
|
productId,
|
||||||
|
} ) }
|
||||||
/>
|
/>
|
||||||
</ProductSectionLayout>
|
</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