Merge branch 'trunk' of github.com:woocommerce/woocommerce into add/wx-nightly-perf
This commit is contained in:
commit
8d4e4748a0
|
@ -0,0 +1,191 @@
|
|||
Like many WordPress plugins, WooCommerce provides a range of actions and filters through which developers can extend and modify the platform.
|
||||
|
||||
Often, when writing new code or revising existing code, there is a desire to add new hooks—but this should always be done with thoughtfulness and care. This document aims to provide high-level guidance on the matter.
|
||||
|
||||
Practices we generally allow, support and encourage include:
|
||||
|
||||
* [Using existing hooks (or other alternatives) in preference to adding new hooks](#prefer-existing-hooks-or-other-alternatives)
|
||||
* [Adding lifecycle hooks](#adding-lifecycle-hooks)
|
||||
* [Optional escape hooks](#escape-hooks)
|
||||
* [Modifying the inputs and outputs of global rendering functions](#modifying-function-input-and-output-global-rendering-functions)
|
||||
* [Preferring the passing of objects over IDs](#prefer-passing-objects-over-ids)
|
||||
|
||||
On the flip side, there are several practices we discourage:
|
||||
|
||||
* [Tying lifecycle hooks to methods of execution](#tying-lifecycle-hooks-to-methods-of-execution)
|
||||
* [Using filters as feature flags](#using-filters-as-feature-flags)
|
||||
* [Placing filter hooks inside templates and data stores](#placement-of-filter-hooks)
|
||||
* [Enumeration values within hook names](#enumeration-values-inside-hook-names)
|
||||
|
||||
Beyond those items, we generally otherwise adhere to WordPress coding standards. In regards to hooks, that specifically means following the:
|
||||
|
||||
* [Documentation standards for hooks](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/php/#4-hooks-actions-and-filters)
|
||||
* [Guidance on Dynamic hook names](https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/#interpolation-for-naming-dynamic-hooks)
|
||||
|
||||
Please note that we provide example code throughout this guide to help illustrate some of the principles. However, to keep things concise, we usually omit unnecessary detail, including doc blocks (in practice, though, hooks should always be accompanied by doc blocks!).
|
||||
|
||||
### Prefer existing hooks (or other alternatives)
|
||||
|
||||
Hooks come with a long-term obligation: the last thing we want is to add a new hook that developers come to depend on, only to strip it away again. However, this can lead to difficulties when the time comes to refactor a piece of code that contains hooks, sometimes delaying meaningful change or limiting how easily we can implement a change without compromising on backward compatibility commitments.
|
||||
|
||||
For those reasons, we always prefer that—wherever reasonable—an existing hook or alternative approach in preference to adding a new hook.
|
||||
|
||||
### Adding lifecycle hooks
|
||||
|
||||
Lifecycle hooks can be used to communicate that a lifecycle event is about to start, or that it has concluded. Examples of such events include:
|
||||
|
||||
* Main product loop
|
||||
* Dispatching emails
|
||||
* Rendering a template
|
||||
* Product or order status changes
|
||||
|
||||
In general, lifecycle hooks:
|
||||
|
||||
* Come in pairs (‘before’ and ‘after’)
|
||||
* Are always actions, never filters
|
||||
* The ‘before’ hook will generally always provide callbacks with the arguments array, if there is one
|
||||
* The ‘after’ hook will generally also provide callbacks with the function’s return value, if there is one
|
||||
|
||||
Note that lifecycle hooks primarily exist to let other systems observe, rather than to modify the result. Of course, this does not stop the function author from additionally providing a filter hook that serves this function.
|
||||
|
||||
For example, noting that it is the process of fetching the promotions which we view as the “lifecycle event”, and not the function itself:
|
||||
|
||||
```php
|
||||
function woocommerce_get_current_promotions( ...$args ) {
|
||||
/* Any initial prep, then first lifecycle hook... */
|
||||
do_action( 'woocommerce_before_get_current_promotions', $args );
|
||||
/* ...Do actual work, then final lifecycle hook... */
|
||||
do_action( 'woocommerce_after_get_current_promotions', $result, $args );
|
||||
/* ...Return the result, optionally via a filter... */
|
||||
return apply_filters( 'woocommerce_get_current_promotions', $result, $args );
|
||||
}
|
||||
```
|
||||
|
||||
### Escape hooks
|
||||
|
||||
In some cases, it may be appropriate to support short-circuiting of functions or methods. This is what we call an escape hook, and can be useful as a means of overriding code when a better way of doing so is not available.
|
||||
|
||||
* Escape hooks are always filters
|
||||
* They should always supply null as the initial filterable value
|
||||
* If the value is changed to a non-null value, then the function should exit early by returning that new value
|
||||
|
||||
For type safety, care should be taken to ensure that, if a function is short-circuited, the return type matches the function signature and/or return type stated in the function doc block.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
function get_product_metrics( $args ): array {
|
||||
$pre = apply_filters( 'pre_woocommerce_get_product_metrics', null, $args );
|
||||
|
||||
if ( $pre !== null ) {
|
||||
return (array) $pre;
|
||||
}
|
||||
|
||||
/* ...Default logic... */
|
||||
return $metrics;
|
||||
}
|
||||
```
|
||||
|
||||
### Modifying function input and output (global rendering functions)
|
||||
|
||||
In the case of global rendering or formatting functions (so-called “template tags”), where it is not readily possible to implement better alternatives, it is permissible to add filters for both the function arguments and the function’s return value.
|
||||
|
||||
This should be done sparingly, and only where necessary. Remember that while providing opportunities for other components to perform extensive customization, it can potentially derail other components which expect unmodified output.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
function woocommerce_format_sale_price( ...$args ): string {
|
||||
/* Prep to fill in any missing $args values... */
|
||||
$args = (array) apply_filters( 'woocommerce_format_sale_price_args', $args );
|
||||
/* ...Actual work to determine the $price string... */
|
||||
return (string) apply_filters( 'woocommerce_format_sale_price', $price, $args );
|
||||
}
|
||||
```
|
||||
|
||||
### Prefer passing objects over IDs
|
||||
|
||||
Some actions or filters provide an object ID (such as a product ID) as their primary value, while others will provide the actual object itself (such as a product object). For consistency, it is preferred that objects be passed.
|
||||
|
||||
Example:
|
||||
|
||||
```php
|
||||
function get_featured_product_for_current_customer( ) {
|
||||
/* ...Logic to find the featured product for this customer… */
|
||||
|
||||
return apply_filters(
|
||||
'woocommerce_featured_product_for_current_customer',
|
||||
$product, /* WC_Product */
|
||||
$customer
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Tying lifecycle hooks to methods of execution
|
||||
|
||||
There can sometimes be multiple paths leading to the same action. For instance, an order can be updated via the REST API, through the admin environment, or on the front end. It may additionally happen via ajax, or via a regular request.
|
||||
|
||||
It is important however not to tie hooks for high-level processes to specific execution paths. For example, an action that fires when an order is created must not only be fired when this happens in the admin environment via an ajax request.
|
||||
|
||||
Instead, prefer a more generic hook that passes context about the method of execution to the callback.
|
||||
|
||||
Example of what we wish to avoid:
|
||||
|
||||
```php
|
||||
/**
|
||||
* Pretend this function is only called following an ajax request
|
||||
* (perhaps it is itself hooked in using a `wp_ajax_*` action).
|
||||
*/
|
||||
function on_ajax_order_creation() {
|
||||
/* Avoid this! */
|
||||
do_action( 'woocommerce_on_order_creation' );
|
||||
}
|
||||
```
|
||||
|
||||
### Using filters as feature flags
|
||||
|
||||
It is sometimes tempting to use a filter as a sort of feature flag, that enables or disables a piece of functionality. This should be avoided! Prefer using an option:
|
||||
|
||||
* Options persist in the database.
|
||||
* Options are already filterable (ideal for a temporary override).
|
||||
|
||||
Example of what we wish to avoid:
|
||||
|
||||
```php
|
||||
/* Avoid this */
|
||||
$super_products_enabled = (bool) apply_filters( 'woocommerce_super_products_are_enabled', true );
|
||||
|
||||
/* Prefer this */
|
||||
$super_products_enabled = get_option( 'woocommerce_super_products_are_enabled', 'no' ) === 'yes';
|
||||
```
|
||||
|
||||
### Placement of filter hooks
|
||||
|
||||
Filters should not be placed inside templates—only actions. If it is important that a value used within a template be filterable, then the relevant logic should be moved to whichever function or method decides to load the template—the result being passed in as a template variable.
|
||||
|
||||
It is also preferred that filter hooks not be placed inside data-store classes, as this can reduce the integrity of those components: since, by design, they are replaceable by custom implementations—the risk of accidentally breaking those custom stores is higher.
|
||||
|
||||
### Enumeration values inside hook names
|
||||
|
||||
Though there is a case for dynamic hook names (where part of the hook name is created using a variable), a good rule of thumb is to avoid this if the variable contains what might be considered an enumeration value.
|
||||
|
||||
This might for instance include a case where an error code forms part of the hook name.
|
||||
|
||||
Example (of what we wish to avoid):
|
||||
|
||||
```php
|
||||
if ( is_wp_error( $result ) ) {
|
||||
/* Avoid this */
|
||||
$error_code = $result->get_error_code();
|
||||
do_action( "woocommerce_foo_bar_{$error_code}_problem", $intermediate_result );
|
||||
|
||||
/* Prefer this */
|
||||
do_action( 'woocommerce_foo_bar_problem', $result );
|
||||
}
|
||||
```
|
||||
|
||||
The primary reason for avoiding this is that the more values there are in the enumeration set, the more filters developers have to include in their code.
|
||||
|
||||
### Summary
|
||||
|
||||
This document is a high-level guide to the inclusion and placement of hooks, not an exhaustive list. There will occasionally be exceptions, and there may be good rules and methodologies we are missing: if you have suggestions or ideas for improvement, please reach out!
|
|
@ -6,7 +6,7 @@ import { CoreProfilerStateMachineContext } from '.';
|
|||
export type ComponentMeta = {
|
||||
/** React component that is rendered when state matches the location this meta key is defined */
|
||||
component: ( arg0: ComponentProps ) => JSX.Element;
|
||||
/** number between 0 - 100 */
|
||||
/** Number between 0 - 100 */
|
||||
progress: number;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export type events = { type: 'FINISH_CUSTOMIZATION' };
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CustomizeStoreComponent } from '../types';
|
||||
|
||||
export type events = { type: 'THEME_SUGGESTED' };
|
||||
export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => {
|
||||
return (
|
||||
<>
|
||||
<h1>Design with AI</h1>
|
||||
<button onClick={ () => sendEvent( { type: 'THEME_SUGGESTED' } ) }>
|
||||
Back to intro
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,193 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createMachine } from 'xstate';
|
||||
import { useEffect, useMemo, useState } from '@wordpress/element';
|
||||
import { useMachine, useSelector } from '@xstate/react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useFullScreen } from '~/utils';
|
||||
import {
|
||||
Intro,
|
||||
events as introEvents,
|
||||
services as introServices,
|
||||
actions as introActions,
|
||||
} from './intro';
|
||||
import { DesignWithAi, events as designWithAiEvents } from './design-with-ai';
|
||||
import { events as assemblerHubEvents } from './assembler-hub';
|
||||
import { findComponentMeta } from '~/utils/xstate/find-component';
|
||||
import {
|
||||
CustomizeStoreComponentMeta,
|
||||
CustomizeStoreComponent,
|
||||
customizeStoreStateMachineContext,
|
||||
} from './types';
|
||||
import { ThemeCard } from './intro/theme-cards';
|
||||
|
||||
export type customizeStoreStateMachineEvents =
|
||||
| introEvents
|
||||
| designWithAiEvents
|
||||
| assemblerHubEvents;
|
||||
|
||||
export const customizeStoreStateMachineServices = {
|
||||
...introServices,
|
||||
};
|
||||
|
||||
export const customizeStoreStateMachineActions = {
|
||||
...introActions,
|
||||
};
|
||||
export const customizeStoreStateMachineDefinition = createMachine( {
|
||||
id: 'customizeStore',
|
||||
initial: 'intro',
|
||||
predictableActionArguments: true,
|
||||
preserveActionOrder: true,
|
||||
schema: {
|
||||
context: {} as customizeStoreStateMachineContext,
|
||||
events: {} as customizeStoreStateMachineEvents,
|
||||
services: {} as {
|
||||
fetchThemeCards: { data: ThemeCard[] };
|
||||
},
|
||||
},
|
||||
context: {
|
||||
intro: {
|
||||
themeCards: [] as ThemeCard[],
|
||||
activeTheme: '',
|
||||
},
|
||||
} as customizeStoreStateMachineContext,
|
||||
states: {
|
||||
intro: {
|
||||
id: 'intro',
|
||||
initial: 'preIntro',
|
||||
states: {
|
||||
preIntro: {
|
||||
invoke: {
|
||||
src: 'fetchThemeCards',
|
||||
onDone: {
|
||||
target: 'intro',
|
||||
actions: [ 'assignThemeCards' ],
|
||||
},
|
||||
},
|
||||
},
|
||||
intro: {
|
||||
meta: {
|
||||
component: Intro,
|
||||
},
|
||||
},
|
||||
},
|
||||
on: {
|
||||
DESIGN_WITH_AI: {
|
||||
target: 'designWithAi',
|
||||
},
|
||||
SELECTED_ACTIVE_THEME: {
|
||||
target: 'assemblerHub',
|
||||
},
|
||||
CLICKED_ON_BREADCRUMB: {
|
||||
target: 'backToHomescreen',
|
||||
},
|
||||
SELECTED_NEW_THEME: {
|
||||
target: '? Appearance Task ?',
|
||||
},
|
||||
SELECTED_BROWSE_ALL_THEMES: {
|
||||
target: '? Appearance Task ?',
|
||||
},
|
||||
},
|
||||
},
|
||||
designWithAi: {
|
||||
initial: 'preDesignWithAi',
|
||||
states: {
|
||||
preDesignWithAi: {
|
||||
always: {
|
||||
target: 'designWithAi',
|
||||
},
|
||||
},
|
||||
designWithAi: {
|
||||
meta: {
|
||||
component: DesignWithAi,
|
||||
},
|
||||
},
|
||||
},
|
||||
on: {
|
||||
THEME_SUGGESTED: {
|
||||
target: 'assemblerHub',
|
||||
},
|
||||
},
|
||||
},
|
||||
assemblerHub: {
|
||||
on: {
|
||||
FINISH_CUSTOMIZATION: {
|
||||
target: 'backToHomescreen',
|
||||
},
|
||||
},
|
||||
},
|
||||
backToHomescreen: {},
|
||||
'? Appearance Task ?': {},
|
||||
},
|
||||
} );
|
||||
|
||||
export const CustomizeStoreController = ( {
|
||||
actionOverrides,
|
||||
servicesOverrides,
|
||||
}: {
|
||||
actionOverrides: Partial< typeof customizeStoreStateMachineActions >;
|
||||
servicesOverrides: Partial< typeof customizeStoreStateMachineServices >;
|
||||
} ) => {
|
||||
useFullScreen( [ 'woocommerce-customize-store' ] );
|
||||
|
||||
const augmentedStateMachine = useMemo( () => {
|
||||
return customizeStoreStateMachineDefinition.withConfig( {
|
||||
services: {
|
||||
...customizeStoreStateMachineServices,
|
||||
...servicesOverrides,
|
||||
},
|
||||
actions: {
|
||||
...customizeStoreStateMachineActions,
|
||||
...actionOverrides,
|
||||
},
|
||||
guards: {},
|
||||
} );
|
||||
}, [ actionOverrides, servicesOverrides ] );
|
||||
|
||||
const [ state, send, service ] = useMachine( augmentedStateMachine, {
|
||||
devTools: process.env.NODE_ENV === 'development',
|
||||
} );
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- false positive due to function name match, this isn't from react std lib
|
||||
const currentNodeMeta = useSelector( service, ( currentState ) =>
|
||||
findComponentMeta< CustomizeStoreComponentMeta >(
|
||||
currentState?.meta ?? undefined
|
||||
)
|
||||
);
|
||||
|
||||
const [ CurrentComponent, setCurrentComponent ] =
|
||||
useState< CustomizeStoreComponent | null >( null );
|
||||
useEffect( () => {
|
||||
if ( currentNodeMeta?.component ) {
|
||||
setCurrentComponent( () => currentNodeMeta?.component );
|
||||
}
|
||||
}, [ CurrentComponent, currentNodeMeta?.component ] );
|
||||
|
||||
const currentNodeCssLabel =
|
||||
state.value instanceof Object
|
||||
? Object.keys( state.value )[ 0 ]
|
||||
: state.value;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={ `woocommerce-profile-wizard__container woocommerce-profile-wizard__step-${ currentNodeCssLabel }` }
|
||||
>
|
||||
{ CurrentComponent ? (
|
||||
<CurrentComponent
|
||||
sendEvent={ send }
|
||||
context={ state.context }
|
||||
/>
|
||||
) : (
|
||||
<div />
|
||||
) }
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomizeStoreController;
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { assign, DoneInvokeEvent } from 'xstate';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { customizeStoreStateMachineEvents } from '..';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { customizeStoreStateMachineContext } from '../types';
|
||||
import { ThemeCard } from './theme-cards';
|
||||
|
||||
export const assignThemeCards = assign<
|
||||
customizeStoreStateMachineContext,
|
||||
customizeStoreStateMachineEvents // this is actually the wrong type for the event but I still don't know how to type this properly
|
||||
>( {
|
||||
intro: ( context, event ) => {
|
||||
const themeCards = ( event as DoneInvokeEvent< ThemeCard[] > ).data; // type coercion workaround for now
|
||||
return { ...context.intro, themeCards };
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CustomizeStoreComponent } from '../types';
|
||||
|
||||
export type events =
|
||||
| { type: 'DESIGN_WITH_AI' }
|
||||
| { type: 'CLICKED_ON_BREADCRUMB' }
|
||||
| { type: 'SELECTED_BROWSE_ALL_THEMES' }
|
||||
| { type: 'SELECTED_ACTIVE_THEME' }
|
||||
| { type: 'SELECTED_NEW_THEME'; payload: { theme: string } };
|
||||
|
||||
export * as actions from './actions';
|
||||
export * as services from './services';
|
||||
|
||||
export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
|
||||
const {
|
||||
intro: { themeCards, activeTheme },
|
||||
} = context;
|
||||
return (
|
||||
<>
|
||||
<h1>Intro</h1>
|
||||
<div>Active theme: { activeTheme }</div>
|
||||
{ themeCards?.map( ( themeCard ) => (
|
||||
<button
|
||||
key={ themeCard.name }
|
||||
onClick={ () =>
|
||||
sendEvent( {
|
||||
type: 'SELECTED_NEW_THEME',
|
||||
payload: { theme: themeCard.name },
|
||||
} )
|
||||
}
|
||||
>
|
||||
{ themeCard.name }
|
||||
</button>
|
||||
) ) }
|
||||
<button onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }>
|
||||
Design with AI
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
// placeholder xstate async service that returns a set of theme cards
|
||||
|
||||
export const fetchThemeCards = async () => {
|
||||
return [
|
||||
{
|
||||
name: 'Twenty Twenty One',
|
||||
description: 'The default theme for WordPress.',
|
||||
},
|
||||
{
|
||||
name: 'Twenty Twenty',
|
||||
description: 'The previous default theme for WordPress.',
|
||||
},
|
||||
];
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
export type ThemeCard = {
|
||||
// placeholder props, possibly take reference from https://github.com/Automattic/wp-calypso/blob/1f1b79210c49ef0d051f8966e24122229a334e29/packages/design-picker/src/components/theme-card/index.tsx#L32
|
||||
name: string;
|
||||
description: string;
|
||||
image: string;
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Sender } from 'xstate';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { customizeStoreStateMachineEvents } from '.';
|
||||
import { ThemeCard } from './intro/theme-cards';
|
||||
|
||||
export type CustomizeStoreComponent = ( props: {
|
||||
sendEvent: Sender< customizeStoreStateMachineEvents >;
|
||||
context: customizeStoreStateMachineContext;
|
||||
} ) => React.ReactElement | null;
|
||||
|
||||
export type CustomizeStoreComponentMeta = {
|
||||
component: CustomizeStoreComponent;
|
||||
};
|
||||
|
||||
export type customizeStoreStateMachineContext = {
|
||||
themeConfiguration: Record< string, unknown >; // placeholder for theme configuration until we know what it looks like
|
||||
intro: {
|
||||
themeCards: ThemeCard[];
|
||||
activeTheme: string;
|
||||
};
|
||||
};
|
|
@ -74,6 +74,10 @@ const WCPaymentsWelcomePage = lazy( () =>
|
|||
)
|
||||
);
|
||||
|
||||
const CustomizeStore = lazy( () =>
|
||||
import( /* webpackChunkName: "customize-store" */ '../customize-store' )
|
||||
);
|
||||
|
||||
export const PAGES_FILTER = 'woocommerce_admin_pages_list';
|
||||
|
||||
export const getPages = () => {
|
||||
|
@ -290,6 +294,18 @@ export const getPages = () => {
|
|||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures[ 'customize-store' ] ) {
|
||||
pages.push( {
|
||||
container: CustomizeStore,
|
||||
path: '/customize-store',
|
||||
breadcrumbs: [
|
||||
...initialBreadcrumbs,
|
||||
__( 'Customize Your Store', 'woocommerce' ),
|
||||
],
|
||||
capability: 'manage_woocommerce',
|
||||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures.settings ) {
|
||||
pages.push( {
|
||||
container: SettingsGroup,
|
||||
|
|
|
@ -38,7 +38,7 @@ export const useCreateProductByType = () => {
|
|||
}
|
||||
|
||||
const assignment = await loadExperimentAssignment(
|
||||
'woocommerce_product_creation_experience_202306_v2'
|
||||
'woocommerce_product_creation_experience_202308_v3'
|
||||
);
|
||||
|
||||
if ( assignment.variationName === 'treatment' ) {
|
||||
|
|
|
@ -4,17 +4,30 @@
|
|||
* @template T - The type of the component meta object
|
||||
*/
|
||||
export function findComponentMeta< T >(
|
||||
obj: Record< string, unknown >
|
||||
obj: Record< string, unknown >,
|
||||
visited = new Set< Record< string, unknown > >()
|
||||
): T | undefined {
|
||||
if ( visited.has( obj ) ) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
visited.add( obj );
|
||||
|
||||
for ( const key in obj ) {
|
||||
if ( key === 'component' ) {
|
||||
return obj as T;
|
||||
} else if ( typeof obj[ key ] === 'object' && obj[ key ] !== null ) {
|
||||
const found = findComponentMeta< T >(
|
||||
obj[ key ] as Record< string, unknown >
|
||||
);
|
||||
if ( found !== undefined ) {
|
||||
return found;
|
||||
if ( obj.hasOwnProperty( key ) ) {
|
||||
if ( key === 'component' ) {
|
||||
return obj as T;
|
||||
} else if (
|
||||
typeof obj[ key ] === 'object' &&
|
||||
obj[ key ] !== null
|
||||
) {
|
||||
const found = findComponentMeta< T >(
|
||||
obj[ key ] as Record< string, unknown >,
|
||||
visited
|
||||
);
|
||||
if ( found !== undefined ) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,4 +61,58 @@ describe( 'findComponentMeta', () => {
|
|||
progress: 100,
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should return undefined if there are circular references and no component', () => {
|
||||
const objA: Record< string, unknown > = {};
|
||||
const objB: Record< string, unknown > = { other: objA };
|
||||
objA.self = objA; // Creating a cyclic reference within objA
|
||||
objA.another = objB; // Adding another object to traverse
|
||||
|
||||
const result = findComponentMeta( objA );
|
||||
expect( result ).toBeUndefined();
|
||||
} );
|
||||
|
||||
it( 'should work correctly with mixed types', () => {
|
||||
const mixedObject = {
|
||||
number: 42,
|
||||
string: 'test',
|
||||
array: [ 1, 2, 3 ],
|
||||
nested: { component: 'found' },
|
||||
};
|
||||
|
||||
const result = findComponentMeta( mixedObject );
|
||||
expect( result ).toEqual( { component: 'found' } );
|
||||
} );
|
||||
|
||||
it( 'should work correctly with null and undefined values', () => {
|
||||
const objectWithNullAndUndefined = {
|
||||
key1: null,
|
||||
key2: undefined,
|
||||
nested: { component: 'found' },
|
||||
};
|
||||
|
||||
const result = findComponentMeta( objectWithNullAndUndefined );
|
||||
expect( result ).toEqual( { component: 'found' } );
|
||||
} );
|
||||
|
||||
it( 'should return the first component meta even if there are deeper ones', () => {
|
||||
const sparseComponentObject = {
|
||||
level1: {
|
||||
component: 'found',
|
||||
level2: {
|
||||
level3: { component: 'not this one' },
|
||||
otherLevel3: { component: 'not this one either' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = findComponentMeta( sparseComponentObject );
|
||||
expect( result ).toEqual( {
|
||||
component: 'found',
|
||||
level2: {
|
||||
level3: { component: 'not this one' },
|
||||
otherLevel3: { component: 'not this one either' },
|
||||
},
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Added xstate scaffolding for customize your store feature
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: fix
|
||||
|
||||
Ensure refund meta data is saved correctly when HPOS is enabled.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: This only updates the experiment name of the product block editor, this does not need to be listed in the changelog.
|
||||
|
||||
|
|
@ -2358,10 +2358,10 @@ FROM $order_meta_table
|
|||
$order->set_date_modified( current_time( 'mysql' ) );
|
||||
}
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
|
||||
$this->persist_order_to_db( $order, $force_all_fields );
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
|
||||
$order->save_meta_data();
|
||||
$order->apply_changes();
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use WC_Meta_Data;
|
||||
|
||||
/**
|
||||
* Class OrdersTableRefundDataStore.
|
||||
*/
|
||||
|
@ -159,8 +161,17 @@ class OrdersTableRefundDataStore extends OrdersTableDataStore {
|
|||
|
||||
$props_to_update = $this->get_props_to_update( $refund, $meta_key_to_props );
|
||||
foreach ( $props_to_update as $meta_key => $prop ) {
|
||||
$value = $refund->{"get_$prop"}( 'edit' );
|
||||
$refund->update_meta_data( $meta_key, $value );
|
||||
$meta_object = new WC_Meta_Data();
|
||||
$meta_object->key = $meta_key;
|
||||
$meta_object->value = $refund->{"get_$prop"}( 'edit' );
|
||||
$existing_meta = $this->data_store_meta->get_metadata_by_key( $refund, $meta_key );
|
||||
if ( $existing_meta ) {
|
||||
$existing_meta = $existing_meta[0];
|
||||
$meta_object->id = $existing_meta->id;
|
||||
$this->update_meta( $refund, $meta_object );
|
||||
} else {
|
||||
$this->add_meta( $refund, $meta_object );
|
||||
}
|
||||
$updated_props[] = $prop;
|
||||
}
|
||||
|
||||
|
|
|
@ -102,10 +102,15 @@ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case {
|
|||
}
|
||||
|
||||
/**
|
||||
* @testDox Test that refund props are set as expected.
|
||||
* @testDox Test that refund props are set as expected with HPOS enabled.
|
||||
*/
|
||||
public function test_refund_data_is_set() {
|
||||
$order = OrderHelper::create_order();
|
||||
$this->toggle_cot_feature_and_usage( true );
|
||||
|
||||
$order = OrderHelper::create_order();
|
||||
$user = $this->factory()->user->create_and_get( array( 'role' => 'administrator' ) );
|
||||
wp_set_current_user( $user->ID );
|
||||
|
||||
$refund = wc_create_refund(
|
||||
array(
|
||||
'order_id' => $order->get_id(),
|
||||
|
@ -119,6 +124,7 @@ class OrdersTableRefundDataStoreTests extends WC_Unit_Test_Case {
|
|||
$this->assertEquals( $refund->get_id(), $refreshed_refund->get_id() );
|
||||
$this->assertEquals( 10, $refreshed_refund->get_data()['amount'] );
|
||||
$this->assertEquals( 'Test', $refreshed_refund->get_data()['reason'] );
|
||||
$this->assertEquals( $user->ID, $refreshed_refund->get_data()['refunded_by'] );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue