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 ( <> - + <>