Extract attribute filtering and fetching logic out of attribute components (#36354)
* Move attribution fetching to separate hook * Add changelog entry * Set all attributes on update of subset of attributes * Move filtering logic to hook * Remove tests that filter attribute by type inside the component * Rename AttributeField to AttributeControl and props from attributes to value
This commit is contained in:
parent
6377314b1b
commit
ea64a98f54
|
@ -26,7 +26,7 @@ import {
|
|||
AttributeTermInputField,
|
||||
CustomAttributeTermInputField,
|
||||
} from '../attribute-term-input-field';
|
||||
import { HydratedAttributeType } from '../attribute-field';
|
||||
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
|
||||
import { getProductAttributeObject } from './utils';
|
||||
|
||||
type AddAttributeModalProps = {
|
||||
|
@ -46,12 +46,12 @@ type AddAttributeModalProps = {
|
|||
confirmCancelLabel?: string;
|
||||
confirmConfirmLabel?: string;
|
||||
onCancel: () => void;
|
||||
onAdd: ( newCategories: HydratedAttributeType[] ) => void;
|
||||
onAdd: ( newCategories: EnhancedProductAttribute[] ) => void;
|
||||
selectedAttributeIds?: number[];
|
||||
};
|
||||
|
||||
type AttributeForm = {
|
||||
attributes: Array< HydratedAttributeType | null >;
|
||||
attributes: Array< EnhancedProductAttribute | null >;
|
||||
};
|
||||
|
||||
export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
||||
|
@ -92,7 +92,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
};
|
||||
|
||||
const onAddingAttributes = ( values: AttributeForm ) => {
|
||||
const newAttributesToAdd: HydratedAttributeType[] = [];
|
||||
const newAttributesToAdd: EnhancedProductAttribute[] = [];
|
||||
values.attributes.forEach( ( attr ) => {
|
||||
if (
|
||||
attr !== null &&
|
||||
|
@ -105,7 +105,7 @@ export const AddAttributeModal: React.FC< AddAttributeModalProps > = ( {
|
|||
? ( attr.terms || [] ).map( ( term ) => term.name )
|
||||
: attr.options;
|
||||
newAttributesToAdd.push( {
|
||||
...( attr as HydratedAttributeType ),
|
||||
...( attr as EnhancedProductAttribute ),
|
||||
options,
|
||||
} );
|
||||
}
|
|
@ -2,13 +2,8 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useState, useCallback, useEffect } from '@wordpress/element';
|
||||
import {
|
||||
ProductAttribute,
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
|
||||
ProductAttributeTerm,
|
||||
} from '@woocommerce/data';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { ProductAttribute } from '@woocommerce/data';
|
||||
import {
|
||||
Sortable,
|
||||
__experimentalSelectControlMenuSlot as SelectControlMenuSlot,
|
||||
|
@ -24,42 +19,31 @@ import { getAdminLink } from '@woocommerce/settings';
|
|||
import './attribute-field.scss';
|
||||
import { AddAttributeModal } from './add-attribute-modal';
|
||||
import { EditAttributeModal } from './edit-attribute-modal';
|
||||
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
|
||||
import {
|
||||
getAttributeKey,
|
||||
reorderSortableProductAttributePositions,
|
||||
} from './utils';
|
||||
import { sift } from '../../../utils';
|
||||
import { AttributeEmptyState } from '../attribute-empty-state';
|
||||
import {
|
||||
AddAttributeListItem,
|
||||
AttributeListItem,
|
||||
} from '../attribute-list-item';
|
||||
|
||||
type AttributeFieldProps = {
|
||||
type AttributeControlProps = {
|
||||
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 > = ( {
|
||||
export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
||||
value,
|
||||
onChange,
|
||||
productId,
|
||||
attributeType = 'regular',
|
||||
onChange,
|
||||
} ) => {
|
||||
const [ showAddAttributeModal, setShowAddAttributeModal ] =
|
||||
useState( false );
|
||||
const [ hydratedAttributes, setHydratedAttributes ] = useState<
|
||||
HydratedAttributeType[]
|
||||
>( [] );
|
||||
const [ editingAttributeId, setEditingAttributeId ] = useState<
|
||||
null | string
|
||||
>( null );
|
||||
|
@ -72,73 +56,12 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
? 'product_add_options_modal_cancel_button_click'
|
||||
: 'product_add_attributes_modal_cancel_button_click';
|
||||
|
||||
const fetchTerms = useCallback(
|
||||
( attributeId: number ) => {
|
||||
return resolveSelect(
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
|
||||
)
|
||||
.getProductAttributeTerms< ProductAttributeTerm[] >( {
|
||||
attribute_id: attributeId,
|
||||
product: productId,
|
||||
} )
|
||||
.then(
|
||||
( attributeTerms ) => {
|
||||
return attributeTerms;
|
||||
},
|
||||
( error ) => {
|
||||
return error;
|
||||
}
|
||||
);
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
// 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 || hydratedAttributes.length !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [ customAttributes, globalAttributes ]: ProductAttribute[][] =
|
||||
sift( value, ( attr: ProductAttribute ) => attr.id === 0 );
|
||||
|
||||
Promise.all(
|
||||
globalAttributes.map( ( attr ) => fetchTerms( attr.id ) )
|
||||
).then( ( allResults ) => {
|
||||
setHydratedAttributes( [
|
||||
...globalAttributes.map( ( attr, index ) => {
|
||||
const fetchedTerms = allResults[ index ];
|
||||
|
||||
const newAttr = {
|
||||
...attr,
|
||||
// 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,
|
||||
] );
|
||||
} );
|
||||
}, [ fetchTerms, hydratedAttributes, value ] );
|
||||
|
||||
const fetchAttributeId = ( attribute: { id: number; name: string } ) =>
|
||||
`${ attribute.id }-${ attribute.name }`;
|
||||
|
||||
const updateAttributes = ( attributes: HydratedAttributeType[] ) => {
|
||||
setHydratedAttributes( attributes );
|
||||
const handleChange = ( newAttributes: EnhancedProductAttribute[] ) => {
|
||||
onChange(
|
||||
attributes.map( ( attr ) => {
|
||||
newAttributes.map( ( attr ) => {
|
||||
return {
|
||||
...attr,
|
||||
options: attr.terms
|
||||
|
@ -157,8 +80,8 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
recordEvent(
|
||||
'product_remove_attribute_confirmation_confirm_click'
|
||||
);
|
||||
updateAttributes(
|
||||
hydratedAttributes.filter(
|
||||
handleChange(
|
||||
value.filter(
|
||||
( attr ) =>
|
||||
fetchAttributeId( attr ) !==
|
||||
fetchAttributeId( attribute )
|
||||
|
@ -169,9 +92,11 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
}
|
||||
};
|
||||
|
||||
const onAddNewAttributes = ( newAttributes: HydratedAttributeType[] ) => {
|
||||
updateAttributes( [
|
||||
...( hydratedAttributes || [] ),
|
||||
const onAddNewAttributes = (
|
||||
newAttributes: EnhancedProductAttribute[]
|
||||
) => {
|
||||
handleChange( [
|
||||
...( value || [] ),
|
||||
...newAttributes
|
||||
.filter(
|
||||
( newAttr ) =>
|
||||
|
@ -193,18 +118,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
setShowAddAttributeModal( false );
|
||||
};
|
||||
|
||||
const filteredAttributes = value
|
||||
? value.filter(
|
||||
( attribute: ProductAttribute ) =>
|
||||
attribute.variation === isOnlyForVariations
|
||||
)
|
||||
: false;
|
||||
|
||||
if (
|
||||
! filteredAttributes ||
|
||||
filteredAttributes.length === 0 ||
|
||||
hydratedAttributes.length === 0
|
||||
) {
|
||||
if ( ! value.length ) {
|
||||
return (
|
||||
<>
|
||||
<AttributeEmptyState
|
||||
|
@ -232,9 +146,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
setShowAddAttributeModal( false );
|
||||
} }
|
||||
onAdd={ onAddNewAttributes }
|
||||
selectedAttributeIds={ ( filteredAttributes || [] ).map(
|
||||
( attr ) => attr.id
|
||||
) }
|
||||
selectedAttributeIds={ [] }
|
||||
/>
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
|
@ -242,9 +154,8 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
);
|
||||
}
|
||||
|
||||
const sortedAttributes = filteredAttributes.sort(
|
||||
( a, b ) => a.position - b.position
|
||||
);
|
||||
const sortedAttributes = value.sort( ( a, b ) => a.position - b.position );
|
||||
|
||||
const attributeKeyValues = value.reduce(
|
||||
(
|
||||
keyValue: Record< number | string, ProductAttribute >,
|
||||
|
@ -256,9 +167,9 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
{} as Record< number | string, ProductAttribute >
|
||||
);
|
||||
|
||||
const attribute = hydratedAttributes.find(
|
||||
const editingAttribute = value.find(
|
||||
( attr ) => fetchAttributeId( attr ) === editingAttributeId
|
||||
) as HydratedAttributeType;
|
||||
) as EnhancedProductAttribute;
|
||||
|
||||
const editAttributeCopy = isOnlyForVariations
|
||||
? __(
|
||||
|
@ -332,15 +243,13 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
/>
|
||||
) }
|
||||
<SelectControlMenuSlot />
|
||||
{ editingAttributeId && (
|
||||
{ editingAttribute && (
|
||||
<EditAttributeModal
|
||||
title={
|
||||
title={ sprintf(
|
||||
/* translators: %s is the attribute name */
|
||||
sprintf(
|
||||
__( 'Edit %s', 'woocommerce' ),
|
||||
attribute.name
|
||||
)
|
||||
}
|
||||
__( 'Edit %s', 'woocommerce' ),
|
||||
editingAttribute.name
|
||||
) }
|
||||
globalAttributeHelperMessage={ interpolateComponents( {
|
||||
mixedString: editAttributeCopy,
|
||||
components: {
|
||||
|
@ -359,7 +268,7 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
} ) }
|
||||
onCancel={ () => setEditingAttributeId( null ) }
|
||||
onEdit={ ( changedAttribute ) => {
|
||||
const newAttributesSet = [ ...hydratedAttributes ];
|
||||
const newAttributesSet = [ ...value ];
|
||||
const changedAttributeIndex: number =
|
||||
newAttributesSet.findIndex( ( attr ) =>
|
||||
attr.id !== 0
|
||||
|
@ -373,10 +282,10 @@ export const AttributeField: React.FC< AttributeFieldProps > = ( {
|
|||
changedAttribute
|
||||
);
|
||||
|
||||
updateAttributes( newAttributesSet );
|
||||
handleChange( newAttributesSet );
|
||||
setEditingAttributeId( null );
|
||||
} }
|
||||
attribute={ attribute }
|
||||
attribute={ editingAttribute }
|
||||
/>
|
||||
) }
|
||||
</div>
|
|
@ -18,7 +18,7 @@ import {
|
|||
AttributeTermInputField,
|
||||
CustomAttributeTermInputField,
|
||||
} from '../attribute-term-input-field';
|
||||
import { HydratedAttributeType } from './attribute-field';
|
||||
import { EnhancedProductAttribute } from '../../hooks/use-product-attributes';
|
||||
|
||||
import './edit-attribute-modal.scss';
|
||||
|
||||
|
@ -36,8 +36,8 @@ type EditAttributeModalProps = {
|
|||
updateAccessibleLabel?: string;
|
||||
updateLabel?: string;
|
||||
onCancel: () => void;
|
||||
onEdit: ( alteredAttribute: HydratedAttributeType ) => void;
|
||||
attribute: HydratedAttributeType;
|
||||
onEdit: ( alteredAttribute: EnhancedProductAttribute ) => void;
|
||||
attribute: EnhancedProductAttribute;
|
||||
};
|
||||
|
||||
export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
||||
|
@ -64,7 +64,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
attribute,
|
||||
} ) => {
|
||||
const [ editableAttribute, setEditableAttribute ] = useState<
|
||||
HydratedAttributeType | undefined
|
||||
EnhancedProductAttribute | undefined
|
||||
>( { ...attribute } );
|
||||
|
||||
const isCustomAttribute = editableAttribute?.id === 0;
|
||||
|
@ -84,7 +84,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
}
|
||||
onChange={ ( val ) =>
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
...( editableAttribute as EnhancedProductAttribute ),
|
||||
name: val,
|
||||
} )
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
attributeId={ editableAttribute?.id }
|
||||
onChange={ ( val ) => {
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
...( editableAttribute as EnhancedProductAttribute ),
|
||||
terms: val,
|
||||
} );
|
||||
} }
|
||||
|
@ -115,7 +115,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
value={ editableAttribute?.options }
|
||||
onChange={ ( val ) => {
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
...( editableAttribute as EnhancedProductAttribute ),
|
||||
options: val,
|
||||
} );
|
||||
} }
|
||||
|
@ -126,7 +126,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
<CheckboxControl
|
||||
onChange={ ( val ) =>
|
||||
setEditableAttribute( {
|
||||
...( editableAttribute as HydratedAttributeType ),
|
||||
...( editableAttribute as EnhancedProductAttribute ),
|
||||
visible: val,
|
||||
} )
|
||||
}
|
||||
|
@ -148,7 +148,7 @@ export const EditAttributeModal: React.FC< EditAttributeModalProps > = ( {
|
|||
isPrimary
|
||||
label={ updateAccessibleLabel }
|
||||
onClick={ () => {
|
||||
onEdit( editableAttribute as HydratedAttributeType );
|
||||
onEdit( editableAttribute as EnhancedProductAttribute );
|
||||
} }
|
||||
>
|
||||
{ updateLabel }
|
|
@ -0,0 +1 @@
|
|||
export * from './attribute-control';
|
|
@ -8,7 +8,7 @@ import { ProductAttribute } from '@woocommerce/data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
import { AttributeControl } from '../attribute-control';
|
||||
|
||||
const attributeList: ProductAttribute[] = [
|
||||
{
|
||||
|
@ -102,7 +102,7 @@ jest.mock( '@woocommerce/components', () => ( {
|
|||
},
|
||||
} ) );
|
||||
|
||||
describe( 'AttributeField', () => {
|
||||
describe( 'AttributeControl', () => {
|
||||
beforeEach( () => {
|
||||
jest.clearAllMocks();
|
||||
} );
|
||||
|
@ -110,17 +110,17 @@ describe( 'AttributeField', () => {
|
|||
describe( 'empty state', () => {
|
||||
it( 'should show subtitle and "Add first attribute" button', () => {
|
||||
const { queryByText } = render(
|
||||
<AttributeField value={ [] } onChange={ () => {} } />
|
||||
<AttributeControl value={ [] } onChange={ () => {} } />
|
||||
);
|
||||
expect( queryByText( 'No attributes yet' ) ).toBeInTheDocument();
|
||||
expect( queryByText( 'Add first attribute' ) ).toBeInTheDocument();
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should render the list of existing attributes', async () => {
|
||||
it( 'should render the list of all attributes', async () => {
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ () => {} }
|
||||
/>
|
||||
|
@ -128,20 +128,20 @@ describe( 'AttributeField', () => {
|
|||
} );
|
||||
|
||||
expect(
|
||||
await screen.findByText( 'No attributes yet' )
|
||||
await screen.queryByText( 'No attributes yet' )
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 0 ].name )
|
||||
await screen.queryByText( attributeList[ 0 ].name )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.queryByText( attributeList[ 1 ].name )
|
||||
).not.toBeInTheDocument();
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should render the first two terms of each option, and show "+ n more" for the rest', async () => {
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ () => {} }
|
||||
attributeType="for-variations"
|
||||
|
@ -149,9 +149,6 @@ describe( 'AttributeField', () => {
|
|||
);
|
||||
} );
|
||||
|
||||
expect(
|
||||
await screen.queryByText( attributeList[ 0 ].options[ 0 ] )
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText( attributeList[ 1 ].options[ 0 ] )
|
||||
).toBeInTheDocument();
|
||||
|
@ -173,7 +170,7 @@ describe( 'AttributeField', () => {
|
|||
jest.spyOn( global, 'confirm' ).mockReturnValueOnce( false );
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ () => {} }
|
||||
/>
|
||||
|
@ -191,7 +188,7 @@ describe( 'AttributeField', () => {
|
|||
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
|
@ -211,7 +208,7 @@ describe( 'AttributeField', () => {
|
|||
const onChange = jest.fn();
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ onChange }
|
||||
/>
|
||||
|
@ -232,7 +229,7 @@ describe( 'AttributeField', () => {
|
|||
|
||||
act( () => {
|
||||
render(
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
value={ [ ...attributeList ] }
|
||||
onChange={ onChange }
|
||||
/>
|
|
@ -1 +0,0 @@
|
|||
export * from './attribute-field';
|
|
@ -22,12 +22,12 @@ import {
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './attribute-input-field.scss';
|
||||
import { HydratedAttributeType } from '../attribute-field';
|
||||
import { EnhancedProductAttribute } from '~/products/hooks/use-product-attributes';
|
||||
|
||||
type NarrowedQueryAttribute = Pick< QueryProductAttribute, 'id' | 'name' >;
|
||||
|
||||
type AttributeInputFieldProps = {
|
||||
value?: HydratedAttributeType | null;
|
||||
value?: EnhancedProductAttribute | null;
|
||||
onChange: (
|
||||
value?:
|
||||
| Omit< ProductAttribute, 'position' | 'visible' | 'variation' >
|
||||
|
|
|
@ -6,7 +6,8 @@ import { ProductAttribute } from '@woocommerce/data';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
import { AttributeControl } from '../attribute-control';
|
||||
import { useProductAttributes } from '~/products/hooks/use-product-attributes';
|
||||
|
||||
type AttributesProps = {
|
||||
value: ProductAttribute[];
|
||||
|
@ -19,12 +20,17 @@ export const Attributes: React.FC< AttributesProps > = ( {
|
|||
onChange,
|
||||
productId,
|
||||
} ) => {
|
||||
const { attributes, handleChange } = useProductAttributes( {
|
||||
allAttributes: value,
|
||||
onChange,
|
||||
productId,
|
||||
} );
|
||||
|
||||
return (
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
attributeType="regular"
|
||||
value={ value }
|
||||
onChange={ onChange }
|
||||
productId={ productId }
|
||||
value={ attributes }
|
||||
onChange={ handleChange }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,8 @@ import { useFormContext } from '@woocommerce/components';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributeField } from '../attribute-field';
|
||||
import { AttributeControl } from '../attribute-control';
|
||||
import { useProductAttributes } from '~/products/hooks/use-product-attributes';
|
||||
import { useProductVariationsHelper } from '../../hooks/use-product-variations-helper';
|
||||
|
||||
type OptionsProps = {
|
||||
|
@ -24,17 +25,24 @@ export const Options: React.FC< OptionsProps > = ( {
|
|||
const { values } = useFormContext< Product >();
|
||||
const { generateProductVariations } = useProductVariationsHelper();
|
||||
|
||||
const handleChange = async ( attributes: ProductAttribute[] ) => {
|
||||
onChange( attributes );
|
||||
generateProductVariations( { ...values, attributes } );
|
||||
};
|
||||
const { attributes, handleChange } = useProductAttributes( {
|
||||
allAttributes: value,
|
||||
isVariationAttributes: true,
|
||||
onChange: ( newAttributes ) => {
|
||||
onChange( newAttributes );
|
||||
generateProductVariations( {
|
||||
...values,
|
||||
attributes: newAttributes,
|
||||
} );
|
||||
},
|
||||
productId,
|
||||
} );
|
||||
|
||||
return (
|
||||
<AttributeField
|
||||
<AttributeControl
|
||||
attributeType="for-variations"
|
||||
value={ value }
|
||||
value={ attributes }
|
||||
onChange={ handleChange }
|
||||
productId={ productId }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME,
|
||||
ProductAttribute,
|
||||
ProductAttributeTerm,
|
||||
} from '@woocommerce/data';
|
||||
import { resolveSelect } from '@wordpress/data';
|
||||
import { useCallback, useEffect, useState } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { sift } from '../../utils';
|
||||
|
||||
type useProductAttributesProps = {
|
||||
allAttributes: ProductAttribute[];
|
||||
isVariationAttributes?: boolean;
|
||||
onChange: ( attributes: ProductAttribute[] ) => void;
|
||||
productId?: number;
|
||||
};
|
||||
|
||||
export type EnhancedProductAttribute = ProductAttribute & {
|
||||
terms?: ProductAttributeTerm[];
|
||||
visible?: boolean;
|
||||
};
|
||||
|
||||
export function useProductAttributes( {
|
||||
allAttributes = [],
|
||||
isVariationAttributes = false,
|
||||
onChange,
|
||||
productId,
|
||||
}: useProductAttributesProps ) {
|
||||
const getFilteredAttributes = () => {
|
||||
return isVariationAttributes
|
||||
? allAttributes.filter( ( attribute ) => !! attribute.variation )
|
||||
: allAttributes.filter( ( attribute ) => ! attribute.variation );
|
||||
};
|
||||
|
||||
const [ attributes, setAttributes ] = useState<
|
||||
EnhancedProductAttribute[]
|
||||
>( getFilteredAttributes() );
|
||||
const [ localAttributes, globalAttributes ]: ProductAttribute[][] = sift(
|
||||
attributes,
|
||||
( attr: ProductAttribute ) => attr.id === 0
|
||||
);
|
||||
|
||||
const fetchTerms = useCallback(
|
||||
( attributeId: number ) => {
|
||||
return resolveSelect(
|
||||
EXPERIMENTAL_PRODUCT_ATTRIBUTE_TERMS_STORE_NAME
|
||||
)
|
||||
.getProductAttributeTerms< ProductAttributeTerm[] >( {
|
||||
attribute_id: attributeId,
|
||||
product: productId,
|
||||
} )
|
||||
.then(
|
||||
( attributeTerms ) => {
|
||||
return attributeTerms;
|
||||
},
|
||||
( error ) => {
|
||||
return error;
|
||||
}
|
||||
);
|
||||
},
|
||||
[ productId ]
|
||||
);
|
||||
|
||||
const enhanceAttribute = (
|
||||
globalAttribute: ProductAttribute,
|
||||
terms: ProductAttributeTerm[]
|
||||
) => {
|
||||
return {
|
||||
...globalAttribute,
|
||||
terms: terms.length > 0 ? terms : undefined,
|
||||
options: terms.length === 0 ? globalAttribute.options : [],
|
||||
};
|
||||
};
|
||||
|
||||
const handleChange = ( newAttributes: ProductAttribute[] ) => {
|
||||
const otherAttributes = isVariationAttributes
|
||||
? allAttributes.filter( ( attribute ) => ! attribute.variation )
|
||||
: allAttributes.filter( ( attribute ) => !! attribute.variation );
|
||||
setAttributes( newAttributes );
|
||||
onChange( [ ...otherAttributes, ...newAttributes ] );
|
||||
};
|
||||
|
||||
useEffect( () => {
|
||||
if ( ! getFilteredAttributes().length || attributes.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all(
|
||||
globalAttributes.map( ( attr ) => fetchTerms( attr.id ) )
|
||||
).then( ( termData ) => {
|
||||
setAttributes( [
|
||||
...globalAttributes.map( ( attr, index ) =>
|
||||
enhanceAttribute( attr, termData[ index ] )
|
||||
),
|
||||
...localAttributes,
|
||||
] );
|
||||
} );
|
||||
}, [ allAttributes, attributes, fetchTerms ] );
|
||||
|
||||
return {
|
||||
attributes,
|
||||
handleChange,
|
||||
setAttributes,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Move product attribute fetching logic into a separate hook
|
Loading…
Reference in New Issue