Auto delete unused variations when auto generating variations (#39733)
* Add auto delete functionality for variations * Add the remove confirmation modal * Update delete unmatched product variation logic * Add tests * Add changelogs * Fix lint errors * Fix lint errors
This commit is contained in:
parent
e8d0a081ad
commit
3edd1bd823
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add isGeneratingVariations selector to product variations store.
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { combineReducers, registerStore, StoreConfig } from '@wordpress/data';
|
||||
import { registerStore, StoreConfig } from '@wordpress/data';
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
/**
|
||||
|
@ -28,8 +28,6 @@ export const createCrudDataStore = ( {
|
|||
pluralResourceName,
|
||||
storeConfig = {},
|
||||
}: CrudDataStore ) => {
|
||||
const crudReducer = createReducer();
|
||||
|
||||
const crudActions = createDispatchActions( {
|
||||
resourceName,
|
||||
namespace,
|
||||
|
@ -54,13 +52,10 @@ export const createCrudDataStore = ( {
|
|||
controls = {},
|
||||
} = storeConfig;
|
||||
|
||||
const crudReducer = createReducer( reducer );
|
||||
|
||||
registerStore( storeName, {
|
||||
reducer: reducer
|
||||
? ( combineReducers( {
|
||||
crudReducer,
|
||||
reducer,
|
||||
} ) as Reducer )
|
||||
: ( crudReducer as Reducer< ResourceState > ),
|
||||
reducer: crudReducer as Reducer< ResourceState >,
|
||||
actions: { ...crudActions, ...actions },
|
||||
selectors: { ...crudSelectors, ...selectors },
|
||||
resolvers: { ...crudResolvers, ...resolvers },
|
||||
|
|
|
@ -27,7 +27,9 @@ export type ResourceState = {
|
|||
requesting: Record< string, boolean >;
|
||||
};
|
||||
|
||||
export const createReducer = () => {
|
||||
export const createReducer = (
|
||||
additionalReducer?: Reducer< ResourceState >
|
||||
) => {
|
||||
const reducer: Reducer< ResourceState, Actions > = (
|
||||
state = {
|
||||
items: {},
|
||||
|
@ -284,11 +286,11 @@ export const createReducer = () => {
|
|||
) ]: true,
|
||||
},
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
if ( additionalReducer ) {
|
||||
return additionalReducer( state, payload );
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export enum TYPES {
|
||||
GENERATE_VARIATIONS_REQUEST = 'GENERATE_VARIATIONS_REQUEST',
|
||||
GENERATE_VARIATIONS_SUCCESS = 'GENERATE_VARIATIONS_SUCCESS',
|
||||
GENERATE_VARIATIONS_ERROR = 'GENERATE_VARIATIONS_ERROR',
|
||||
BATCH_UPDATE_VARIATIONS_ERROR = 'BATCH_UPDATE_VARIATIONS_ERROR',
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
import { controls } from '@wordpress/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -10,22 +11,67 @@ import { getUrlParameters, getRestPath, parseId } from '../crud/utils';
|
|||
import TYPES from './action-types';
|
||||
import { IdQuery, IdType, Item } from '../crud/types';
|
||||
import { WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
|
||||
import type { BatchUpdateRequest, BatchUpdateResponse } from './types';
|
||||
import type {
|
||||
BatchUpdateRequest,
|
||||
BatchUpdateResponse,
|
||||
GenerateRequest,
|
||||
} from './types';
|
||||
import CRUD_ACTIONS from './crud-actions';
|
||||
import { ProductAttribute } from '../products/types';
|
||||
|
||||
export function generateProductVariationsError( key: IdType, error: unknown ) {
|
||||
return {
|
||||
type: TYPES.GENERATE_VARIATIONS_ERROR as const,
|
||||
key,
|
||||
error,
|
||||
errorType: 'GENERATE_VARIATIONS',
|
||||
errorType: CRUD_ACTIONS.GENERATE_VARIATIONS,
|
||||
};
|
||||
}
|
||||
|
||||
export const generateProductVariations = function* ( idQuery: IdQuery ) {
|
||||
export function generateProductVariationsRequest( key: IdType ) {
|
||||
return {
|
||||
type: TYPES.GENERATE_VARIATIONS_REQUEST as const,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateProductVariationsSuccess( key: IdType ) {
|
||||
return {
|
||||
type: TYPES.GENERATE_VARIATIONS_SUCCESS as const,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
export const generateProductVariations = function* (
|
||||
idQuery: IdQuery,
|
||||
productData: {
|
||||
type?: string;
|
||||
attributes: ProductAttribute[];
|
||||
},
|
||||
data: GenerateRequest
|
||||
) {
|
||||
const urlParameters = getUrlParameters(
|
||||
WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
idQuery
|
||||
);
|
||||
const { key } = parseId( idQuery, urlParameters );
|
||||
yield generateProductVariationsRequest( key );
|
||||
|
||||
try {
|
||||
yield controls.dispatch(
|
||||
'core',
|
||||
'saveEntityRecord',
|
||||
'postType',
|
||||
'product',
|
||||
{
|
||||
id: urlParameters[ 0 ],
|
||||
...productData,
|
||||
}
|
||||
);
|
||||
} catch ( error ) {
|
||||
yield generateProductVariationsError( key, error );
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
const result: Item = yield apiFetch( {
|
||||
|
@ -35,12 +81,12 @@ export const generateProductVariations = function* ( idQuery: IdQuery ) {
|
|||
urlParameters
|
||||
),
|
||||
method: 'POST',
|
||||
data,
|
||||
} );
|
||||
|
||||
yield generateProductVariationsSuccess( key );
|
||||
return result;
|
||||
} catch ( error ) {
|
||||
const { key } = parseId( idQuery, urlParameters );
|
||||
|
||||
yield generateProductVariationsError( key, error );
|
||||
throw error;
|
||||
}
|
||||
|
@ -86,3 +132,9 @@ export function* batchUpdateProductVariations(
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export type Actions = ReturnType<
|
||||
| typeof generateProductVariationsRequest
|
||||
| typeof generateProductVariationsError
|
||||
| typeof generateProductVariationsSuccess
|
||||
>;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
export enum CRUD_ACTIONS {
|
||||
GENERATE_VARIATIONS = 'GENERATE_VARIATIONS',
|
||||
}
|
||||
|
||||
export default CRUD_ACTIONS;
|
|
@ -1,9 +1,17 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_NAME, WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
|
||||
import { createCrudDataStore } from '../crud';
|
||||
import * as actions from './actions';
|
||||
import * as selectors from './selectors';
|
||||
import { reducer } from './reducer';
|
||||
import { ResourceState } from '../crud/reducer';
|
||||
|
||||
createCrudDataStore( {
|
||||
storeName: STORE_NAME,
|
||||
|
@ -11,7 +19,9 @@ createCrudDataStore( {
|
|||
pluralResourceName: 'ProductVariations',
|
||||
namespace: WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
storeConfig: {
|
||||
reducer: reducer as Reducer< ResourceState >,
|
||||
actions,
|
||||
selectors,
|
||||
},
|
||||
} );
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Reducer } from 'redux';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Actions } from './actions';
|
||||
import { TYPES } from './action-types';
|
||||
import { ResourceState } from '../crud/reducer';
|
||||
import { getRequestIdentifier } from '../crud/utils';
|
||||
import CRUD_ACTIONS from './crud-actions';
|
||||
|
||||
const reducer: Reducer< ResourceState, Actions > = (
|
||||
state = {
|
||||
items: {},
|
||||
data: {},
|
||||
itemsCount: {},
|
||||
errors: {},
|
||||
requesting: {},
|
||||
},
|
||||
payload
|
||||
) => {
|
||||
if ( payload && 'type' in payload ) {
|
||||
switch ( payload.type ) {
|
||||
case TYPES.GENERATE_VARIATIONS_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
requesting: {
|
||||
...state.requesting,
|
||||
[ getRequestIdentifier(
|
||||
CRUD_ACTIONS.GENERATE_VARIATIONS,
|
||||
payload.key
|
||||
) ]: true,
|
||||
},
|
||||
};
|
||||
case TYPES.GENERATE_VARIATIONS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
requesting: {
|
||||
...state.requesting,
|
||||
[ getRequestIdentifier(
|
||||
CRUD_ACTIONS.GENERATE_VARIATIONS,
|
||||
payload.key
|
||||
) ]: false,
|
||||
},
|
||||
};
|
||||
case TYPES.GENERATE_VARIATIONS_ERROR:
|
||||
return {
|
||||
...state,
|
||||
errors: {
|
||||
...state.errors,
|
||||
[ getRequestIdentifier(
|
||||
payload.errorType,
|
||||
payload.key
|
||||
) ]: payload.error,
|
||||
},
|
||||
requesting: {
|
||||
...state.requesting,
|
||||
[ getRequestIdentifier(
|
||||
CRUD_ACTIONS.GENERATE_VARIATIONS,
|
||||
payload.key
|
||||
) ]: false,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
export { reducer };
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ResourceState } from '../crud/reducer';
|
||||
import { IdQuery } from '../crud/types';
|
||||
import { getRequestIdentifier, getUrlParameters, parseId } from '../crud/utils';
|
||||
import { WC_PRODUCT_VARIATIONS_NAMESPACE } from './constants';
|
||||
import CRUD_ACTIONS from './crud-actions';
|
||||
|
||||
export const isGeneratingVariations = (
|
||||
state: ResourceState,
|
||||
idQuery: IdQuery
|
||||
) => {
|
||||
const urlParameters = getUrlParameters(
|
||||
WC_PRODUCT_VARIATIONS_NAMESPACE,
|
||||
idQuery
|
||||
);
|
||||
const { key } = parseId( idQuery, urlParameters );
|
||||
const itemQuery = getRequestIdentifier(
|
||||
CRUD_ACTIONS.GENERATE_VARIATIONS,
|
||||
key
|
||||
);
|
||||
return state.requesting[ itemQuery ];
|
||||
};
|
|
@ -94,6 +94,10 @@ export type ProductVariationSelectors = CrudSelectors<
|
|||
|
||||
export type ActionDispatchers = DispatchFromMap< ProductVariationActions >;
|
||||
|
||||
export type GenerateRequest = {
|
||||
delete?: boolean;
|
||||
};
|
||||
|
||||
export type BatchUpdateRequest = {
|
||||
create?: Partial< Omit< ProductVariation, 'id' > >[];
|
||||
update?: ( Pick< ProductVariation, 'id' > &
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add new option in attributes control for a delete confirmation modal instead of the window.alert.
|
|
@ -1,4 +1,7 @@
|
|||
.wp-block-woocommerce-product-variations-items-field {
|
||||
.wp-block-woocommerce-product-variation-items-field {
|
||||
@media ( min-width: #{ ($break-medium) } ) {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
.variation-items-product-tour {
|
||||
|
|
|
@ -90,6 +90,7 @@ export function Edit() {
|
|||
] ) }
|
||||
onChange={ handleChange }
|
||||
createNewAttributesAsGlobal={ true }
|
||||
useRemoveConfirmationModal={ true }
|
||||
uiStrings={ {
|
||||
globalAttributeHelperMessage: '',
|
||||
customAttributeHelperMessage: '',
|
||||
|
@ -117,8 +118,8 @@ export function Edit() {
|
|||
'Remove variation option',
|
||||
'woocommerce'
|
||||
),
|
||||
attributeRemoveConfirmationMessage: __(
|
||||
'Remove this variation option?',
|
||||
attributeRemoveConfirmationModalMessage: __(
|
||||
'If you continue, some variations of this product will be deleted and customers will no longer be able to purchase them.',
|
||||
'woocommerce'
|
||||
),
|
||||
} }
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
} from './utils';
|
||||
import { AttributeListItem } from '../attribute-list-item';
|
||||
import { NewAttributeModal } from './new-attribute-modal';
|
||||
import { RemoveConfirmationModal } from './remove-confirmation-modal';
|
||||
|
||||
type AttributeControlProps = {
|
||||
value: EnhancedProductAttribute[];
|
||||
|
@ -45,6 +46,7 @@ type AttributeControlProps = {
|
|||
onEditModalClose?: ( attribute?: ProductAttribute ) => void;
|
||||
onEditModalOpen?: ( attribute?: ProductAttribute ) => void;
|
||||
createNewAttributesAsGlobal?: boolean;
|
||||
useRemoveConfirmationModal?: boolean;
|
||||
uiStrings?: {
|
||||
emptyStateSubtitle?: string;
|
||||
newAttributeListItemLabel?: string;
|
||||
|
@ -54,6 +56,7 @@ type AttributeControlProps = {
|
|||
customAttributeHelperMessage?: string;
|
||||
attributeRemoveLabel?: string;
|
||||
attributeRemoveConfirmationMessage?: string;
|
||||
attributeRemoveConfirmationModalMessage?: string;
|
||||
globalAttributeHelperMessage: string;
|
||||
};
|
||||
};
|
||||
|
@ -73,6 +76,7 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
onRemoveCancel = () => {},
|
||||
uiStrings,
|
||||
createNewAttributesAsGlobal = false,
|
||||
useRemoveConfirmationModal = false,
|
||||
} ) => {
|
||||
uiStrings = {
|
||||
newAttributeListItemLabel: __( 'Add new', 'woocommerce' ),
|
||||
|
@ -91,6 +95,8 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
...uiStrings,
|
||||
};
|
||||
const [ isNewModalVisible, setIsNewModalVisible ] = useState( false );
|
||||
const [ removingAttribute, setRemovingAttribute ] =
|
||||
useState< null | ProductAttribute >();
|
||||
const [ currentAttributeId, setCurrentAttributeId ] = useState<
|
||||
null | string
|
||||
>( null );
|
||||
|
@ -111,8 +117,6 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
};
|
||||
|
||||
const handleRemove = ( attribute: ProductAttribute ) => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if ( window.confirm( uiStrings?.attributeRemoveConfirmationMessage ) ) {
|
||||
handleChange(
|
||||
value.filter(
|
||||
( attr ) =>
|
||||
|
@ -120,6 +124,17 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
)
|
||||
);
|
||||
onRemove( attribute );
|
||||
setRemovingAttribute( null );
|
||||
};
|
||||
|
||||
const showRemoveConfirmation = ( attribute: ProductAttribute ) => {
|
||||
if ( useRemoveConfirmationModal ) {
|
||||
setRemovingAttribute( attribute );
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-alert
|
||||
if ( window.confirm( uiStrings?.attributeRemoveConfirmationMessage ) ) {
|
||||
handleRemove( attribute );
|
||||
return;
|
||||
}
|
||||
onRemoveCancel( attribute );
|
||||
|
@ -231,7 +246,9 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
removeLabel={ uiStrings?.attributeRemoveLabel }
|
||||
key={ getAttributeId( attr ) }
|
||||
onEditClick={ () => openEditModal( attr ) }
|
||||
onRemoveClick={ () => handleRemove( attr ) }
|
||||
onRemoveClick={ () =>
|
||||
showRemoveConfirmation( attr )
|
||||
}
|
||||
/>
|
||||
) ) }
|
||||
</Sortable>
|
||||
|
@ -288,6 +305,22 @@ export const AttributeControl: React.FC< AttributeControlProps > = ( {
|
|||
attribute={ currentAttribute }
|
||||
/>
|
||||
) }
|
||||
{ removingAttribute && (
|
||||
<RemoveConfirmationModal
|
||||
title={ sprintf(
|
||||
__( 'Delete %(attributeName)s', 'woocommerce' ),
|
||||
{ attributeName: removingAttribute.name }
|
||||
) }
|
||||
description={
|
||||
uiStrings.attributeRemoveConfirmationModalMessage
|
||||
}
|
||||
onRemove={ () => handleRemove( removingAttribute ) }
|
||||
onCancel={ () => {
|
||||
onRemoveCancel( removingAttribute );
|
||||
setRemovingAttribute( null );
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.woocommerce-remove-attribute-modal {
|
||||
max-width: 650px;
|
||||
|
||||
&__buttons {
|
||||
margin-top: $gap-larger;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
|
||||
type RemoveConfirmationModalProps = {
|
||||
title?: string;
|
||||
description?: string | React.ReactElement;
|
||||
onCancel: () => void;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
export const RemoveConfirmationModal: React.FC<
|
||||
RemoveConfirmationModalProps
|
||||
> = ( {
|
||||
title = __( 'Add attributes', 'woocommerce' ),
|
||||
description = '',
|
||||
onCancel,
|
||||
onRemove,
|
||||
} ) => {
|
||||
return (
|
||||
<Modal
|
||||
title={ title }
|
||||
onRequestClose={ (
|
||||
event:
|
||||
| React.KeyboardEvent< Element >
|
||||
| React.MouseEvent< Element >
|
||||
| React.FocusEvent< Element >
|
||||
) => {
|
||||
if ( ! event.isPropagationStopped() ) {
|
||||
onCancel();
|
||||
}
|
||||
} }
|
||||
className="woocommerce-remove-attribute-modal"
|
||||
>
|
||||
{ description && <p>{ description }</p> }
|
||||
|
||||
<div className="woocommerce-remove-attribute-modal__buttons">
|
||||
<Button
|
||||
isDestructive
|
||||
variant="primary"
|
||||
label={ __( 'Delete', 'woocommerce' ) }
|
||||
onClick={ onRemove }
|
||||
>
|
||||
{ __( 'Delete', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
variant="tertiary"
|
||||
label={ __( 'Cancel', 'woocommerce' ) }
|
||||
onClick={ onCancel }
|
||||
>
|
||||
{ __( 'Cancel', 'woocommerce' ) }
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,17 +1,31 @@
|
|||
.woocommerce-product-variations {
|
||||
ol {
|
||||
@media ( min-width: #{ ($break-medium) } ) {
|
||||
min-height: 420px;
|
||||
}
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: $gap;
|
||||
|
||||
.components-spinner {
|
||||
width: $gap-largest;
|
||||
height: $gap-largest;
|
||||
}
|
||||
}
|
||||
|
||||
&__status-dot {
|
||||
margin-right: $gap-smaller;
|
||||
&.green {
|
||||
|
@ -83,20 +97,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
min-height: 476px;
|
||||
|
||||
.components-spinner {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: $gap;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { __ } from '@wordpress/i18n';
|
|||
import { Button, Spinner, Tooltip } from '@wordpress/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
ProductAttribute,
|
||||
ProductVariation,
|
||||
} from '@woocommerce/data';
|
||||
import {
|
||||
|
@ -24,7 +23,7 @@ import { CurrencyContext } from '@woocommerce/currency';
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore No types for this exist yet.
|
||||
// eslint-disable-next-line @woocommerce/dependency-group
|
||||
import { useEntityId, useEntityProp } from '@wordpress/core-data';
|
||||
import { useEntityId } from '@wordpress/core-data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -47,25 +46,18 @@ export function VariationsTable() {
|
|||
const [ isUpdating, setIsUpdating ] = useState< Record< string, boolean > >(
|
||||
{}
|
||||
);
|
||||
const [ entityAttributes ] = useEntityProp< ProductAttribute[] >(
|
||||
'postType',
|
||||
'product',
|
||||
'attributes'
|
||||
);
|
||||
const variableAttributeTags = entityAttributes
|
||||
.filter( ( attr ) => attr.variation )
|
||||
.map( ( attr ) => attr.options )
|
||||
.flat();
|
||||
|
||||
const productId = useEntityId( 'postType', 'product' );
|
||||
const context = useContext( CurrencyContext );
|
||||
const { formatAmount } = context;
|
||||
const { isLoading, variations, totalCount } = useSelect(
|
||||
const { isLoading, variations, totalCount, isGeneratingVariations } =
|
||||
useSelect(
|
||||
( select ) => {
|
||||
const {
|
||||
getProductVariations,
|
||||
hasFinishedResolution,
|
||||
getProductVariationsTotalCount,
|
||||
isGeneratingVariations: getIsGeneratingVariations,
|
||||
} = select( EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME );
|
||||
const requestParams = {
|
||||
product_id: productId,
|
||||
|
@ -75,13 +67,21 @@ export function VariationsTable() {
|
|||
orderby: 'menu_order',
|
||||
};
|
||||
return {
|
||||
isLoading: ! hasFinishedResolution( 'getProductVariations', [
|
||||
requestParams,
|
||||
] ),
|
||||
isLoading: ! hasFinishedResolution(
|
||||
'getProductVariations',
|
||||
[ requestParams ]
|
||||
),
|
||||
isGeneratingVariations: getIsGeneratingVariations( {
|
||||
product_id: productId,
|
||||
} ),
|
||||
variations:
|
||||
getProductVariations< ProductVariation[] >( requestParams ),
|
||||
getProductVariations< ProductVariation[] >(
|
||||
requestParams
|
||||
),
|
||||
totalCount:
|
||||
getProductVariationsTotalCount< number >( requestParams ),
|
||||
getProductVariationsTotalCount< number >(
|
||||
requestParams
|
||||
),
|
||||
};
|
||||
},
|
||||
[ currentPage, perPage, productId ]
|
||||
|
@ -91,10 +91,15 @@ export function VariationsTable() {
|
|||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME
|
||||
);
|
||||
|
||||
if ( ! variations || isLoading ) {
|
||||
if ( ! variations && isLoading ) {
|
||||
return (
|
||||
<div className="woocommerce-product-variations is-loading">
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
{ isGeneratingVariations && (
|
||||
<span>
|
||||
{ __( 'Generating variations…', 'woocommerce' ) }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -121,17 +126,25 @@ export function VariationsTable() {
|
|||
|
||||
return (
|
||||
<div className="woocommerce-product-variations">
|
||||
{ isLoading ||
|
||||
( isGeneratingVariations && (
|
||||
<div className="woocommerce-product-variations__loading">
|
||||
<Spinner />
|
||||
{ isGeneratingVariations && (
|
||||
<span>
|
||||
{ __(
|
||||
'Generating variations…',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
) }
|
||||
</div>
|
||||
) ) }
|
||||
<Sortable>
|
||||
{ variations.map( ( variation ) => (
|
||||
<ListItem key={ `${ variation.id }` }>
|
||||
<div className="woocommerce-product-variations__attributes">
|
||||
{ variation.attributes
|
||||
.filter( ( attribute ) =>
|
||||
variableAttributeTags.includes(
|
||||
attribute.option
|
||||
)
|
||||
)
|
||||
.map( ( attribute ) => {
|
||||
{ variation.attributes.map( ( attribute ) => {
|
||||
const tag = (
|
||||
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||
/* @ts-ignore Additional props are not required. */
|
||||
|
@ -139,15 +152,10 @@ export function VariationsTable() {
|
|||
id={ attribute.id }
|
||||
className="woocommerce-product-variations__attribute"
|
||||
key={ attribute.id }
|
||||
label={ truncate(
|
||||
attribute.option,
|
||||
{
|
||||
label={ truncate( attribute.option, {
|
||||
length: PRODUCT_VARIATION_TITLE_LIMIT,
|
||||
}
|
||||
) }
|
||||
screenReaderLabel={
|
||||
attribute.option
|
||||
}
|
||||
} ) }
|
||||
screenReaderLabel={ attribute.option }
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
import { useDispatch } from '@wordpress/data';
|
||||
import { useEntityProp } from '@wordpress/core-data';
|
||||
import { useCallback, useState } from '@wordpress/element';
|
||||
import {
|
||||
Product,
|
||||
EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME,
|
||||
} from '@woocommerce/data';
|
||||
import { EXPERIMENTAL_PRODUCT_VARIATIONS_STORE_NAME } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -20,7 +17,6 @@ export function useProductVariationsHelper() {
|
|||
'product',
|
||||
'id'
|
||||
);
|
||||
const { saveEntityRecord } = useDispatch( 'core' );
|
||||
const {
|
||||
generateProductVariations: _generateProductVariations,
|
||||
invalidateResolutionForStoreSelector,
|
||||
|
@ -32,36 +28,32 @@ export function useProductVariationsHelper() {
|
|||
async ( attributes: EnhancedProductAttribute[] ) => {
|
||||
setIsGenerating( true );
|
||||
|
||||
const updateProductAttributes = async () => {
|
||||
const hasVariableAttribute = attributes.some(
|
||||
( attr ) => attr.variation
|
||||
);
|
||||
await saveEntityRecord< Promise< Product > >(
|
||||
'postType',
|
||||
'product',
|
||||
|
||||
return _generateProductVariations< {
|
||||
count: number;
|
||||
deleted_count: number;
|
||||
} >(
|
||||
{
|
||||
product_id: productId,
|
||||
},
|
||||
{
|
||||
id: productId,
|
||||
type: hasVariableAttribute ? 'variable' : 'simple',
|
||||
attributes,
|
||||
},
|
||||
{
|
||||
delete: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return updateProductAttributes()
|
||||
)
|
||||
.then( () => {
|
||||
return _generateProductVariations< { count: number } >( {
|
||||
product_id: productId,
|
||||
} );
|
||||
} )
|
||||
.then( ( data ) => {
|
||||
if ( data.count > 0 ) {
|
||||
invalidateResolutionForStoreSelector(
|
||||
'getProductVariations'
|
||||
);
|
||||
return invalidateResolutionForStoreSelector(
|
||||
'getProductVariationsTotalCount'
|
||||
);
|
||||
}
|
||||
} )
|
||||
.finally( () => {
|
||||
setIsGenerating( false );
|
||||
|
|
|
@ -1,38 +1,39 @@
|
|||
/* Editor */
|
||||
|
||||
@import 'components/editor/style.scss';
|
||||
@import 'components/block-editor/style.scss';
|
||||
@import "components/editor/style.scss";
|
||||
@import "components/block-editor/style.scss";
|
||||
|
||||
/* Structural */
|
||||
|
||||
@import 'components/tabs/style.scss';
|
||||
@import 'components/product-section-layout/style.scss';
|
||||
@import 'components/header/style.scss';
|
||||
@import 'components/footer/style.scss';
|
||||
@import 'components/add-new-shipping-class-modal/style.scss';
|
||||
@import "components/tabs/style.scss";
|
||||
@import "components/product-section-layout/style.scss";
|
||||
@import "components/header/style.scss";
|
||||
@import "components/footer/style.scss";
|
||||
@import "components/add-new-shipping-class-modal/style.scss";
|
||||
|
||||
/* Components */
|
||||
|
||||
@import 'components/content-preview/style.scss';
|
||||
@import 'components/radio-field/style.scss';
|
||||
@import 'components/iframe-editor/style.scss';
|
||||
@import 'components/details-categories-field/style.scss';
|
||||
@import 'components/details-categories-field/create-category-modal.scss';
|
||||
@import 'components/modal-editor/style.scss';
|
||||
@import 'components/feedback-bar/style.scss';
|
||||
@import 'components/product-mvp-feedback-modal/style.scss';
|
||||
@import 'components/edit-product-link-modal/style.scss';
|
||||
@import 'components/edit-product-link-modal/style.scss';
|
||||
@import 'components/details-categories-field/style.scss';
|
||||
@import 'components/details-categories-field/create-category-modal.scss';
|
||||
@import 'components/attribute-control/attribute-field.scss';
|
||||
@import 'components/attribute-control/edit-attribute-modal.scss';
|
||||
@import 'components/attribute-control/new-attribute-modal.scss';
|
||||
@import 'components/attribute-input-field/attribute-input-field.scss';
|
||||
@import 'components/attribute-list-item/attribute-list-item.scss';
|
||||
@import 'components/attribute-term-input-field/attribute-term-input-field.scss';
|
||||
@import 'components/variations-table/styles.scss';
|
||||
@import "components/content-preview/style.scss";
|
||||
@import "components/radio-field/style.scss";
|
||||
@import "components/iframe-editor/style.scss";
|
||||
@import "components/details-categories-field/style.scss";
|
||||
@import "components/details-categories-field/create-category-modal.scss";
|
||||
@import "components/modal-editor/style.scss";
|
||||
@import "components/feedback-bar/style.scss";
|
||||
@import "components/product-mvp-feedback-modal/style.scss";
|
||||
@import "components/edit-product-link-modal/style.scss";
|
||||
@import "components/edit-product-link-modal/style.scss";
|
||||
@import "components/details-categories-field/style.scss";
|
||||
@import "components/details-categories-field/create-category-modal.scss";
|
||||
@import "components/attribute-control/attribute-field.scss";
|
||||
@import "components/attribute-control/edit-attribute-modal.scss";
|
||||
@import "components/attribute-control/new-attribute-modal.scss";
|
||||
@import "components/attribute-control/remove-confirmation-modal.scss";
|
||||
@import "components/attribute-input-field/attribute-input-field.scss";
|
||||
@import "components/attribute-list-item/attribute-list-item.scss";
|
||||
@import "components/attribute-term-input-field/attribute-term-input-field.scss";
|
||||
@import "components/variations-table/styles.scss";
|
||||
|
||||
/* Field Blocks */
|
||||
|
||||
@import 'blocks/style.scss'
|
||||
@import "blocks/style.scss";
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add delete option to generate variations API, to auto delete unmatched variations.
|
|
@ -44,6 +44,10 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
|
|||
'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ),
|
||||
'type' => 'integer',
|
||||
),
|
||||
'delete' => array(
|
||||
'description' => __( 'Deletes unused variations.', 'woocommerce' ),
|
||||
'type' => 'boolean',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
|
@ -926,6 +930,40 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
|
|||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all unmatched variations (aka duplicates).
|
||||
*
|
||||
* @param WC_Product $product Variable product.
|
||||
* @return int Number of deleted variations.
|
||||
*/
|
||||
private function delete_unmatched_product_variations( $product ) {
|
||||
$deleted_count = 0;
|
||||
|
||||
if ( ! $product ) {
|
||||
return $deleted_count;
|
||||
}
|
||||
|
||||
$attributes = wc_list_pluck( array_filter( $product->get_attributes(), 'wc_attributes_array_filter_variation' ), 'get_slugs' );
|
||||
|
||||
// Get existing variations so we don't create duplicates.
|
||||
$existing_variations = array_map( 'wc_get_product', $product->get_children() );
|
||||
|
||||
$possible_attribute_combinations = array_reverse( wc_array_cartesian( $attributes ) );
|
||||
|
||||
foreach ( $existing_variations as $existing_variation ) {
|
||||
$matching_attribute_key = array_search( $existing_variation->get_attributes(), $possible_attribute_combinations ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
if ( $matching_attribute_key !== false ) {
|
||||
// We only want one possible variation for each possible attribute combination.
|
||||
unset( $possible_attribute_combinations[ $matching_attribute_key ] );
|
||||
continue;
|
||||
}
|
||||
$existing_variation->delete( true );
|
||||
$deleted_count ++;
|
||||
}
|
||||
|
||||
return $deleted_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all variations for a given product.
|
||||
*
|
||||
|
@ -947,6 +985,11 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
|
|||
$data_store = $product->get_data_store();
|
||||
$response['count'] = $data_store->create_all_product_variations( $product, Constants::get_constant( 'WC_MAX_LINKED_VARIATIONS' ) );
|
||||
|
||||
if ( isset( $request['delete'] ) && $request['delete'] ) {
|
||||
$deleted_count = $this->delete_unmatched_product_variations( $product );
|
||||
$response['deleted_count'] = $deleted_count;
|
||||
}
|
||||
|
||||
$data_store->sort_all_product_variations( $product->get_id() );
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
|
|
|
@ -497,4 +497,177 @@ class Product_Variations_API extends WC_REST_Unit_Test_Case {
|
|||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 'parent', $variation['manage_stock'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a variation stock.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*/
|
||||
public function test_generate_new_variations() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
|
||||
$color_attribute_data = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_attribute( 'color', array( 'red', 'blue', 'yellow' ) );
|
||||
$color_attribute = new WC_Product_Attribute();
|
||||
$color_attribute->set_id( $color_attribute_data['attribute_id'] );
|
||||
$color_attribute->set_name( $color_attribute_data['attribute_taxonomy'] );
|
||||
$color_attribute->set_options( $color_attribute_data['term_ids'] );
|
||||
$color_attribute->set_position( 1 );
|
||||
$color_attribute->set_visible( true );
|
||||
$color_attribute->set_variation( true );
|
||||
$attributes = $product->get_attributes();
|
||||
$attributes[] = $color_attribute;
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
// Set stock to true.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
|
||||
$response = $this->server->dispatch( $request );
|
||||
$variation = $response->get_data();
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
$children = $product->get_children();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 6, $variation['count'] );
|
||||
// Generated 2 new ones.
|
||||
$this->assertEquals( 8, count( $children ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a variation stock.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public function test_generate_new_variations_with_delete_set_to_true() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
|
||||
$color_attribute_data = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_attribute( 'color', array( 'red', 'blue', 'yellow' ) );
|
||||
$color_attribute = new WC_Product_Attribute();
|
||||
$color_attribute->set_id( $color_attribute_data['attribute_id'] );
|
||||
$color_attribute->set_name( $color_attribute_data['attribute_taxonomy'] );
|
||||
$color_attribute->set_options( $color_attribute_data['term_ids'] );
|
||||
$color_attribute->set_position( 1 );
|
||||
$color_attribute->set_visible( true );
|
||||
$color_attribute->set_variation( true );
|
||||
$attributes = $product->get_attributes();
|
||||
$attributes[] = $color_attribute;
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
// Set stock to true.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
$request->set_body_params( array( 'delete' => true ) );
|
||||
|
||||
$response = $this->server->dispatch( $request );
|
||||
$variation = $response->get_data();
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
$children = $product->get_children();
|
||||
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 6, $variation['count'] );
|
||||
$this->assertEquals( 2, $variation['deleted_count'] );
|
||||
// Generated 2 new ones.
|
||||
$this->assertEquals( 6, count( $children ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a variation stock.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public function test_delete_unmatched_variations_when_removing_term() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
|
||||
$color_attribute = new WC_Product_Attribute();
|
||||
$color_attribute->set_name( 'color' );
|
||||
$color_attribute->set_visible( true );
|
||||
$color_attribute->set_variation( true );
|
||||
$color_attribute->set_options( array( 'red', 'green', 'blue' ) );
|
||||
$color_attribute->set_position( 1 );
|
||||
$attributes = $product->get_attributes();
|
||||
$attributes[] = $color_attribute;
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
// Set stock to true.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
$request->set_body_params( array( 'delete' => true ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
|
||||
$product_attributes = get_post_meta( $product->get_id() , '_product_attributes' );
|
||||
|
||||
// Removing blue term from product.
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
$attributes = $product->get_attributes();
|
||||
$color_attribute->set_options( array( 'red', 'green' ) );
|
||||
$attributes['color'] = $color_attribute;
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
$request->set_body_params( array( 'delete' => true ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
|
||||
$variation = $response->get_data();
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 0, $variation['count'] );
|
||||
$this->assertEquals( 2, $variation['deleted_count'] );
|
||||
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
// Removed two.
|
||||
$this->assertEquals( 4, count( $product->get_children() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating a variation stock.
|
||||
*
|
||||
* @since 3.5.0
|
||||
*/
|
||||
public function test_delete_unmatched_variations_when_removing_attribute() {
|
||||
wp_set_current_user( $this->user );
|
||||
|
||||
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
|
||||
$color_attribute = new WC_Product_Attribute();
|
||||
$color_attribute->set_name( 'color' );
|
||||
$color_attribute->set_visible( true );
|
||||
$color_attribute->set_variation( true );
|
||||
$color_attribute->set_options( array( 'red', 'yellow', 'green' ) );
|
||||
$color_attribute->set_position( 1 );
|
||||
$attributes = $product->get_attributes();
|
||||
$attributes[] = $color_attribute;
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
// Set stock to true.
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
$request->set_body_params( array( 'delete' => true ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
|
||||
$product_attributes = get_post_meta( $product->get_id() , '_product_attributes' );
|
||||
|
||||
// Removing color attribute from product.
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
$attributes = $product->get_attributes();
|
||||
unset( $attributes['color'] );
|
||||
$product->set_attributes( $attributes );
|
||||
$product->save();
|
||||
|
||||
$request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/generate' );
|
||||
$request->set_body_params( array( 'delete' => true ) );
|
||||
$response = $this->server->dispatch( $request );
|
||||
|
||||
$variation = $response->get_data();
|
||||
$this->assertEquals( 200, $response->get_status() );
|
||||
$this->assertEquals( 0, $variation['count'] );
|
||||
$this->assertEquals( 4, $variation['deleted_count'] );
|
||||
|
||||
$product = wc_get_product( $product->get_id() );
|
||||
// Removed four.
|
||||
$this->assertEquals( 2, count( $product->get_children() ) );
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue