Add generic error boundary component (#48363)
* Fix storybook * Add experimental error boundary component * Add error boundary component * Fix empty content button * Add changelog * Fix storybook * Reset state after actioned * Fix story * Address feedback
This commit is contained in:
parent
4112c97d13
commit
54321a5a60
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Add error boundary component and fix empty content button
|
|
@ -45,12 +45,13 @@ class EmptyContent extends Component {
|
||||||
: this.props.actionCallback;
|
: this.props.actionCallback;
|
||||||
|
|
||||||
const isPrimary = type === 'secondary' ? false : true;
|
const isPrimary = type === 'secondary' ? false : true;
|
||||||
|
const buttonVariant = isPrimary ? 'primary' : 'secondary';
|
||||||
|
|
||||||
if ( actionURL && actionCallback ) {
|
if ( actionURL && actionCallback ) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-empty-content__action"
|
className="woocommerce-empty-content__action"
|
||||||
isPrimary={ isPrimary }
|
variant={ buttonVariant }
|
||||||
onClick={ actionCallback }
|
onClick={ actionCallback }
|
||||||
href={ actionURL }
|
href={ actionURL }
|
||||||
>
|
>
|
||||||
|
@ -61,7 +62,7 @@ class EmptyContent extends Component {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-empty-content__action"
|
className="woocommerce-empty-content__action"
|
||||||
isPrimary={ isPrimary }
|
variant={ buttonVariant }
|
||||||
href={ actionURL }
|
href={ actionURL }
|
||||||
>
|
>
|
||||||
{ actionLabel }
|
{ actionLabel }
|
||||||
|
@ -71,7 +72,7 @@ class EmptyContent extends Component {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="woocommerce-empty-content__action"
|
className="woocommerce-empty-content__action"
|
||||||
isPrimary={ isPrimary }
|
variant={ buttonVariant }
|
||||||
onClick={ actionCallback }
|
onClick={ actionCallback }
|
||||||
>
|
>
|
||||||
{ actionLabel }
|
{ actionLabel }
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const alertIcon =
|
||||||
|
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAyMCAyMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTguNTc0NjUgMy4yMTYzNUwxLjUxNjMyIDE0Ljk5OTdDMS4zNzA3OSAxNS4yNTE3IDEuMjkzNzkgMTUuNTM3NCAxLjI5Mjk4IDE1LjgyODRDMS4yOTIxNiAxNi4xMTk1IDEuMzY3NTYgMTYuNDA1NiAxLjUxMTY3IDE2LjY1ODVDMS42NTU3OSAxNi45MTEzIDEuODYzNTkgMTcuMTIyIDIuMTE0NDEgMTcuMjY5NkMyLjM2NTIzIDE3LjQxNzEgMi42NTAzMiAxNy40OTY1IDIuOTQxMzIgMTcuNDk5N0gxNy4wNThDMTcuMzQ5IDE3LjQ5NjUgMTcuNjM0MSAxNy40MTcxIDE3Ljg4NDkgMTcuMjY5NkMxOC4xMzU3IDE3LjEyMiAxOC4zNDM1IDE2LjkxMTMgMTguNDg3NiAxNi42NTg1QzE4LjYzMTcgMTYuNDA1NiAxOC43MDcxIDE2LjExOTUgMTguNzA2MyAxNS44Mjg0QzE4LjcwNTUgMTUuNTM3NCAxOC42Mjg1IDE1LjI1MTcgMTguNDgzIDE0Ljk5OTdMMTEuNDI0NyAzLjIxNjM1QzExLjI3NjEgMi45NzE0NCAxMS4wNjY5IDIuNzY4OTUgMTAuODE3MyAyLjYyODQyQzEwLjU2NzcgMi40ODc4OSAxMC4yODYxIDIuNDE0MDYgOS45OTk2NSAyLjQxNDA2QzkuNzEzMjEgMi40MTQwNiA5LjQzMTU5IDIuNDg3ODkgOS4xODE5OSAyLjYyODQyQzguOTMyMzggMi43Njg5NSA4LjcyMzIxIDIuOTcxNDQgOC41NzQ2NSAzLjIxNjM1VjMuMjE2MzVaIiBzdHJva2U9IiMxZTFlMWUiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIi8+CjxwYXRoIGQ9Ik0xMCA3LjVWMTAuODMzMyIgc3Ryb2tlPSIjMWUxZTFlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8cGF0aCBkPSJNMTAgMTQuMTY4SDEwLjAwODMiIHN0cm9rZT0iIzFlMWUxZSIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==';
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from '@wordpress/element';
|
||||||
|
import { Component, ReactNode, ErrorInfo } from 'react';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import EmptyContent from '../empty-content';
|
||||||
|
import { alertIcon } from './constants';
|
||||||
|
|
||||||
|
export type ErrorBoundaryProps = {
|
||||||
|
/**
|
||||||
|
* The content to be rendered inside the ErrorBoundary component.
|
||||||
|
*/
|
||||||
|
children: ReactNode;
|
||||||
|
/**
|
||||||
|
* The custom error message to be displayed. Defaults to a generic message.
|
||||||
|
*/
|
||||||
|
errorMessage?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Determines whether to show an action button. Defaults to true.
|
||||||
|
*/
|
||||||
|
showActionButton?: boolean;
|
||||||
|
/**
|
||||||
|
* The label to be used for the action button. Defaults to 'Reload'.
|
||||||
|
*/
|
||||||
|
actionLabel?: string;
|
||||||
|
/**
|
||||||
|
* The callback function to be executed when the action button is clicked. Defaults to window.location.reload.
|
||||||
|
*
|
||||||
|
* @param error - The error that was caught.
|
||||||
|
*/
|
||||||
|
actionCallback?: ( error: Error ) => void;
|
||||||
|
/**
|
||||||
|
* Determines whether to reset the error boundary state after the action is performed. Defaults to true.
|
||||||
|
*/
|
||||||
|
resetErrorAfterAction?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function to be executed when an error is caught.
|
||||||
|
*
|
||||||
|
* @param error - The error that was caught.
|
||||||
|
* @param errorInfo - The error info object.
|
||||||
|
*/
|
||||||
|
onError?: ( error: Error, errorInfo: ErrorInfo ) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ErrorBoundaryState = {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
errorInfo: ErrorInfo | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ErrorBoundary extends Component<
|
||||||
|
ErrorBoundaryProps,
|
||||||
|
ErrorBoundaryState
|
||||||
|
> {
|
||||||
|
static defaultProps: Partial< ErrorBoundaryProps > = {
|
||||||
|
showActionButton: true,
|
||||||
|
resetErrorAfterAction: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor( props: ErrorBoundaryProps ) {
|
||||||
|
super( props );
|
||||||
|
this.state = { hasError: false, error: null, errorInfo: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(
|
||||||
|
error: Error
|
||||||
|
): Partial< ErrorBoundaryState > {
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch( _error: Error, errorInfo: ErrorInfo ) {
|
||||||
|
this.setState( { errorInfo } );
|
||||||
|
|
||||||
|
if ( this.props.onError ) {
|
||||||
|
this.props.onError( _error, errorInfo );
|
||||||
|
}
|
||||||
|
// TODO: Log error to error tracking service
|
||||||
|
}
|
||||||
|
|
||||||
|
handleReload = () => {
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAction = () => {
|
||||||
|
const { actionCallback, resetErrorAfterAction } = this.props;
|
||||||
|
|
||||||
|
if ( actionCallback ) {
|
||||||
|
actionCallback( this.state.error as Error );
|
||||||
|
} else {
|
||||||
|
this.handleReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( resetErrorAfterAction ) {
|
||||||
|
this.setState( { hasError: false, error: null, errorInfo: null } );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, errorMessage, showActionButton, actionLabel } =
|
||||||
|
this.props;
|
||||||
|
|
||||||
|
if ( this.state.hasError ) {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-error-boundary">
|
||||||
|
<EmptyContent
|
||||||
|
title=""
|
||||||
|
actionLabel=""
|
||||||
|
message={
|
||||||
|
errorMessage ||
|
||||||
|
__(
|
||||||
|
'Oops, something went wrong. Please try again',
|
||||||
|
'woocommerce'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
secondaryActionLabel={
|
||||||
|
actionLabel || __( 'Reload', 'woocommerce' )
|
||||||
|
}
|
||||||
|
secondaryActionURL={ null }
|
||||||
|
secondaryActionCallback={
|
||||||
|
showActionButton ? this.handleAction : undefined
|
||||||
|
}
|
||||||
|
illustrationWidth={ 36 }
|
||||||
|
illustrationHeight={ 36 }
|
||||||
|
illustration={ alertIcon }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
// ErrorBoundary.stories.tsx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import React, { createElement } from '@wordpress/element';
|
||||||
|
import { Meta, Story } from '@storybook/react';
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ErrorBoundary, ErrorBoundaryProps } from '../';
|
||||||
|
|
||||||
|
const ChildComponent = () => {
|
||||||
|
throw new Error( 'This is a test error' );
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template: Story< ErrorBoundaryProps > = ( args ) => (
|
||||||
|
<ErrorBoundary { ...args }>
|
||||||
|
<ChildComponent />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Default = Template.bind( {} );
|
||||||
|
Default.args = {};
|
||||||
|
|
||||||
|
export const CustomErrorMessage = Template.bind( {} );
|
||||||
|
CustomErrorMessage.args = {
|
||||||
|
errorMessage: 'Custom error message',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WithoutActionButton = Template.bind( {} );
|
||||||
|
WithoutActionButton.args = {
|
||||||
|
showActionButton: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomActionLabel = Template.bind( {} );
|
||||||
|
CustomActionLabel.args = {
|
||||||
|
actionLabel: 'Retry',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomActionCallback = Template.bind( {} );
|
||||||
|
CustomActionCallback.args = {
|
||||||
|
actionCallback: () => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
window.alert( 'Custom action callback triggered' );
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'WooCommerce Admin/experimental/Error Boundary',
|
||||||
|
component: ErrorBoundary,
|
||||||
|
argTypes: {
|
||||||
|
errorMessage: {
|
||||||
|
control: 'text',
|
||||||
|
defaultValue: 'Oops, something went wrong. Please try again',
|
||||||
|
},
|
||||||
|
showActionButton: {
|
||||||
|
control: 'boolean',
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
actionLabel: {
|
||||||
|
control: 'text',
|
||||||
|
defaultValue: 'Reload',
|
||||||
|
},
|
||||||
|
actionCallback: {
|
||||||
|
action: 'clicked',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
|
@ -0,0 +1,15 @@
|
||||||
|
.woocommerce-error-boundary {
|
||||||
|
margin-block: 16px;
|
||||||
|
|
||||||
|
.woocommerce-empty-content__message {
|
||||||
|
margin-block: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-empty-content {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-empty-content__illustration {
|
||||||
|
margin-bottom: -7px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,159 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import React, { createElement } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ErrorBoundary } from '..';
|
||||||
|
|
||||||
|
const ThrowError = () => {
|
||||||
|
throw new Error( 'Test error' );
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe( 'ErrorBoundary', () => {
|
||||||
|
function onError( event: Event ) {
|
||||||
|
// Note: this will swallow reports about unhandled errors!
|
||||||
|
// Use with extreme caution.
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock window.location.reload by using a global variable
|
||||||
|
const originalLocation = window.location;
|
||||||
|
|
||||||
|
beforeAll( () => {
|
||||||
|
// Opt Out of the jsdom error messages
|
||||||
|
window.addEventListener( 'error', onError );
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - Ignore TS error for deleting window.location
|
||||||
|
delete window.location;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore - Ignore TS error for assigning window.location
|
||||||
|
window.location = { reload: jest.fn() };
|
||||||
|
} );
|
||||||
|
|
||||||
|
afterAll( () => {
|
||||||
|
window.location = originalLocation;
|
||||||
|
window.removeEventListener( 'error', onError );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'catches errors and displays an error message', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText( 'Oops, something went wrong. Please try again' )
|
||||||
|
).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'shows reload button', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( screen.getByText( 'Reload' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'refreshes the page when Reload Page button is clicked', () => {
|
||||||
|
const reloadMock = jest.fn();
|
||||||
|
Object.defineProperty( window.location, 'reload', {
|
||||||
|
configurable: true,
|
||||||
|
value: reloadMock,
|
||||||
|
} );
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click( screen.getByText( 'Reload' ) );
|
||||||
|
|
||||||
|
expect( reloadMock ).toHaveBeenCalled();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'triggers on error callback when provided', () => {
|
||||||
|
const onError = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ErrorBoundary onError={ onError }>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( onError ).toHaveBeenCalled();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'triggers custom action callback when provided', () => {
|
||||||
|
const customActionCallback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ErrorBoundary actionCallback={ customActionCallback }>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click( screen.getByText( 'Reload' ) );
|
||||||
|
|
||||||
|
expect( customActionCallback ).toHaveBeenCalledWith(
|
||||||
|
new Error( 'Test error' )
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'does not display the action button when showActionButton is false', () => {
|
||||||
|
render(
|
||||||
|
<ErrorBoundary showActionButton={ false }>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect( screen.queryByText( 'Reload' ) ).not.toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'resets error boundary state after action is performed when resetErrorAfterAction is true', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div>Child Component</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click( screen.getByText( 'Reload' ) );
|
||||||
|
|
||||||
|
expect( screen.getByText( 'Child Component' ) ).toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'does not reset error boundary state after action is performed when resetErrorAfterAction is false', () => {
|
||||||
|
const { rerender } = render(
|
||||||
|
<ErrorBoundary resetErrorAfterAction={ false }>
|
||||||
|
<ThrowError />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click( screen.getByText( 'Reload' ) );
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<ErrorBoundary resetErrorAfterAction={ false }>
|
||||||
|
<div>Child Component</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText( 'Child Component' )
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -114,3 +114,4 @@ export {
|
||||||
export { DisplayState } from './display-state';
|
export { DisplayState } from './display-state';
|
||||||
export { ProgressBar } from './progress-bar';
|
export { ProgressBar } from './progress-bar';
|
||||||
export { ConfettiAnimation } from './confetti-animation';
|
export { ConfettiAnimation } from './confetti-animation';
|
||||||
|
export { ErrorBoundary as __experimentalErrorBoundary } from './error-boundary';
|
||||||
|
|
|
@ -61,3 +61,4 @@
|
||||||
@import 'tree-select-control/index.scss';
|
@import 'tree-select-control/index.scss';
|
||||||
@import 'progress-bar/style.scss';
|
@import 'progress-bar/style.scss';
|
||||||
@import 'phone-number-input/style.scss';
|
@import 'phone-number-input/style.scss';
|
||||||
|
@import 'error-boundary/style.scss';
|
||||||
|
|
862
pnpm-lock.yaml
862
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -47,6 +47,7 @@
|
||||||
"@storybook/theming": "6.5.17-alpha.0",
|
"@storybook/theming": "6.5.17-alpha.0",
|
||||||
"@woocommerce/eslint-plugin": "workspace:*",
|
"@woocommerce/eslint-plugin": "workspace:*",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react18": "npm:react@^18.3.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
const path = require( 'path' );
|
const path = require( 'path' );
|
||||||
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
|
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
|
||||||
|
const webpack = require( 'webpack' );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
|
@ -39,6 +40,13 @@ module.exports = ( storybookConfig ) => {
|
||||||
'./setting.mock.js'
|
'./setting.mock.js'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
storybookConfig.resolve.alias[ 'react/jsx-runtime' ] =
|
||||||
|
require.resolve( 'react/jsx-runtime' );
|
||||||
|
|
||||||
|
// We need to use react 18 for the storybook since some dependencies are not compatible with react 17
|
||||||
|
// Once we upgrade react to 18 in repo, we can remove this alias
|
||||||
|
storybookConfig.resolve.alias.react = require.resolve( 'react18' );
|
||||||
|
|
||||||
storybookConfig.resolve.modules = [
|
storybookConfig.resolve.modules = [
|
||||||
path.join( __dirname, '../../plugins/woocommerce-admin/client' ),
|
path.join( __dirname, '../../plugins/woocommerce-admin/client' ),
|
||||||
path.join( __dirname, '../../packages/js/product-editor/src' ),
|
path.join( __dirname, '../../packages/js/product-editor/src' ),
|
||||||
|
|
Loading…
Reference in New Issue