diff --git a/packages/js/product-editor/changelog/enhancement-46491 b/packages/js/product-editor/changelog/enhancement-46491
new file mode 100644
index 00000000000..bf2258baaa9
--- /dev/null
+++ b/packages/js/product-editor/changelog/enhancement-46491
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Enhancement editor loading speed
diff --git a/packages/js/product-editor/src/blocks/generic/tab/edit.tsx b/packages/js/product-editor/src/blocks/generic/tab/edit.tsx
index 3a091ff54ee..0b2b49fc1c2 100644
--- a/packages/js/product-editor/src/blocks/generic/tab/edit.tsx
+++ b/packages/js/product-editor/src/blocks/generic/tab/edit.tsx
@@ -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 (
@@ -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. */ }
-
+ { canRenderChildren && (
+ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
+ /* @ts-ignore Content only template locking does exist for this property. */
+
+ ) }
);
diff --git a/packages/js/product-editor/src/components/block-editor/block-editor.tsx b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
index 0b0324baef9..0dc1397cdb7 100644
--- a/packages/js/product-editor/src/components/block-editor/block-editor.tsx
+++ b/packages/js/product-editor/src/components/block-editor/block-editor.tsx
@@ -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 (
+
+
+
+ );
+ }
if ( isModalEditorOpen ) {
return (
-
+
+
+
);
}
@@ -265,17 +286,15 @@ export function BlockEditor( {
- { isEditorLoading ? (
-
- ) : (
-
- ) }
+
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }
- { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
-
+
+ { /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
+
+
diff --git a/packages/js/product-editor/src/components/editor/editor.tsx b/packages/js/product-editor/src/components/editor/editor.tsx
index 4bcfa075943..6ff5724e7e3 100644
--- a/packages/js/product-editor/src/components/editor/editor.tsx
+++ b/packages/js/product-editor/src/components/editor/editor.tsx
@@ -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 ) {
-
+
@@ -67,17 +68,17 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
header={
}
content={
<>
)
}
diff --git a/packages/js/product-editor/src/components/editor/types.ts b/packages/js/product-editor/src/components/editor/types.ts
index 430308b0a69..e2b473e80c3 100644
--- a/packages/js/product-editor/src/components/editor/types.ts
+++ b/packages/js/product-editor/src/components/editor/types.ts
@@ -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;
};
diff --git a/packages/js/product-editor/src/components/header/header.tsx b/packages/js/product-editor/src/components/header/header.tsx
index 3bac0d8d277..ae6ae30118a 100644
--- a/packages/js/product-editor/src/components/header/header.tsx
+++ b/packages/js/product-editor/src/components/header/header.tsx
@@ -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 }
/>
-
+
+
+
diff --git a/packages/js/product-editor/src/components/prepublish-panel/post-publish/post-publish-section.tsx b/packages/js/product-editor/src/components/prepublish-panel/post-publish/post-publish-section.tsx
index fd5a58a6d46..6db125b250c 100644
--- a/packages/js/product-editor/src/components/prepublish-panel/post-publish/post-publish-section.tsx
+++ b/packages/js/product-editor/src/components/prepublish-panel/post-publish/post-publish-section.tsx
@@ -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,
diff --git a/packages/js/product-editor/src/components/tabs/tabs.tsx b/packages/js/product-editor/src/components/tabs/tabs.tsx
index a5f9904d67f..77fe3ab7735 100644
--- a/packages/js/product-editor/src/components/tabs/tabs.tsx
+++ b/packages/js/product-editor/src/components/tabs/tabs.tsx
@@ -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 (
+ {
+ if ( selected ) {
+ return;
+ }
+ setSelected( tabId );
+ onChange( tabId );
+ } }
+ />
+ );
+ }
+
return (
{} }: 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 }
);
diff --git a/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx
index f94f96a1d44..ca034907e20 100644
--- a/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx
+++ b/packages/js/product-editor/src/components/tabs/test/tabs.spec.tsx
@@ -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 (
);
@@ -84,26 +115,30 @@ describe( 'Tabs', () => {
} );
it( 'should render tab buttons added to the slot', () => {
- const { queryByText } = render( );
- expect( queryByText( 'Test button 1' ) ).toBeInTheDocument();
- expect( queryByText( 'Test button 2' ) ).toBeInTheDocument();
+ render( );
+
+ 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( );
- expect( queryByText( 'Test button 1' ) ).toHaveAttribute(
+ it( 'should set the first tab as active initially', async () => {
+ render( );
+
+ 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( );
- const button = getByText( 'Test button 2' );
+ render( );
+
+ const button = screen.getByText( 'Test button 2' );
fireEvent.click( button );
expect( navigateTo ).toHaveBeenLastCalledWith( {
@@ -116,16 +151,16 @@ describe( 'Tabs', () => {
tab: 'test2',
} );
- const { getByText } = render( );
+ render( );
- 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( );
+ const { rerender } = render( );
( getQuery as jest.Mock ).mockReturnValue( {
tab: 'test3',
@@ -133,7 +168,7 @@ describe( 'Tabs', () => {
rerender( );
- 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( );
- const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
- const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
+ render( );
+
+ 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( );
- const button = getByText( 'Test button 2' );
+ const { rerender } = render( );
+
+ 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',
diff --git a/packages/js/product-editor/src/contexts/validation-context/types.ts b/packages/js/product-editor/src/contexts/validation-context/types.ts
index 48b45f8674a..6bfce6e40a2 100644
--- a/packages/js/product-editor/src/contexts/validation-context/types.ts
+++ b/packages/js/product-editor/src/contexts/validation-context/types.ts
@@ -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;
diff --git a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
index c40a493b13c..de6e2bad218 100644
--- a/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
+++ b/packages/js/product-editor/src/contexts/validation-context/validation-provider.tsx
@@ -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,
diff --git a/packages/js/product-editor/src/hooks/use-product-url.ts b/packages/js/product-editor/src/hooks/use-product-url.ts
index 4f07183cc46..9df8488dd3d 100644
--- a/packages/js/product-editor/src/hooks/use-product-url.ts
+++ b/packages/js/product-editor/src/hooks/use-product-url.ts
@@ -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 };
}
diff --git a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
index 823e371869d..99a79e863cf 100644
--- a/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
+++ b/plugins/woocommerce-admin/client/products/hooks/use-product-entity-record.ts
@@ -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;
}
diff --git a/plugins/woocommerce-admin/client/products/product-page.tsx b/plugins/woocommerce-admin/client/products/product-page.tsx
index 566ee9746dc..fbe401fd4b5 100644
--- a/plugins/woocommerce-admin/client/products/product-page.tsx
+++ b/plugins/woocommerce-admin/client/products/product-page.tsx
@@ -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() {
}>
@@ -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 (
- <>
-
- >
- );
+ const productId = useProductEntityRecord( productIdSearchParam );
+
+ if ( ! productId ) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
}
diff --git a/plugins/woocommerce-admin/client/products/product-variation-page.tsx b/plugins/woocommerce-admin/client/products/product-variation-page.tsx
index f49ab9ec205..59786ef39f3 100644
--- a/plugins/woocommerce-admin/client/products/product-variation-page.tsx
+++ b/plugins/woocommerce-admin/client/products/product-variation-page.tsx
@@ -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 (
+
+
+
+ );
+ }
+
return (
<>
-
+
<>