Enhancement editor loading speed (#47425)

* Lazy load the PluginArea and the ModalEditor

* Remove repeated product request when editing a specific product

* Fix linter errors

* Add changelog files

* Fix linter errors

* Refactor the block editor to remove some extra rerenders

* Defer the publish button processing

* Defer the tab content render 500ms to reduce the total blocking time

* Fix linter errors

* Fix unit test and tabs unexpected rerender

* Fix linter errors

* Reduce the defered time to 300ms

* Fix get product url when the product has been duplicated since the new copy does not have permalink

* Fix the invalid unregistration of wc-admin-more-menu in the product page

* Fix compilation errors
This commit is contained in:
Maikel Perez 2024-05-24 08:39:53 -04:00 committed by GitHub
parent 756bb8ccfa
commit 4ddfd43864
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 339 additions and 184 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Enhancement editor loading speed

View File

@ -3,7 +3,7 @@
*/
import { InnerBlocks } from '@wordpress/block-editor';
import classnames from 'classnames';
import { createElement } from '@wordpress/element';
import { createElement, useEffect, useState } from '@wordpress/element';
import type { BlockAttributes } from '@wordpress/blocks';
import { useWooBlockProps } from '@woocommerce/block-templates';
@ -25,21 +25,30 @@ export function TabBlockEdit( {
context,
}: ProductEditorBlockEditProps< TabBlockAttributes > ) {
const blockProps = useWooBlockProps( attributes );
const {
id,
title,
_templateBlockOrder: order,
isSelected: contextIsSelected,
} = attributes;
const isSelected = context.selectedTab === id;
if ( isSelected !== contextIsSelected ) {
setAttributes( { isSelected } );
}
const { id, title, _templateBlockOrder: order, isSelected } = attributes;
const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
'is-selected': isSelected,
} );
const [ canRenderChildren, setCanRenderChildren ] = useState( false );
useEffect( () => {
if ( ! context.selectedTab ) return;
const isSelectedInContext = context.selectedTab === id;
setAttributes( { isSelected: isSelectedInContext } );
if ( isSelectedInContext ) {
setCanRenderChildren( true );
return;
}
const timeoutId = setTimeout( setCanRenderChildren, 300, true );
return () => clearTimeout( timeoutId );
}, [ context.selectedTab, id ] );
return (
<div { ...blockProps }>
<TabButton id={ id } selected={ isSelected } order={ order }>
@ -51,9 +60,11 @@ export function TabBlockEdit( {
role="tabpanel"
className={ classes }
>
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore Content only template locking does exist for this property. */ }
<InnerBlocks templateLock="contentOnly" />
{ canRenderChildren && (
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
/* @ts-ignore Content only template locking does exist for this property. */
<InnerBlocks templateLock="contentOnly" />
) }
</div>
</div>
);

View File

@ -8,10 +8,11 @@ import {
useLayoutEffect,
useEffect,
useState,
lazy,
Suspense,
} from '@wordpress/element';
import { useDispatch, useSelect, select as WPSelect } from '@wordpress/data';
import { dispatch, select, useSelect } from '@wordpress/data';
import { uploadMedia } from '@wordpress/media-utils';
import { PluginArea } from '@wordpress/plugins';
import { __ } from '@wordpress/i18n';
import { useLayoutTemplate } from '@woocommerce/block-templates';
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
@ -46,12 +47,23 @@ import { useConfirmUnsavedProductChanges } from '../../hooks/use-confirm-unsaved
import { useProductTemplate } from '../../hooks/use-product-template';
import { PostTypeContext } from '../../contexts/post-type-context';
import { store as productEditorUiStore } from '../../store/product-editor-ui';
import { ModalEditor } from '../modal-editor';
import { ProductEditorSettings } from '../editor';
import { BlockEditorProps } from './types';
import { ProductTemplate } from '../../types';
import { LoadingState } from './loading-state';
const PluginArea = lazy( () =>
import( '@wordpress/plugins' ).then( ( module ) => ( {
default: module.PluginArea,
} ) )
);
const ModalEditor = lazy( () =>
import( '../modal-editor' ).then( ( module ) => ( {
default: module.ModalEditor,
} ) )
);
function getLayoutTemplateId(
productTemplate: ProductTemplate | undefined,
postType: string
@ -76,11 +88,6 @@ export function BlockEditor( {
}: BlockEditorProps ) {
useConfirmUnsavedProductChanges( postType );
const canUserCreateMedia = useSelect( ( select: typeof WPSelect ) => {
const { canUser } = select( 'core' );
return canUser( 'create', 'media', '' ) !== false;
}, [] );
/**
* Fire wp-pin-menu event once to trigger the pinning of the menu.
* That can be necessary since wpwrap's height wasn't being recalculated after the skeleton
@ -94,10 +101,9 @@ export function BlockEditor( {
return () => window.removeEventListener( 'scroll', wpPinMenuEvent );
}, [] );
// @ts-expect-error Type definitions are missing
const { registerShortcut } = useDispatch( keyboardShortcutsStore );
useEffect( () => {
// @ts-expect-error Type definitions are missing
const { registerShortcut } = dispatch( keyboardShortcutsStore );
if ( registerShortcut ) {
registerShortcut( {
name: 'core/editor/save',
@ -109,7 +115,7 @@ export function BlockEditor( {
},
} );
}
}, [ registerShortcut ] );
}, [] );
const [ settingsGlobal, setSettingsGlobal ] = useState<
Partial< ProductEditorSettings > | undefined
@ -140,6 +146,9 @@ export function BlockEditor( {
return undefined;
}
const canUserCreateMedia =
select( 'core' ).canUser( 'create', 'media', '' ) !== false;
const mediaSettings = canUserCreateMedia
? {
mediaUpload( {
@ -165,7 +174,7 @@ export function BlockEditor( {
...mediaSettings,
templateLock: 'all',
};
}, [ settingsGlobal, canUserCreateMedia ] );
}, [ settingsGlobal ] );
const { editedRecord: product } = useEntityRecord< Product >(
'postType',
@ -175,10 +184,14 @@ export function BlockEditor( {
{ enabled: productId !== -1 }
);
const productTemplateId = product?.meta_data?.find(
( metaEntry: { key: string } ) =>
metaEntry.key === '_product_template_id'
)?.value;
const productTemplateId = useMemo(
() =>
product?.meta_data?.find(
( metaEntry: { key: string } ) =>
metaEntry.key === '_product_template_id'
)?.value,
[ product?.meta_data ]
);
const { productTemplate } = useProductTemplate(
productTemplateId,
@ -196,8 +209,6 @@ export function BlockEditor( {
{ id: productId !== -1 ? productId : 0 }
);
const { updateEditorSettings } = useDispatch( 'core/editor' );
const isEditorLoading =
! settings ||
! layoutTemplate ||
@ -217,7 +228,7 @@ export function BlockEditor( {
onChange( blockInstances, {} );
updateEditorSettings( {
dispatch( 'core/editor' ).updateEditorSettings( {
...settings,
productTemplate,
} as Partial< ProductEditorSettings > );
@ -232,21 +243,31 @@ export function BlockEditor( {
// the blocks by calling onChange.
//
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ layoutTemplate, settings, productTemplate, productId ] );
}, [ isEditorLoading, productId ] );
// Check if the Modal editor is open from the store.
const isModalEditorOpen = useSelect( ( select ) => {
return select( productEditorUiStore ).isModalEditorOpen();
const isModalEditorOpen = useSelect( ( selectCore ) => {
return selectCore( productEditorUiStore ).isModalEditorOpen();
}, [] );
const { closeModalEditor } = useDispatch( productEditorUiStore );
if ( isEditorLoading ) {
return (
<div className="woocommerce-product-block-editor">
<LoadingState />
</div>
);
}
if ( isModalEditorOpen ) {
return (
<ModalEditor
onClose={ closeModalEditor }
title={ __( 'Edit description', 'woocommerce' ) }
/>
<Suspense fallback={ null }>
<ModalEditor
onClose={
dispatch( productEditorUiStore ).closeModalEditor
}
title={ __( 'Edit description', 'woocommerce' ) }
/>
</Suspense>
);
}
@ -265,17 +286,15 @@ export function BlockEditor( {
<BlockEditorKeyboardShortcuts.Register />
<BlockTools>
<ObserveTyping>
{ isEditorLoading ? (
<LoadingState />
) : (
<BlockList className="woocommerce-product-block-editor__block-list" />
) }
<BlockList className="woocommerce-product-block-editor__block-list" />
</ObserveTyping>
</BlockTools>
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }
<PostTypeContext.Provider value={ context.postType! }>
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-block-editor" />
<Suspense fallback={ null }>
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
<PluginArea scope="woocommerce-product-block-editor" />
</Suspense>
</PostTypeContext.Provider>
</BlockEditorProvider>
</BlockContextProvider>

View File

@ -37,14 +37,12 @@ import { EditorProps } from './types';
import { store as productEditorUiStore } from '../../store/product-editor-ui';
import { PrepublishPanel } from '../prepublish-panel/prepublish-panel';
export function Editor( { product, productType = 'product' }: EditorProps ) {
export function Editor( { productId, postType = 'product' }: EditorProps ) {
const [ isEditorLoading, setIsEditorLoading ] = useState( true );
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
const updatedLayoutContext = useExtendLayout( 'product-block-editor' );
const productId = product?.id || -1;
// Check if the prepublish sidebar is open from the store.
const isPrepublishPanelOpen = useSelect( ( select ) => {
return select( productEditorUiStore ).isPrepublishPanelOpen();
@ -55,11 +53,14 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
<StrictMode>
<EntityProvider
kind="postType"
type={ productType }
type={ postType }
id={ productId }
>
<ShortcutProvider>
<ValidationProvider initialValue={ product }>
<ValidationProvider
postType={ postType }
productId={ productId }
>
<EditorLoadingContext.Provider
value={ isEditorLoading }
>
@ -67,17 +68,17 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
header={
<Header
onTabSelect={ setSelectedTab }
productType={ productType }
productType={ postType }
/>
}
content={
<>
<BlockEditor
postType={ productType }
postType={ postType }
productId={ productId }
context={ {
selectedTab,
postType: productType,
postType,
postId: productId,
} }
setIsEditorLoading={
@ -89,7 +90,7 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
actions={
isPrepublishPanelOpen && (
<PrepublishPanel
productType={ productType }
productType={ postType }
/>
)
}

View File

@ -1,7 +1,6 @@
/**
* External dependencies
*/
import { Product } from '@woocommerce/data';
import {
EditorSettings,
EditorBlockListSettings,
@ -29,6 +28,6 @@ export type ProductEditorSettings = Partial<
};
export type EditorProps = {
product?: Pick< Product, 'id' | 'type' > | null;
productType?: string;
productId: number;
postType?: string;
};

View File

@ -9,6 +9,8 @@ import {
useContext,
useEffect,
Fragment,
lazy,
Suspense,
} from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Button, Tooltip } from '@wordpress/components';
@ -31,13 +33,18 @@ import { getHeaderTitle } from '../../utils';
import { MoreMenu } from './more-menu';
import { PreviewButton } from './preview-button';
import { SaveDraftButton } from './save-draft-button';
import { PublishButton } from './publish-button';
import { LoadingState } from './loading-state';
import { Tabs } from '../tabs';
import { HEADER_PINNED_ITEMS_SCOPE, TRACKS_SOURCE } from '../../constants';
import { useShowPrepublishChecks } from '../../hooks/use-show-prepublish-checks';
import { HeaderProps, Image } from './types';
const PublishButton = lazy( () =>
import( './publish-button' ).then( ( module ) => ( {
default: module.PublishButton,
} ) )
);
const RETURN_TO_MAIN_PRODUCT = __(
'Return to the main product',
'woocommerce'
@ -254,11 +261,13 @@ export function Header( {
productStatus={ lastPersistedProduct?.status }
/>
<PublishButton
productType={ productType }
isPrePublishPanelVisible={ showPrepublishChecks }
isMenuButton
/>
<Suspense fallback={ null }>
<PublishButton
productType={ productType }
isPrePublishPanelVisible={ showPrepublishChecks }
isMenuButton
/>
</Suspense>
<WooHeaderItem.Slot name="product" />
<PinnedItems.Slot scope={ HEADER_PINNED_ITEMS_SCOPE } />

View File

@ -26,6 +26,8 @@ export function PostPublishSection( { postType }: PostPublishSectionProps ) {
const productURL = getProductURL( isScheduled );
if ( ! productURL ) return null;
const CopyButton = ( { text, onCopy, children }: CopyButtonProps ) => {
const ref = useCopyToClipboard(
text,

View File

@ -1,12 +1,17 @@
/**
* External dependencies
*/
import { createElement, useEffect, useState } from '@wordpress/element';
import { ReactElement } from 'react';
import {
createElement,
useEffect,
useState,
Fragment,
} from '@wordpress/element';
import { ReactElement, useMemo } from 'react';
import { NavigableMenu, Slot } from '@wordpress/components';
import { Product } from '@woocommerce/data';
import { recordEvent } from '@woocommerce/tracks';
import { useSelect } from '@wordpress/data';
import { select } from '@wordpress/data';
import { useEntityProp } from '@wordpress/core-data';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore No types for this exist yet.
@ -17,7 +22,6 @@ import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
* Internal dependencies
*/
import { getTabTracksData } from './utils/get-tab-tracks-data';
import { sortFillsByOrder } from '../../utils';
import { TABS_SLOT_NAME } from './constants';
type TabsProps = {
@ -28,6 +32,38 @@ export type TabsFillProps = {
onClick: ( tabId: string ) => void;
};
function TabFills( {
fills,
onDefaultSelection,
}: {
fills: readonly ( readonly ReactElement[] )[];
onDefaultSelection( tabId: string ): void;
} ) {
const sortedFills = useMemo(
function sortFillsByOrder() {
return [ ...fills ].sort(
( [ { props: a } ], [ { props: b } ] ) => a.order - b.order
);
},
[ fills ]
);
useEffect( () => {
for ( let i = 0; i < sortedFills.length; i++ ) {
const [ { props } ] = fills[ i ];
if ( ! props.disabled ) {
const tabId = props.children?.key;
if ( tabId ) {
onDefaultSelection( tabId );
}
return;
}
}
}, [ sortedFills ] );
return <>{ sortedFills }</>;
}
export function Tabs( { onChange = () => {} }: TabsProps ) {
const [ selected, setSelected ] = useState< string | null >( null );
const query = getQuery() as Record< string, string >;
@ -36,41 +72,14 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
'product',
'id'
);
const product: Product = useSelect( ( select ) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
select( 'core' ).getEditedEntityRecord(
'postType',
'product',
productId
)
);
useEffect( () => {
onChange( selected );
}, [ selected ] );
useEffect( () => {
if ( query.tab ) {
setSelected( query.tab );
onChange( query.tab );
}
}, [ query.tab ] );
function maybeSetSelected( fills: readonly ( readonly ReactElement[] )[] ) {
if ( selected ) {
return;
}
for ( let i = 0; i < fills.length; i++ ) {
if ( fills[ i ][ 0 ].props.disabled ) {
continue;
}
const tabId = fills[ i ][ 0 ].props?.children?.key || null;
setSelected( tabId );
return;
}
}
function selectTabOnNavigate(
_childIndex: number,
child: HTMLButtonElement
@ -78,6 +87,21 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
child.click();
}
function renderFills( fills: readonly ( readonly ReactElement[] )[] ) {
return (
<TabFills
fills={ fills }
onDefaultSelection={ ( tabId ) => {
if ( selected ) {
return;
}
setSelected( tabId );
onChange( tabId );
} }
/>
);
}
return (
<NavigableMenu
role="tablist"
@ -92,6 +116,17 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
navigateTo( {
url: getNewPath( { tab: tabId } ),
} );
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const { getEditedEntityRecord } = select( 'core' );
const product: Product = getEditedEntityRecord(
'postType',
'product',
productId
);
recordEvent(
'product_tab_click',
getTabTracksData( tabId, product )
@ -101,14 +136,7 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
}
name={ TABS_SLOT_NAME }
>
{ ( fills ) => {
if ( ! sortFillsByOrder ) {
return null;
}
maybeSetSelected( fills );
return sortFillsByOrder( fills );
} }
{ renderFills }
</Slot>
</NavigableMenu>
);

View File

@ -1,17 +1,20 @@
/**
* External dependencies
*/
import { render, fireEvent } from '@testing-library/react';
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { getQuery, navigateTo } from '@woocommerce/navigation';
import React, { createElement } from 'react';
import { SlotFillProvider } from '@wordpress/components';
import { useState } from '@wordpress/element';
import { useState, createElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { Tabs } from '../';
import { TabBlockEdit as Tab } from '../../../blocks/generic/tab/edit';
import {
TabBlockEdit as Tab,
TabBlockAttributes,
} from '../../../blocks/generic/tab/edit';
jest.mock( '@woocommerce/block-templates', () => ( {
...jest.requireActual( '@woocommerce/block-templates' ),
@ -40,6 +43,16 @@ function MockTabs( { onChange = jest.fn() } ) {
selectedTab: selected,
};
function getAttributes( id: string ) {
return function setAttributes( {
isSelected,
}: Partial< TabBlockAttributes > ) {
if ( isSelected ) {
setSelected( id );
}
};
}
return (
<SlotFillProvider>
<Tabs
@ -50,27 +63,45 @@ function MockTabs( { onChange = jest.fn() } ) {
/>
<Tab
{ ...blockProps }
attributes={ { id: 'test1', title: 'Test button 1', order: 1 } }
attributes={ {
id: 'test1',
title: 'Test button 1',
order: 1,
isSelected: selected === 'test1',
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test1"
setAttributes={ getAttributes( 'test1' ) }
/>
<Tab
{ ...blockProps }
attributes={ { id: 'test2', title: 'Test button 2', order: 2 } }
attributes={ {
id: 'test2',
title: 'Test button 2',
order: 2,
isSelected: selected === 'test2',
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test2"
setAttributes={ getAttributes( 'test2' ) }
/>
<Tab
{ ...blockProps }
attributes={ { id: 'test3', title: 'Test button 3', order: 3 } }
attributes={ {
id: 'test3',
title: 'Test button 3',
order: 3,
isSelected: selected === 'test3',
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
context={ mockContext }
name="test3"
setAttributes={ getAttributes( 'test3' ) }
/>
</SlotFillProvider>
);
@ -84,26 +115,30 @@ describe( 'Tabs', () => {
} );
it( 'should render tab buttons added to the slot', () => {
const { queryByText } = render( <MockTabs /> );
expect( queryByText( 'Test button 1' ) ).toBeInTheDocument();
expect( queryByText( 'Test button 2' ) ).toBeInTheDocument();
render( <MockTabs /> );
expect( screen.queryByText( 'Test button 1' ) ).toBeInTheDocument();
expect( screen.queryByText( 'Test button 2' ) ).toBeInTheDocument();
} );
it( 'should set the first tab as active initially', () => {
const { queryByText } = render( <MockTabs /> );
expect( queryByText( 'Test button 1' ) ).toHaveAttribute(
it( 'should set the first tab as active initially', async () => {
render( <MockTabs /> );
expect( screen.queryByText( 'Test button 1' ) ).toHaveAttribute(
'aria-selected',
'true'
);
expect( queryByText( 'Test button 2' ) ).toHaveAttribute(
expect( screen.queryByText( 'Test button 2' ) ).toHaveAttribute(
'aria-selected',
'false'
);
} );
it( 'should navigate to a new URL when a tab is clicked', () => {
const { getByText } = render( <MockTabs /> );
const button = getByText( 'Test button 2' );
render( <MockTabs /> );
const button = screen.getByText( 'Test button 2' );
fireEvent.click( button );
expect( navigateTo ).toHaveBeenLastCalledWith( {
@ -116,16 +151,16 @@ describe( 'Tabs', () => {
tab: 'test2',
} );
const { getByText } = render( <MockTabs /> );
render( <MockTabs /> );
expect( getByText( 'Test button 2' ) ).toHaveAttribute(
expect( screen.getByText( 'Test button 2' ) ).toHaveAttribute(
'aria-selected',
'true'
);
} );
it( 'should select the tab provided on URL change', () => {
const { getByText, rerender } = render( <MockTabs /> );
const { rerender } = render( <MockTabs /> );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test3',
@ -133,7 +168,7 @@ describe( 'Tabs', () => {
rerender( <MockTabs /> );
expect( getByText( 'Test button 3' ) ).toHaveAttribute(
expect( screen.getByText( 'Test button 3' ) ).toHaveAttribute(
'aria-selected',
'true'
);
@ -155,20 +190,30 @@ describe( 'Tabs', () => {
} );
it( 'should add a class to the initially selected tab panel', async () => {
const { getByRole } = render( <MockTabs /> );
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
render( <MockTabs /> );
const panel1 = screen.getByRole( 'tabpanel', {
name: 'Test button 1',
} );
const panel2 = screen.getByRole( 'tabpanel', {
name: 'Test button 2',
} );
expect( panel1.classList ).toContain( 'is-selected' );
expect( panel2.classList ).not.toContain( 'is-selected' );
} );
it( 'should add a class to the newly selected tab panel', async () => {
const { getByText, getByRole, rerender } = render( <MockTabs /> );
const button = getByText( 'Test button 2' );
const { rerender } = render( <MockTabs /> );
const button = screen.getByText( 'Test button 2' );
fireEvent.click( button );
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
const panel1 = screen.getByRole( 'tabpanel', {
name: 'Test button 1',
} );
const panel2 = screen.getByRole( 'tabpanel', {
name: 'Test button 2',
} );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test2',

View File

@ -19,8 +19,9 @@ export type ValidationContextProps< T > = {
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
};
export type ValidationProviderProps< T > = {
initialValue?: T;
export type ValidationProviderProps = {
postType: string;
productId: number;
};
export type ValidationError = string | undefined;

View File

@ -1,8 +1,9 @@
/**
* External dependencies
*/
import type { PropsWithChildren } from 'react';
import { useEntityRecord } from '@wordpress/core-data';
import { createElement, useRef, useState } from '@wordpress/element';
import { PropsWithChildren } from 'react';
/**
* Internal dependencies
@ -17,12 +18,18 @@ import { ValidationContext } from './validation-context';
import { findFirstInvalidElement } from './helpers';
export function ValidationProvider< T >( {
initialValue,
postType,
productId,
children,
}: PropsWithChildren< ValidationProviderProps< T > > ) {
}: PropsWithChildren< ValidationProviderProps > ) {
const validatorsRef = useRef< Record< string, Validator< T > > >( {} );
const fieldRefs = useRef< Record< string, HTMLElement > >( {} );
const [ errors, setErrors ] = useState< ValidationErrors >( {} );
const { record: initialValue } = useEntityRecord< T >(
'postType',
postType,
productId
);
function registerValidator(
validatorId: string,

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { useEntityProp } from '@wordpress/core-data';
import { useCallback } from 'react';
import { useCallback } from '@wordpress/element';
export function useProductURL( productType: string ) {
const [ permalink ] = useEntityProp< string >(
@ -10,15 +10,19 @@ export function useProductURL( productType: string ) {
productType,
'permalink'
);
const getProductURL = useCallback(
( isPreview: boolean ) => {
const productURL = new URL( permalink ) as URL | undefined;
if ( ! permalink ) return undefined;
const productURL = new URL( permalink );
if ( isPreview ) {
productURL?.searchParams.append( 'preview', 'true' );
productURL.searchParams.append( 'preview', 'true' );
}
return productURL?.toString() || '';
return productURL.toString();
},
[ permalink ]
);
return { getProductURL };
}

View File

@ -2,40 +2,39 @@
* External dependencies
*/
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
import { Product } from '@woocommerce/data';
import { useDispatch, resolveSelect } from '@wordpress/data';
import { type Product } from '@woocommerce/data';
import { dispatch } from '@wordpress/data';
import { useEffect, useState } from '@wordpress/element';
export function useProductEntityRecord(
productId: string | undefined
): Product | undefined {
const { saveEntityRecord } = useDispatch( 'core' );
const [ product, setProduct ] = useState< Product | undefined >(
undefined
);
): number | undefined {
const [ id, setId ] = useState< number | undefined >( undefined );
useEffect( () => {
const getRecordPromise: Promise< Product > = productId
? resolveSelect( 'core' ).getEntityRecord< Product >(
'postType',
'product',
Number.parseInt( productId, 10 )
)
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Incorrect types.
( saveEntityRecord( 'postType', 'product', {
title: AUTO_DRAFT_NAME,
status: 'auto-draft',
} ) as Promise< Product > );
getRecordPromise
.then( ( autoDraftProduct: Product ) => {
setProduct( autoDraftProduct );
} )
if ( productId ) {
setId( Number.parseInt( productId, 10 ) );
return;
}
const createProductPromise = dispatch( 'core' ).saveEntityRecord(
'postType',
'product',
{
title: AUTO_DRAFT_NAME,
status: 'auto-draft',
}
) as never as Promise< Product >;
createProductPromise
.then( ( autoDraftProduct: Product ) =>
setId( autoDraftProduct.id )
)
.catch( ( e ) => {
setProduct( undefined );
setId( undefined );
throw e;
} );
}, [ productId ] );
return product;
return id;
}

View File

@ -17,6 +17,7 @@ import React, { lazy, Suspense, useContext, useEffect } from 'react';
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
import { useParams } from 'react-router-dom';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@ -39,9 +40,7 @@ const ProductMVPFeedbackModalContainer = lazy( () =>
);
export default function ProductPage() {
const { productId } = useParams();
const product = useProductEntityRecord( productId );
const { productId: productIdSearchParam } = useParams();
useEffect( () => {
document.body.classList.add( 'is-product-editor' );
@ -70,8 +69,11 @@ export default function ProductPage() {
<Suspense fallback={ <Spinner /> }>
<ProductMVPFeedbackModalContainer
productId={
productId
? parseInt( productId, 10 )
productIdSearchParam
? Number.parseInt(
productIdSearchParam,
10
)
: undefined
}
/>
@ -91,17 +93,17 @@ export default function ProductPage() {
return () => {
document.body.classList.remove( 'is-product-editor' );
unregisterPlugin( 'wc-admin-more-menu' );
unregisterPlugin( 'wc-admin-product-editor' );
unregisterBlocks();
};
}, [ productId ] );
}, [ productIdSearchParam ] );
useEffect(
function trackViewEvents() {
if ( productId ) {
if ( productIdSearchParam ) {
recordEvent( 'product_edit_view', {
source: TRACKS_SOURCE,
product_id: productId,
product_id: productIdSearchParam,
} );
} else {
recordEvent( 'product_add_view', {
@ -109,12 +111,20 @@ export default function ProductPage() {
} );
}
},
[ productId ]
[ productIdSearchParam ]
);
return (
<>
<Editor product={ product } />
</>
);
const productId = useProductEntityRecord( productIdSearchParam );
if ( ! productId ) {
return (
<div className="woocommerce-layout__loading">
<Spinner
aria-label={ __( 'Creating the product', 'woocommerce' ) }
/>
</div>
);
}
return <Editor productId={ productId } />;
}

View File

@ -16,6 +16,8 @@ import { useEffect } from '@wordpress/element';
import { WooFooterItem } from '@woocommerce/admin-layout';
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
import { useParams } from 'react-router-dom';
import { Spinner } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@ -78,9 +80,19 @@ export default function ProductPage() {
[ productId ]
);
if ( ! variation ) {
return (
<div className="woocommerce-layout__loading">
<Spinner
aria-label={ __( 'Creating the product', 'woocommerce' ) }
/>
</div>
);
}
return (
<>
<Editor product={ variation } productType="product_variation" />
<Editor productId={ variation.id } postType="product_variation" />
<WooFooterItem order={ 0 }>
<>
<VariationSwitcherFooter

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Enhancement editor loading speed