Merge branch 'trunk' into feature/34906-marketing-channels-card

This commit is contained in:
Gan Eng Chin 2023-02-04 02:33:01 +08:00
commit b717ce9645
No known key found for this signature in database
GPG Key ID: 94D5D972860ADB01
22 changed files with 3012 additions and 2683 deletions

View File

@ -9,7 +9,6 @@ import {
__experimentalSelectControlMenuSlot as SelectControlMenuSlot, __experimentalSelectControlMenuSlot as SelectControlMenuSlot,
Link, Link,
} from '@woocommerce/components'; } from '@woocommerce/components';
import { recordEvent } from '@woocommerce/tracks';
import interpolateComponents from '@automattic/interpolate-components'; import interpolateComponents from '@automattic/interpolate-components';
import { getAdminLink } from '@woocommerce/settings'; import { getAdminLink } from '@woocommerce/settings';
@ -17,48 +16,69 @@ import { getAdminLink } from '@woocommerce/settings';
* Internal dependencies * Internal dependencies
*/ */
import './attribute-field.scss'; import './attribute-field.scss';
import { AddAttributeModal } from './add-attribute-modal';
import { EditAttributeModal } from './edit-attribute-modal'; import { EditAttributeModal } from './edit-attribute-modal';
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes'; import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { import {
getAttributeId,
getAttributeKey, getAttributeKey,
reorderSortableProductAttributePositions, reorderSortableProductAttributePositions,
} from './utils'; } from './utils';
import { AttributeEmptyState } from '../attribute-empty-state'; import { AttributeEmptyState } from '../attribute-empty-state';
import { import {
AddAttributeListItem,
AttributeListItem, AttributeListItem,
NewAttributeListItem,
} from '../attribute-list-item'; } from '../attribute-list-item';
import { NewAttributeModal } from './new-attribute-modal';
type AttributeControlProps = { type AttributeControlProps = {
value: ProductAttribute[]; value: ProductAttribute[];
onAdd?: ( attribute: EnhancedProductAttribute[] ) => void;
onChange: ( value: ProductAttribute[] ) => void; onChange: ( value: ProductAttribute[] ) => void;
// TODO: should we support an 'any' option to show all attributes? onEdit?: ( attribute: ProductAttribute ) => void;
attributeType?: 'regular' | 'for-variations'; onRemove?: ( attribute: ProductAttribute ) => void;
onRemoveCancel?: ( attribute: ProductAttribute ) => void;
onNewModalCancel?: () => void;
onNewModalClose?: () => void;
onNewModalOpen?: () => void;
onEditModalCancel?: ( attribute?: ProductAttribute ) => void;
onEditModalClose?: ( attribute?: ProductAttribute ) => void;
onEditModalOpen?: ( attribute?: ProductAttribute ) => void;
uiStrings?: {
emptyStateSubtitle?: string;
newAttributeListItemLabel?: string;
newAttributeModalTitle?: string;
globalAttributeHelperMessage: string;
};
}; };
export const AttributeControl: React.FC< AttributeControlProps > = ( { export const AttributeControl: React.FC< AttributeControlProps > = ( {
value, value,
attributeType = 'regular', onAdd = () => {},
onChange, onChange,
onEdit = () => {},
onNewModalCancel = () => {},
onNewModalClose = () => {},
onNewModalOpen = () => {},
onEditModalCancel = () => {},
onEditModalClose = () => {},
onEditModalOpen = () => {},
onRemove = () => {},
onRemoveCancel = () => {},
uiStrings = {
newAttributeModalTitle: undefined,
emptyStateSubtitle: undefined,
newAttributeListItemLabel: undefined,
globalAttributeHelperMessage: __(
`You can change the attribute's name in {{link}}Attributes{{/link}}.`,
'woocommerce'
),
},
} ) => { } ) => {
const [ showAddAttributeModal, setShowAddAttributeModal ] = const [ isNewModalVisible, setIsNewModalVisible ] = useState( false );
useState( false ); const [ currentAttributeId, setCurrentAttributeId ] = useState<
const [ editingAttributeId, setEditingAttributeId ] = useState<
null | string null | string
>( null ); >( null );
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 fetchAttributeId = ( attribute: { id: number; name: string } ) =>
`${ attribute.id }-${ attribute.name }`;
const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => { const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => {
onChange( onChange(
newAttributes.map( ( attr ) => { newAttributes.map( ( attr ) => {
@ -74,79 +94,89 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
); );
}; };
const onRemove = ( attribute: ProductAttribute ) => { const handleRemove = ( attribute: ProductAttribute ) => {
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) { if ( window.confirm( __( 'Remove this attribute?', 'woocommerce' ) ) ) {
recordEvent(
'product_remove_attribute_confirmation_confirm_click'
);
handleChange( handleChange(
value.filter( value.filter(
( attr ) => ( attr ) =>
fetchAttributeId( attr ) !== getAttributeId( attr ) !== getAttributeId( attribute )
fetchAttributeId( attribute )
) )
); );
} else { onRemove( attribute );
recordEvent( 'product_remove_attribute_confirmation_cancel_click' ); return;
} }
onRemoveCancel( attribute );
}; };
const onAddNewAttributes = ( const openNewModal = () => {
newAttributes: EnhancedProductAttribute[] setIsNewModalVisible( true );
) => { onNewModalOpen();
};
const closeNewModal = () => {
setIsNewModalVisible( false );
onNewModalClose();
};
const openEditModal = ( attribute: ProductAttribute ) => {
setCurrentAttributeId( getAttributeId( attribute ) );
onEditModalOpen( attribute );
};
const closeEditModal = ( attribute: ProductAttribute ) => {
setCurrentAttributeId( null );
onEditModalClose( attribute );
};
const handleAdd = ( newAttributes: EnhancedProductAttribute[] ) => {
handleChange( [ handleChange( [
...( value || [] ), ...value,
...newAttributes ...newAttributes.filter(
.filter( ( newAttr ) =>
( newAttr ) => ! value.find(
! ( value || [] ).find( ( attr ) => ( attr ) =>
newAttr.id === 0 getAttributeId( newAttr ) === getAttributeId( attr )
? newAttr.name === attr.name // check name if custom attribute = id === 0. )
: attr.id === newAttr.id ),
)
)
.map( ( newAttr, index ) => {
return {
...newAttributeProps,
...newAttr,
position: ( value || [] ).length + index,
};
} ),
] ); ] );
recordEvent( 'product_add_attributes_modal_add_button_click' ); onAdd( newAttributes );
setShowAddAttributeModal( false ); closeNewModal();
};
const handleEdit = ( updatedAttribute: ProductAttribute ) => {
const updatedAttributes = value.map( ( attr ) => {
if (
getAttributeId( attr ) === getAttributeId( updatedAttribute )
) {
return updatedAttribute;
}
return attr;
} );
onEdit( updatedAttribute );
handleChange( updatedAttributes );
closeEditModal( updatedAttribute );
}; };
if ( ! value.length ) { if ( ! value.length ) {
return ( return (
<> <>
<AttributeEmptyState <AttributeEmptyState
addNewLabel={ addNewLabel={ uiStrings.newAttributeModalTitle }
isOnlyForVariations onNewClick={ () => openNewModal() }
? __( 'Add options', 'woocommerce' ) subtitle={ uiStrings.emptyStateSubtitle }
: undefined
}
onNewClick={ () => {
recordEvent(
'product_add_first_attribute_button_click'
);
setShowAddAttributeModal( true );
} }
subtitle={
isOnlyForVariations
? __( 'No options yet', 'woocommerce' )
: undefined
}
/> />
{ showAddAttributeModal && ( { isNewModalVisible && (
<AddAttributeModal <NewAttributeModal
onCancel={ () => { onCancel={ () => {
recordEvent( CANCEL_BUTTON_EVENT_NAME ); closeNewModal();
setShowAddAttributeModal( false ); onNewModalCancel();
} } } }
onAdd={ onAddNewAttributes } onAdd={ handleAdd }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
title={ uiStrings.newAttributeModalTitle }
/> />
) } ) }
<SelectControlMenuSlot /> <SelectControlMenuSlot />
@ -167,20 +197,10 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
{} as Record< number | string, ProductAttribute > {} as Record< number | string, ProductAttribute >
); );
const editingAttribute = value.find( const currentAttribute = value.find(
( attr ) => fetchAttributeId( attr ) === editingAttributeId ( attr ) => getAttributeId( attr ) === currentAttributeId
) as EnhancedProductAttribute; ) as EnhancedProductAttribute;
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
@ -204,54 +224,37 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
{ sortedAttributes.map( ( attr ) => ( { sortedAttributes.map( ( attr ) => (
<AttributeListItem <AttributeListItem
attribute={ attr } attribute={ attr }
key={ fetchAttributeId( attr ) } key={ getAttributeId( attr ) }
onEditClick={ () => onEditClick={ () => openEditModal( attr ) }
setEditingAttributeId( fetchAttributeId( attr ) ) onRemoveClick={ () => handleRemove( attr ) }
}
onRemoveClick={ () => onRemove( attr ) }
/> />
) ) } ) ) }
</Sortable> </Sortable>
<AddAttributeListItem <NewAttributeListItem
label={ label={ uiStrings.newAttributeListItemLabel }
isOnlyForVariations onClick={ () => openNewModal() }
? __( 'Add option', 'woocommerce' )
: undefined
}
onAddClick={ () => {
recordEvent(
isOnlyForVariations
? 'product_add_option_button'
: 'product_add_attribute_button'
);
setShowAddAttributeModal( true );
} }
/> />
{ showAddAttributeModal && ( { isNewModalVisible && (
<AddAttributeModal <NewAttributeModal
title={ title={ uiStrings.newAttributeModalTitle }
isOnlyForVariations
? __( 'Add options', 'woocommerce' )
: undefined
}
onCancel={ () => { onCancel={ () => {
recordEvent( CANCEL_BUTTON_EVENT_NAME ); closeNewModal();
setShowAddAttributeModal( false ); onNewModalCancel();
} } } }
onAdd={ onAddNewAttributes } onAdd={ handleAdd }
selectedAttributeIds={ value.map( ( attr ) => attr.id ) } selectedAttributeIds={ value.map( ( attr ) => attr.id ) }
/> />
) } ) }
<SelectControlMenuSlot /> <SelectControlMenuSlot />
{ editingAttribute && ( { currentAttribute && (
<EditAttributeModal <EditAttributeModal
title={ sprintf( title={ sprintf(
/* translators: %s is the attribute name */ /* translators: %s is the attribute name */
__( 'Edit %s', 'woocommerce' ), __( 'Edit %s', 'woocommerce' ),
editingAttribute.name currentAttribute.name
) } ) }
globalAttributeHelperMessage={ interpolateComponents( { globalAttributeHelperMessage={ interpolateComponents( {
mixedString: editAttributeCopy, mixedString: uiStrings.globalAttributeHelperMessage,
components: { components: {
link: ( link: (
<Link <Link
@ -266,26 +269,14 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
), ),
}, },
} ) } } ) }
onCancel={ () => setEditingAttributeId( null ) } onCancel={ () => {
onEdit={ ( changedAttribute ) => { closeEditModal( currentAttribute );
const newAttributesSet = [ ...value ]; onEditModalCancel( currentAttribute );
const changedAttributeIndex: number =
newAttributesSet.findIndex( ( attr ) =>
attr.id !== 0
? attr.id === changedAttribute.id
: attr.name === changedAttribute.name
);
newAttributesSet.splice(
changedAttributeIndex,
1,
changedAttribute
);
handleChange( newAttributesSet );
setEditingAttributeId( null );
} } } }
attribute={ editingAttribute } onEdit={ ( updatedAttribute ) => {
handleEdit( updatedAttribute );
} }
attribute={ currentAttribute }
/> />
) } ) }
</div> </div>

View File

@ -1,5 +1,13 @@
.woocommerce-edit-attribute-modal { .woocommerce-edit-attribute-modal {
overflow: visible; overflow: visible;
&__buttons {
margin-top: $gap-larger;
display: flex;
flex-direction: row;
gap: $gap-smaller;
justify-content: flex-end;
}
} }
.woocommerce-edit-attribute-modal__body { .woocommerce-edit-attribute-modal__body {

View File

@ -136,7 +136,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
<Tooltip text={ visibleTooltip } /> <Tooltip text={ visibleTooltip } />
</div> </div>
</div> </div>
<div className="woocommerce-add-attribute-modal__buttons"> <div className="woocommerce-edit-attribute-modal__buttons">
<Button <Button
isSecondary isSecondary
label={ cancelAccessibleLabel } label={ cancelAccessibleLabel }

View File

@ -1,4 +1,4 @@
.woocommerce-add-attribute-modal { .woocommerce-new-attribute-modal {
.components-notice.is-info { .components-notice.is-info {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;

View File

@ -20,7 +20,7 @@ import {
/** /**
* Internal dependencies * Internal dependencies
*/ */
import './add-attribute-modal.scss'; import './new-attribute-modal.scss';
import { AttributeInputField } from '../attribute-input-field'; import { AttributeInputField } from '../attribute-input-field';
import { import {
AttributeTermInputField, AttributeTermInputField,
@ -29,7 +29,7 @@ import {
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes'; import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
import { getProductAttributeObject } from './utils'; import { getProductAttributeObject } from './utils';
type AddAttributeModalProps = { type NewAttributeModalProps = {
title?: string; title?: string;
notice?: string; notice?: string;
attributeLabel?: string; attributeLabel?: string;
@ -54,7 +54,7 @@ type AttributeForm = {
attributes: Array< EnhancedProductAttribute | null >; attributes: Array< EnhancedProductAttribute | null >;
}; };
export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( { export const NewAttributeModal: React.FC< NewAttributeModalProps > = ( {
title = __( 'Add attributes', 'woocommerce' ), title = __( 'Add attributes', 'woocommerce' ),
notice = __( notice = __(
'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.', 'By default, attributes are filterable and visible on the product page. You can change these settings for each attribute separately later.',
@ -83,12 +83,11 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
const scrollAttributeIntoView = ( index: number ) => { const scrollAttributeIntoView = ( index: number ) => {
setTimeout( () => { setTimeout( () => {
const attributeRow = document.querySelector( const attributeRow = document.querySelector(
`.woocommerce-add-attribute-modal__table-row-${ index }` `.woocommerce-new-attribute-modal__table-row-${ index }`
); );
attributeRow?.scrollIntoView( { behavior: 'smooth' } ); attributeRow?.scrollIntoView( { behavior: 'smooth' } );
}, 0 ); }, 0 );
}; };
const [ showConfirmClose, setShowConfirmClose ] = useState( false ); const [ showConfirmClose, setShowConfirmClose ] = useState( false );
const addAnother = ( const addAnother = (
values: AttributeForm, values: AttributeForm,
@ -148,9 +147,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
setTimeout( () => { setTimeout( () => {
const valueInputField: HTMLInputElement | null = const valueInputField: HTMLInputElement | null =
document.querySelector( document.querySelector(
'.woocommerce-add-attribute-modal__table-row-' + '.woocommerce-new-attribute-modal__table-row-' +
index + index +
' .woocommerce-add-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input' ' .woocommerce-new-attribute-modal__table-attribute-value-column .woocommerce-experimental-select-control__input'
); );
if ( valueInputField ) { if ( valueInputField ) {
valueInputField.focus(); valueInputField.focus();
@ -198,16 +197,16 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
onClose( values ); onClose( values );
} }
} } } }
className="woocommerce-add-attribute-modal" className="woocommerce-new-attribute-modal"
> >
<Notice isDismissible={ false }> <Notice isDismissible={ false }>
<p>{ notice }</p> <p>{ notice }</p>
</Notice> </Notice>
<div className="woocommerce-add-attribute-modal__body"> <div className="woocommerce-new-attribute-modal__body">
<table className="woocommerce-add-attribute-modal__table"> <table className="woocommerce-new-attribute-modal__table">
<thead> <thead>
<tr className="woocommerce-add-attribute-modal__table-header"> <tr className="woocommerce-new-attribute-modal__table-header">
<th>{ attributeLabel }</th> <th>{ attributeLabel }</th>
<th>{ valueLabel }</th> <th>{ valueLabel }</th>
</tr> </tr>
@ -217,9 +216,9 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
( attribute, index ) => ( ( attribute, index ) => (
<tr <tr
key={ index } key={ index }
className={ `woocommerce-add-attribute-modal__table-row woocommerce-add-attribute-modal__table-row-${ index }` } className={ `woocommerce-new-attribute-modal__table-row woocommerce-new-attribute-modal__table-row-${ index }` }
> >
<td className="woocommerce-add-attribute-modal__table-attribute-column"> <td className="woocommerce-new-attribute-modal__table-attribute-column">
<AttributeInputField <AttributeInputField
placeholder={ placeholder={
attributePlaceholder attributePlaceholder
@ -265,7 +264,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
] } ] }
/> />
</td> </td>
<td className="woocommerce-add-attribute-modal__table-attribute-value-column"> <td className="woocommerce-new-attribute-modal__table-attribute-value-column">
{ attribute === null || { attribute === null ||
attribute.id !== 0 ? ( attribute.id !== 0 ? (
<AttributeTermInputField <AttributeTermInputField
@ -329,7 +328,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
/> />
) } ) }
</td> </td>
<td className="woocommerce-add-attribute-modal__table-attribute-trash-column"> <td className="woocommerce-new-attribute-modal__table-attribute-trash-column">
<Button <Button
icon={ trash } icon={ trash }
disabled={ disabled={
@ -361,7 +360,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
</div> </div>
<div> <div>
<Button <Button
className="woocommerce-add-attribute-modal__add-attribute" className="woocommerce-new-attribute-modal__add-attribute"
variant="tertiary" variant="tertiary"
label={ addAnotherAccessibleLabel } label={ addAnotherAccessibleLabel }
onClick={ () => { onClick={ () => {
@ -374,7 +373,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
{ addAnotherLabel } { addAnotherLabel }
</Button> </Button>
</div> </div>
<div className="woocommerce-add-attribute-modal__buttons"> <div className="woocommerce-new-attribute-modal__buttons">
<Button <Button
isSecondary isSecondary
label={ cancelLabel } label={ cancelLabel }

View File

@ -144,7 +144,6 @@ describe( 'AttributeControl', () => {
<AttributeControl <AttributeControl
value={ [ ...attributeList ] } value={ [ ...attributeList ] }
onChange={ () => {} } onChange={ () => {} }
attributeType="for-variations"
/> />
); );
} ); } );

View File

@ -7,7 +7,7 @@ import { ProductAttribute, ProductAttributeTerm } from '@woocommerce/data';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { AddAttributeModal } from '../add-attribute-modal'; import { NewAttributeModal } from '../new-attribute-modal';
let attributeOnChange: ( val: ProductAttribute ) => void; let attributeOnChange: ( val: ProductAttribute ) => void;
jest.mock( '../../attribute-input-field', () => ( { jest.mock( '../../attribute-input-field', () => ( {
@ -118,14 +118,14 @@ const attributeTermList: ProductAttributeTerm[] = [
}, },
]; ];
describe( 'AddAttributeModal', () => { describe( 'NewAttributeModal', () => {
beforeEach( () => { beforeEach( () => {
jest.clearAllMocks(); jest.clearAllMocks();
} ); } );
it( 'should render at-least one row with the attribute dropdown fields', () => { it( 'should render at-least one row with the attribute dropdown fields', () => {
const { queryAllByText } = render( const { queryAllByText } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ () => {} } onAdd={ () => {} }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -139,7 +139,7 @@ describe( 'AddAttributeModal', () => {
it( 'should enable attribute term field once attribute is selected', () => { it( 'should enable attribute term field once attribute is selected', () => {
const { queryAllByText } = render( const { queryAllByText } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ () => {} } onAdd={ () => {} }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -155,7 +155,7 @@ describe( 'AddAttributeModal', () => {
it( 'should allow us to add multiple new rows with the attribute fields', () => { it( 'should allow us to add multiple new rows with the attribute fields', () => {
const { queryAllByText, queryByRole } = render( const { queryAllByText, queryByRole } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ () => {} } onAdd={ () => {} }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -175,7 +175,7 @@ describe( 'AddAttributeModal', () => {
it( 'should allow us to remove the added fields', () => { it( 'should allow us to remove the added fields', () => {
const { queryAllByText, queryByRole, queryAllByLabelText } = render( const { queryAllByText, queryByRole, queryAllByLabelText } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ () => {} } onAdd={ () => {} }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -201,7 +201,7 @@ describe( 'AddAttributeModal', () => {
it( 'should not allow us to remove all the rows', () => { it( 'should not allow us to remove all the rows', () => {
const { queryAllByText, queryAllByLabelText } = render( const { queryAllByText, queryAllByLabelText } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ () => {} } onAdd={ () => {} }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -221,7 +221,7 @@ describe( 'AddAttributeModal', () => {
it( 'should not return empty attribute rows', () => { it( 'should not return empty attribute rows', () => {
const onAddMock = jest.fn(); const onAddMock = jest.fn();
const { queryAllByText, queryByLabelText, queryByRole } = render( const { queryAllByText, queryByLabelText, queryByRole } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ onAddMock } onAdd={ onAddMock }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -247,7 +247,7 @@ describe( 'AddAttributeModal', () => {
it( 'should not add attribute if no terms were selected', () => { it( 'should not add attribute if no terms were selected', () => {
const onAddMock = jest.fn(); const onAddMock = jest.fn();
const { queryByRole } = render( const { queryByRole } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ onAddMock } onAdd={ onAddMock }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }
@ -265,7 +265,7 @@ describe( 'AddAttributeModal', () => {
it( 'should add attribute with array of terms', () => { it( 'should add attribute with array of terms', () => {
const onAddMock = jest.fn(); const onAddMock = jest.fn();
const { queryByRole } = render( const { queryByRole } = render(
<AddAttributeModal <NewAttributeModal
onCancel={ () => {} } onCancel={ () => {} }
onAdd={ onAddMock } onAdd={ onAddMock }
selectedAttributeIds={ [] } selectedAttributeIds={ [] }

View File

@ -15,6 +15,15 @@ export function getAttributeKey(
return attribute.id !== 0 ? attribute.id : attribute.name; return attribute.id !== 0 ? attribute.id : attribute.name;
} }
/**
* Get an attribute ID that works universally across global and local attributes.
*
* @param attribute Product attribute.
* @return string
*/
export const getAttributeId = ( attribute: ProductAttribute ) =>
`${ attribute.id }-${ attribute.name }`;
/** /**
* Updates the position of a product attribute from the new items list. * Updates the position of a product attribute from the new items list.
* *

View File

@ -2,25 +2,24 @@
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { DragEventHandler } from 'react';
import { Button } from '@wordpress/components'; import { Button } from '@wordpress/components';
import { ListItem } from '@woocommerce/components'; import { ListItem } from '@woocommerce/components';
type AddAttributeListItemProps = { type NewAttributeListItemProps = {
label?: string; label?: string;
onAddClick?: () => void; onClick?: () => void;
}; };
export const AddAttributeListItem: React.FC< AddAttributeListItemProps > = ( { export const NewAttributeListItem: React.FC< NewAttributeListItemProps > = ( {
label = __( 'Add attribute', 'woocommerce' ), label = __( 'Add attribute', 'woocommerce' ),
onAddClick, onClick,
} ) => { } ) => {
return ( return (
<ListItem className="woocommerce-add-attribute-list-item"> <ListItem className="woocommerce-add-attribute-list-item">
<Button <Button
variant="secondary" variant="secondary"
className="woocommerce-add-attribute-list-item__add-button" className="woocommerce-add-attribute-list-item__add-button"
onClick={ onAddClick } onClick={ onClick }
> >
{ label } { label }
</Button> </Button>

View File

@ -2,6 +2,7 @@
* External dependencies * External dependencies
*/ */
import { ProductAttribute } from '@woocommerce/data'; import { ProductAttribute } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
/** /**
* Internal dependencies * Internal dependencies
@ -28,9 +29,33 @@ export const Attributes: React.FC< AttributesProps > = ( {
return ( return (
<AttributeControl <AttributeControl
attributeType="regular"
value={ attributes } value={ attributes }
onAdd={ () => {
recordEvent( 'product_add_attributes_modal_add_button_click' );
} }
onChange={ handleChange } onChange={ handleChange }
onNewModalCancel={ () => {
recordEvent(
'product_add_attributes_modal_cancel_button_click'
);
} }
onNewModalOpen={ () => {
if ( ! attributes.length ) {
recordEvent( 'product_add_first_attribute_button_click' );
return;
}
recordEvent( 'product_add_attribute_button' );
} }
onRemove={ () =>
recordEvent(
'product_remove_attribute_confirmation_confirm_click'
)
}
onRemoveCancel={ () =>
recordEvent(
'product_remove_attribute_confirmation_cancel_click'
)
}
/> />
); );
}; };

View File

@ -1,7 +1,9 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n';
import { Product, ProductAttribute } from '@woocommerce/data'; import { Product, ProductAttribute } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useFormContext } from '@woocommerce/components'; import { useFormContext } from '@woocommerce/components';
/** /**
@ -40,9 +42,38 @@ export const Options: React.FC< OptionsProps > = ( {
return ( return (
<AttributeControl <AttributeControl
attributeType="for-variations"
value={ attributes } value={ attributes }
onAdd={ () => {
recordEvent( 'product_add_options_modal_add_button_click' );
} }
onChange={ handleChange } onChange={ handleChange }
onNewModalCancel={ () => {
recordEvent( 'product_add_options_modal_cancel_button_click' );
} }
onNewModalOpen={ () => {
if ( ! attributes.length ) {
recordEvent( 'product_add_first_option_button_click' );
return;
}
recordEvent( 'product_add_option_button' );
} }
uiStrings={ {
emptyStateSubtitle: __( 'No options yet', 'woocommerce' ),
newAttributeListItemLabel: __( 'Add option', 'woocommerce' ),
newAttributeModalTitle: __( 'Add options', 'woocommerce' ),
globalAttributeHelperMessage: __(
`You can change the option's name in {{link}}Attributes{{/link}}.`,
'woocommerce'
),
} }
onRemove={ () =>
recordEvent(
'product_remove_option_confirmation_confirm_click'
)
}
onRemoveCancel={ () =>
recordEvent( 'product_remove_option_confirmation_cancel_click' )
}
/> />
); );
}; };

View File

@ -78,12 +78,21 @@ export function useProductAttributes( {
}; };
}; };
const getAugmentedAttributes = ( atts: ProductAttribute[] ) => {
return atts.map( ( attribute, index ) => ( {
...attribute,
variation: isVariationAttributes,
position: attributes.length + index,
} ) );
};
const handleChange = ( newAttributes: ProductAttribute[] ) => { const handleChange = ( newAttributes: ProductAttribute[] ) => {
const augmentedAttributes = getAugmentedAttributes( newAttributes );
const otherAttributes = isVariationAttributes const otherAttributes = isVariationAttributes
? allAttributes.filter( ( attribute ) => ! attribute.variation ) ? allAttributes.filter( ( attribute ) => ! attribute.variation )
: allAttributes.filter( ( attribute ) => !! attribute.variation ); : allAttributes.filter( ( attribute ) => !! attribute.variation );
setAttributes( newAttributes ); setAttributes( augmentedAttributes );
onChange( [ ...otherAttributes, ...newAttributes ] ); onChange( [ ...otherAttributes, ...augmentedAttributes ] );
}; };
useEffect( () => { useEffect( () => {

View File

@ -0,0 +1,4 @@
Significance: minor
Type: tweak
Add IR and fields priorities to list of get_country_locale() method to follow conventional way of addressing in Iran.

View File

@ -0,0 +1,4 @@
Significance: patch
Type: enhancement
Change the sass variable names to more predictable ones.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
Remove attribute type logic from attribute component

View File

@ -0,0 +1,5 @@
Significance: patch
Type: update
Comment: Mark purchase tests skipped temporarily due to a change in the endpoint behavior

View File

@ -0,0 +1,4 @@
Significance: patch
Type: update
Update woocommerce-blocks to 9.4.3.

View File

@ -293,7 +293,7 @@
} }
mark.yes { mark.yes {
color: $green; color: var(--wc-green);
} }
mark.no { mark.no {

View File

@ -21,7 +21,7 @@
"maxmind-db/reader": "^1.11", "maxmind-db/reader": "^1.11",
"pelago/emogrifier": "^6.0", "pelago/emogrifier": "^6.0",
"woocommerce/action-scheduler": "3.5.4", "woocommerce/action-scheduler": "3.5.4",
"woocommerce/woocommerce-blocks": "9.4.2" "woocommerce/woocommerce-blocks": "9.4.3"
}, },
"require-dev": { "require-dev": {
"automattic/jetpack-changelogger": "^3.3.0", "automattic/jetpack-changelogger": "^3.3.0",

File diff suppressed because it is too large Load Diff

View File

@ -1164,6 +1164,20 @@ class WC_Countries {
'label' => __( 'State', 'woocommerce' ), 'label' => __( 'State', 'woocommerce' ),
), ),
), ),
'IR' => array(
'state' => array(
'priority' => 50,
),
'city' => array(
'priority' => 60,
),
'address_1' => array(
'priority' => 70,
),
'address_2' => array(
'priority' => 80,
),
),
'IT' => array( 'IT' => array(
'postcode' => array( 'postcode' => array(
'priority' => 65, 'priority' => 65,

View File

@ -82,6 +82,7 @@ class WC_Admin_Tests_OnboardingTasks_Task_Purchase extends WC_Unit_Test_Case {
* Test is_complete function of Purchase task. * Test is_complete function of Purchase task.
*/ */
public function test_is_not_complete_if_remaining_paid_products() { public function test_is_not_complete_if_remaining_paid_products() {
$this->markTestSkipped( 'Skipped temporarily due to change in endpoint behavior.' );
update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'memberships' ) ) ); update_option( OnboardingProfile::DATA_OPTION, array( 'product_types' => array( 'memberships' ) ) );
$this->assertEquals( false, $this->task->is_complete() ); $this->assertEquals( false, $this->task->is_complete() );
} }
@ -160,6 +161,7 @@ class WC_Admin_Tests_OnboardingTasks_Task_Purchase extends WC_Unit_Test_Case {
* Test the task title if 2 paid items exist. * Test the task title if 2 paid items exist.
*/ */
public function test_get_title_if_multiple_paid_themes() { public function test_get_title_if_multiple_paid_themes() {
$this->markTestSkipped( 'Skipped temporarily due to change in endpoint behavior.' );
update_option( update_option(
OnboardingProfile::DATA_OPTION, OnboardingProfile::DATA_OPTION,
array( array(
@ -174,6 +176,7 @@ class WC_Admin_Tests_OnboardingTasks_Task_Purchase extends WC_Unit_Test_Case {
* Test the task title if multiple additional paid items exist. * Test the task title if multiple additional paid items exist.
*/ */
public function test_get_title_if_multiple_paid_products() { public function test_get_title_if_multiple_paid_products() {
$this->markTestSkipped( 'Skipped temporarily due to change in endpoint behavior.' );
update_option( update_option(
OnboardingProfile::DATA_OPTION, OnboardingProfile::DATA_OPTION,
array( array(