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:
parent
756bb8ccfa
commit
4ddfd43864
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Enhancement editor loading speed
|
|
@ -3,7 +3,7 @@
|
||||||
*/
|
*/
|
||||||
import { InnerBlocks } from '@wordpress/block-editor';
|
import { InnerBlocks } from '@wordpress/block-editor';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { createElement } from '@wordpress/element';
|
import { createElement, useEffect, useState } from '@wordpress/element';
|
||||||
import type { BlockAttributes } from '@wordpress/blocks';
|
import type { BlockAttributes } from '@wordpress/blocks';
|
||||||
import { useWooBlockProps } from '@woocommerce/block-templates';
|
import { useWooBlockProps } from '@woocommerce/block-templates';
|
||||||
|
|
||||||
|
@ -25,21 +25,30 @@ export function TabBlockEdit( {
|
||||||
context,
|
context,
|
||||||
}: ProductEditorBlockEditProps< TabBlockAttributes > ) {
|
}: ProductEditorBlockEditProps< TabBlockAttributes > ) {
|
||||||
const blockProps = useWooBlockProps( attributes );
|
const blockProps = useWooBlockProps( attributes );
|
||||||
const {
|
const { id, title, _templateBlockOrder: order, isSelected } = attributes;
|
||||||
id,
|
|
||||||
title,
|
|
||||||
_templateBlockOrder: order,
|
|
||||||
isSelected: contextIsSelected,
|
|
||||||
} = attributes;
|
|
||||||
const isSelected = context.selectedTab === id;
|
|
||||||
if ( isSelected !== contextIsSelected ) {
|
|
||||||
setAttributes( { isSelected } );
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
|
const classes = classnames( 'wp-block-woocommerce-product-tab__content', {
|
||||||
'is-selected': isSelected,
|
'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 (
|
return (
|
||||||
<div { ...blockProps }>
|
<div { ...blockProps }>
|
||||||
<TabButton id={ id } selected={ isSelected } order={ order }>
|
<TabButton id={ id } selected={ isSelected } order={ order }>
|
||||||
|
@ -51,9 +60,11 @@ export function TabBlockEdit( {
|
||||||
role="tabpanel"
|
role="tabpanel"
|
||||||
className={ classes }
|
className={ classes }
|
||||||
>
|
>
|
||||||
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
{ canRenderChildren && (
|
||||||
{ /* @ts-ignore Content only template locking does exist for this property. */ }
|
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
|
||||||
<InnerBlocks templateLock="contentOnly" />
|
/* @ts-ignore Content only template locking does exist for this property. */
|
||||||
|
<InnerBlocks templateLock="contentOnly" />
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,10 +8,11 @@ import {
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
|
lazy,
|
||||||
|
Suspense,
|
||||||
} from '@wordpress/element';
|
} 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 { uploadMedia } from '@wordpress/media-utils';
|
||||||
import { PluginArea } from '@wordpress/plugins';
|
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useLayoutTemplate } from '@woocommerce/block-templates';
|
import { useLayoutTemplate } from '@woocommerce/block-templates';
|
||||||
import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts';
|
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 { useProductTemplate } from '../../hooks/use-product-template';
|
||||||
import { PostTypeContext } from '../../contexts/post-type-context';
|
import { PostTypeContext } from '../../contexts/post-type-context';
|
||||||
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
||||||
import { ModalEditor } from '../modal-editor';
|
|
||||||
import { ProductEditorSettings } from '../editor';
|
import { ProductEditorSettings } from '../editor';
|
||||||
import { BlockEditorProps } from './types';
|
import { BlockEditorProps } from './types';
|
||||||
import { ProductTemplate } from '../../types';
|
import { ProductTemplate } from '../../types';
|
||||||
import { LoadingState } from './loading-state';
|
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(
|
function getLayoutTemplateId(
|
||||||
productTemplate: ProductTemplate | undefined,
|
productTemplate: ProductTemplate | undefined,
|
||||||
postType: string
|
postType: string
|
||||||
|
@ -76,11 +88,6 @@ export function BlockEditor( {
|
||||||
}: BlockEditorProps ) {
|
}: BlockEditorProps ) {
|
||||||
useConfirmUnsavedProductChanges( postType );
|
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.
|
* 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
|
* 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 );
|
return () => window.removeEventListener( 'scroll', wpPinMenuEvent );
|
||||||
}, [] );
|
}, [] );
|
||||||
|
|
||||||
// @ts-expect-error Type definitions are missing
|
|
||||||
const { registerShortcut } = useDispatch( keyboardShortcutsStore );
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
// @ts-expect-error Type definitions are missing
|
||||||
|
const { registerShortcut } = dispatch( keyboardShortcutsStore );
|
||||||
if ( registerShortcut ) {
|
if ( registerShortcut ) {
|
||||||
registerShortcut( {
|
registerShortcut( {
|
||||||
name: 'core/editor/save',
|
name: 'core/editor/save',
|
||||||
|
@ -109,7 +115,7 @@ export function BlockEditor( {
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
}, [ registerShortcut ] );
|
}, [] );
|
||||||
|
|
||||||
const [ settingsGlobal, setSettingsGlobal ] = useState<
|
const [ settingsGlobal, setSettingsGlobal ] = useState<
|
||||||
Partial< ProductEditorSettings > | undefined
|
Partial< ProductEditorSettings > | undefined
|
||||||
|
@ -140,6 +146,9 @@ export function BlockEditor( {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canUserCreateMedia =
|
||||||
|
select( 'core' ).canUser( 'create', 'media', '' ) !== false;
|
||||||
|
|
||||||
const mediaSettings = canUserCreateMedia
|
const mediaSettings = canUserCreateMedia
|
||||||
? {
|
? {
|
||||||
mediaUpload( {
|
mediaUpload( {
|
||||||
|
@ -165,7 +174,7 @@ export function BlockEditor( {
|
||||||
...mediaSettings,
|
...mediaSettings,
|
||||||
templateLock: 'all',
|
templateLock: 'all',
|
||||||
};
|
};
|
||||||
}, [ settingsGlobal, canUserCreateMedia ] );
|
}, [ settingsGlobal ] );
|
||||||
|
|
||||||
const { editedRecord: product } = useEntityRecord< Product >(
|
const { editedRecord: product } = useEntityRecord< Product >(
|
||||||
'postType',
|
'postType',
|
||||||
|
@ -175,10 +184,14 @@ export function BlockEditor( {
|
||||||
{ enabled: productId !== -1 }
|
{ enabled: productId !== -1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const productTemplateId = product?.meta_data?.find(
|
const productTemplateId = useMemo(
|
||||||
( metaEntry: { key: string } ) =>
|
() =>
|
||||||
metaEntry.key === '_product_template_id'
|
product?.meta_data?.find(
|
||||||
)?.value;
|
( metaEntry: { key: string } ) =>
|
||||||
|
metaEntry.key === '_product_template_id'
|
||||||
|
)?.value,
|
||||||
|
[ product?.meta_data ]
|
||||||
|
);
|
||||||
|
|
||||||
const { productTemplate } = useProductTemplate(
|
const { productTemplate } = useProductTemplate(
|
||||||
productTemplateId,
|
productTemplateId,
|
||||||
|
@ -196,8 +209,6 @@ export function BlockEditor( {
|
||||||
{ id: productId !== -1 ? productId : 0 }
|
{ id: productId !== -1 ? productId : 0 }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { updateEditorSettings } = useDispatch( 'core/editor' );
|
|
||||||
|
|
||||||
const isEditorLoading =
|
const isEditorLoading =
|
||||||
! settings ||
|
! settings ||
|
||||||
! layoutTemplate ||
|
! layoutTemplate ||
|
||||||
|
@ -217,7 +228,7 @@ export function BlockEditor( {
|
||||||
|
|
||||||
onChange( blockInstances, {} );
|
onChange( blockInstances, {} );
|
||||||
|
|
||||||
updateEditorSettings( {
|
dispatch( 'core/editor' ).updateEditorSettings( {
|
||||||
...settings,
|
...settings,
|
||||||
productTemplate,
|
productTemplate,
|
||||||
} as Partial< ProductEditorSettings > );
|
} as Partial< ProductEditorSettings > );
|
||||||
|
@ -232,21 +243,31 @@ export function BlockEditor( {
|
||||||
// the blocks by calling onChange.
|
// the blocks by calling onChange.
|
||||||
//
|
//
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [ layoutTemplate, settings, productTemplate, productId ] );
|
}, [ isEditorLoading, productId ] );
|
||||||
|
|
||||||
// Check if the Modal editor is open from the store.
|
// Check if the Modal editor is open from the store.
|
||||||
const isModalEditorOpen = useSelect( ( select ) => {
|
const isModalEditorOpen = useSelect( ( selectCore ) => {
|
||||||
return select( productEditorUiStore ).isModalEditorOpen();
|
return selectCore( productEditorUiStore ).isModalEditorOpen();
|
||||||
}, [] );
|
}, [] );
|
||||||
|
|
||||||
const { closeModalEditor } = useDispatch( productEditorUiStore );
|
if ( isEditorLoading ) {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-product-block-editor">
|
||||||
|
<LoadingState />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( isModalEditorOpen ) {
|
if ( isModalEditorOpen ) {
|
||||||
return (
|
return (
|
||||||
<ModalEditor
|
<Suspense fallback={ null }>
|
||||||
onClose={ closeModalEditor }
|
<ModalEditor
|
||||||
title={ __( 'Edit description', 'woocommerce' ) }
|
onClose={
|
||||||
/>
|
dispatch( productEditorUiStore ).closeModalEditor
|
||||||
|
}
|
||||||
|
title={ __( 'Edit description', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,17 +286,15 @@ export function BlockEditor( {
|
||||||
<BlockEditorKeyboardShortcuts.Register />
|
<BlockEditorKeyboardShortcuts.Register />
|
||||||
<BlockTools>
|
<BlockTools>
|
||||||
<ObserveTyping>
|
<ObserveTyping>
|
||||||
{ isEditorLoading ? (
|
<BlockList className="woocommerce-product-block-editor__block-list" />
|
||||||
<LoadingState />
|
|
||||||
) : (
|
|
||||||
<BlockList className="woocommerce-product-block-editor__block-list" />
|
|
||||||
) }
|
|
||||||
</ObserveTyping>
|
</ObserveTyping>
|
||||||
</BlockTools>
|
</BlockTools>
|
||||||
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }
|
{ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ }
|
||||||
<PostTypeContext.Provider value={ context.postType! }>
|
<PostTypeContext.Provider value={ context.postType! }>
|
||||||
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
|
<Suspense fallback={ null }>
|
||||||
<PluginArea scope="woocommerce-product-block-editor" />
|
{ /* @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated. */ }
|
||||||
|
<PluginArea scope="woocommerce-product-block-editor" />
|
||||||
|
</Suspense>
|
||||||
</PostTypeContext.Provider>
|
</PostTypeContext.Provider>
|
||||||
</BlockEditorProvider>
|
</BlockEditorProvider>
|
||||||
</BlockContextProvider>
|
</BlockContextProvider>
|
||||||
|
|
|
@ -37,14 +37,12 @@ import { EditorProps } from './types';
|
||||||
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
import { store as productEditorUiStore } from '../../store/product-editor-ui';
|
||||||
import { PrepublishPanel } from '../prepublish-panel/prepublish-panel';
|
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 [ isEditorLoading, setIsEditorLoading ] = useState( true );
|
||||||
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
|
const [ selectedTab, setSelectedTab ] = useState< string | null >( null );
|
||||||
|
|
||||||
const updatedLayoutContext = useExtendLayout( 'product-block-editor' );
|
const updatedLayoutContext = useExtendLayout( 'product-block-editor' );
|
||||||
|
|
||||||
const productId = product?.id || -1;
|
|
||||||
|
|
||||||
// Check if the prepublish sidebar is open from the store.
|
// Check if the prepublish sidebar is open from the store.
|
||||||
const isPrepublishPanelOpen = useSelect( ( select ) => {
|
const isPrepublishPanelOpen = useSelect( ( select ) => {
|
||||||
return select( productEditorUiStore ).isPrepublishPanelOpen();
|
return select( productEditorUiStore ).isPrepublishPanelOpen();
|
||||||
|
@ -55,11 +53,14 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<EntityProvider
|
<EntityProvider
|
||||||
kind="postType"
|
kind="postType"
|
||||||
type={ productType }
|
type={ postType }
|
||||||
id={ productId }
|
id={ productId }
|
||||||
>
|
>
|
||||||
<ShortcutProvider>
|
<ShortcutProvider>
|
||||||
<ValidationProvider initialValue={ product }>
|
<ValidationProvider
|
||||||
|
postType={ postType }
|
||||||
|
productId={ productId }
|
||||||
|
>
|
||||||
<EditorLoadingContext.Provider
|
<EditorLoadingContext.Provider
|
||||||
value={ isEditorLoading }
|
value={ isEditorLoading }
|
||||||
>
|
>
|
||||||
|
@ -67,17 +68,17 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
|
||||||
header={
|
header={
|
||||||
<Header
|
<Header
|
||||||
onTabSelect={ setSelectedTab }
|
onTabSelect={ setSelectedTab }
|
||||||
productType={ productType }
|
productType={ postType }
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
content={
|
content={
|
||||||
<>
|
<>
|
||||||
<BlockEditor
|
<BlockEditor
|
||||||
postType={ productType }
|
postType={ postType }
|
||||||
productId={ productId }
|
productId={ productId }
|
||||||
context={ {
|
context={ {
|
||||||
selectedTab,
|
selectedTab,
|
||||||
postType: productType,
|
postType,
|
||||||
postId: productId,
|
postId: productId,
|
||||||
} }
|
} }
|
||||||
setIsEditorLoading={
|
setIsEditorLoading={
|
||||||
|
@ -89,7 +90,7 @@ export function Editor( { product, productType = 'product' }: EditorProps ) {
|
||||||
actions={
|
actions={
|
||||||
isPrepublishPanelOpen && (
|
isPrepublishPanelOpen && (
|
||||||
<PrepublishPanel
|
<PrepublishPanel
|
||||||
productType={ productType }
|
productType={ postType }
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { Product } from '@woocommerce/data';
|
|
||||||
import {
|
import {
|
||||||
EditorSettings,
|
EditorSettings,
|
||||||
EditorBlockListSettings,
|
EditorBlockListSettings,
|
||||||
|
@ -29,6 +28,6 @@ export type ProductEditorSettings = Partial<
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EditorProps = {
|
export type EditorProps = {
|
||||||
product?: Pick< Product, 'id' | 'type' > | null;
|
productId: number;
|
||||||
productType?: string;
|
postType?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
Fragment,
|
Fragment,
|
||||||
|
lazy,
|
||||||
|
Suspense,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { Button, Tooltip } from '@wordpress/components';
|
import { Button, Tooltip } from '@wordpress/components';
|
||||||
|
@ -31,13 +33,18 @@ import { getHeaderTitle } from '../../utils';
|
||||||
import { MoreMenu } from './more-menu';
|
import { MoreMenu } from './more-menu';
|
||||||
import { PreviewButton } from './preview-button';
|
import { PreviewButton } from './preview-button';
|
||||||
import { SaveDraftButton } from './save-draft-button';
|
import { SaveDraftButton } from './save-draft-button';
|
||||||
import { PublishButton } from './publish-button';
|
|
||||||
import { LoadingState } from './loading-state';
|
import { LoadingState } from './loading-state';
|
||||||
import { Tabs } from '../tabs';
|
import { Tabs } from '../tabs';
|
||||||
import { HEADER_PINNED_ITEMS_SCOPE, TRACKS_SOURCE } from '../../constants';
|
import { HEADER_PINNED_ITEMS_SCOPE, TRACKS_SOURCE } from '../../constants';
|
||||||
import { useShowPrepublishChecks } from '../../hooks/use-show-prepublish-checks';
|
import { useShowPrepublishChecks } from '../../hooks/use-show-prepublish-checks';
|
||||||
import { HeaderProps, Image } from './types';
|
import { HeaderProps, Image } from './types';
|
||||||
|
|
||||||
|
const PublishButton = lazy( () =>
|
||||||
|
import( './publish-button' ).then( ( module ) => ( {
|
||||||
|
default: module.PublishButton,
|
||||||
|
} ) )
|
||||||
|
);
|
||||||
|
|
||||||
const RETURN_TO_MAIN_PRODUCT = __(
|
const RETURN_TO_MAIN_PRODUCT = __(
|
||||||
'Return to the main product',
|
'Return to the main product',
|
||||||
'woocommerce'
|
'woocommerce'
|
||||||
|
@ -254,11 +261,13 @@ export function Header( {
|
||||||
productStatus={ lastPersistedProduct?.status }
|
productStatus={ lastPersistedProduct?.status }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PublishButton
|
<Suspense fallback={ null }>
|
||||||
productType={ productType }
|
<PublishButton
|
||||||
isPrePublishPanelVisible={ showPrepublishChecks }
|
productType={ productType }
|
||||||
isMenuButton
|
isPrePublishPanelVisible={ showPrepublishChecks }
|
||||||
/>
|
isMenuButton
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
<WooHeaderItem.Slot name="product" />
|
<WooHeaderItem.Slot name="product" />
|
||||||
<PinnedItems.Slot scope={ HEADER_PINNED_ITEMS_SCOPE } />
|
<PinnedItems.Slot scope={ HEADER_PINNED_ITEMS_SCOPE } />
|
||||||
|
|
|
@ -26,6 +26,8 @@ export function PostPublishSection( { postType }: PostPublishSectionProps ) {
|
||||||
|
|
||||||
const productURL = getProductURL( isScheduled );
|
const productURL = getProductURL( isScheduled );
|
||||||
|
|
||||||
|
if ( ! productURL ) return null;
|
||||||
|
|
||||||
const CopyButton = ( { text, onCopy, children }: CopyButtonProps ) => {
|
const CopyButton = ( { text, onCopy, children }: CopyButtonProps ) => {
|
||||||
const ref = useCopyToClipboard(
|
const ref = useCopyToClipboard(
|
||||||
text,
|
text,
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { createElement, useEffect, useState } from '@wordpress/element';
|
import {
|
||||||
import { ReactElement } from 'react';
|
createElement,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
Fragment,
|
||||||
|
} from '@wordpress/element';
|
||||||
|
import { ReactElement, useMemo } from 'react';
|
||||||
import { NavigableMenu, Slot } from '@wordpress/components';
|
import { NavigableMenu, Slot } from '@wordpress/components';
|
||||||
import { Product } from '@woocommerce/data';
|
import { Product } from '@woocommerce/data';
|
||||||
import { recordEvent } from '@woocommerce/tracks';
|
import { recordEvent } from '@woocommerce/tracks';
|
||||||
import { useSelect } from '@wordpress/data';
|
import { select } from '@wordpress/data';
|
||||||
import { useEntityProp } from '@wordpress/core-data';
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore No types for this exist yet.
|
// @ts-ignore No types for this exist yet.
|
||||||
|
@ -17,7 +22,6 @@ import { navigateTo, getNewPath, getQuery } from '@woocommerce/navigation';
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { getTabTracksData } from './utils/get-tab-tracks-data';
|
import { getTabTracksData } from './utils/get-tab-tracks-data';
|
||||||
import { sortFillsByOrder } from '../../utils';
|
|
||||||
import { TABS_SLOT_NAME } from './constants';
|
import { TABS_SLOT_NAME } from './constants';
|
||||||
|
|
||||||
type TabsProps = {
|
type TabsProps = {
|
||||||
|
@ -28,6 +32,38 @@ export type TabsFillProps = {
|
||||||
onClick: ( tabId: string ) => void;
|
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 ) {
|
export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
const [ selected, setSelected ] = useState< string | null >( null );
|
const [ selected, setSelected ] = useState< string | null >( null );
|
||||||
const query = getQuery() as Record< string, string >;
|
const query = getQuery() as Record< string, string >;
|
||||||
|
@ -36,41 +72,14 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
'product',
|
'product',
|
||||||
'id'
|
'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( () => {
|
useEffect( () => {
|
||||||
if ( query.tab ) {
|
if ( query.tab ) {
|
||||||
setSelected( query.tab );
|
setSelected( query.tab );
|
||||||
|
onChange( query.tab );
|
||||||
}
|
}
|
||||||
}, [ 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(
|
function selectTabOnNavigate(
|
||||||
_childIndex: number,
|
_childIndex: number,
|
||||||
child: HTMLButtonElement
|
child: HTMLButtonElement
|
||||||
|
@ -78,6 +87,21 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
child.click();
|
child.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFills( fills: readonly ( readonly ReactElement[] )[] ) {
|
||||||
|
return (
|
||||||
|
<TabFills
|
||||||
|
fills={ fills }
|
||||||
|
onDefaultSelection={ ( tabId ) => {
|
||||||
|
if ( selected ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelected( tabId );
|
||||||
|
onChange( tabId );
|
||||||
|
} }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigableMenu
|
<NavigableMenu
|
||||||
role="tablist"
|
role="tablist"
|
||||||
|
@ -92,6 +116,17 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
navigateTo( {
|
navigateTo( {
|
||||||
url: getNewPath( { tab: tabId } ),
|
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(
|
recordEvent(
|
||||||
'product_tab_click',
|
'product_tab_click',
|
||||||
getTabTracksData( tabId, product )
|
getTabTracksData( tabId, product )
|
||||||
|
@ -101,14 +136,7 @@ export function Tabs( { onChange = () => {} }: TabsProps ) {
|
||||||
}
|
}
|
||||||
name={ TABS_SLOT_NAME }
|
name={ TABS_SLOT_NAME }
|
||||||
>
|
>
|
||||||
{ ( fills ) => {
|
{ renderFills }
|
||||||
if ( ! sortFillsByOrder ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeSetSelected( fills );
|
|
||||||
return sortFillsByOrder( fills );
|
|
||||||
} }
|
|
||||||
</Slot>
|
</Slot>
|
||||||
</NavigableMenu>
|
</NavigableMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* 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 { getQuery, navigateTo } from '@woocommerce/navigation';
|
||||||
import React, { createElement } from 'react';
|
|
||||||
import { SlotFillProvider } from '@wordpress/components';
|
import { SlotFillProvider } from '@wordpress/components';
|
||||||
import { useState } from '@wordpress/element';
|
import { useState, createElement } from '@wordpress/element';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { Tabs } from '../';
|
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.mock( '@woocommerce/block-templates', () => ( {
|
||||||
...jest.requireActual( '@woocommerce/block-templates' ),
|
...jest.requireActual( '@woocommerce/block-templates' ),
|
||||||
|
@ -40,6 +43,16 @@ function MockTabs( { onChange = jest.fn() } ) {
|
||||||
selectedTab: selected,
|
selectedTab: selected,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getAttributes( id: string ) {
|
||||||
|
return function setAttributes( {
|
||||||
|
isSelected,
|
||||||
|
}: Partial< TabBlockAttributes > ) {
|
||||||
|
if ( isSelected ) {
|
||||||
|
setSelected( id );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SlotFillProvider>
|
<SlotFillProvider>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
@ -50,27 +63,45 @@ function MockTabs( { onChange = jest.fn() } ) {
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
{ ...blockProps }
|
{ ...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
|
// 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
|
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
|
||||||
context={ mockContext }
|
context={ mockContext }
|
||||||
name="test1"
|
name="test1"
|
||||||
|
setAttributes={ getAttributes( 'test1' ) }
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
{ ...blockProps }
|
{ ...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
|
// 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
|
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
|
||||||
context={ mockContext }
|
context={ mockContext }
|
||||||
name="test2"
|
name="test2"
|
||||||
|
setAttributes={ getAttributes( 'test2' ) }
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
{ ...blockProps }
|
{ ...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
|
// 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
|
// @ts-ignore editedProduct is not used, so we can just ignore the fact that our context doesn't have it
|
||||||
context={ mockContext }
|
context={ mockContext }
|
||||||
name="test3"
|
name="test3"
|
||||||
|
setAttributes={ getAttributes( 'test3' ) }
|
||||||
/>
|
/>
|
||||||
</SlotFillProvider>
|
</SlotFillProvider>
|
||||||
);
|
);
|
||||||
|
@ -84,26 +115,30 @@ describe( 'Tabs', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should render tab buttons added to the slot', () => {
|
it( 'should render tab buttons added to the slot', () => {
|
||||||
const { queryByText } = render( <MockTabs /> );
|
render( <MockTabs /> );
|
||||||
expect( queryByText( 'Test button 1' ) ).toBeInTheDocument();
|
|
||||||
expect( queryByText( 'Test button 2' ) ).toBeInTheDocument();
|
expect( screen.queryByText( 'Test button 1' ) ).toBeInTheDocument();
|
||||||
|
expect( screen.queryByText( 'Test button 2' ) ).toBeInTheDocument();
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should set the first tab as active initially', () => {
|
it( 'should set the first tab as active initially', async () => {
|
||||||
const { queryByText } = render( <MockTabs /> );
|
render( <MockTabs /> );
|
||||||
expect( queryByText( 'Test button 1' ) ).toHaveAttribute(
|
|
||||||
|
expect( screen.queryByText( 'Test button 1' ) ).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
expect( queryByText( 'Test button 2' ) ).toHaveAttribute(
|
|
||||||
|
expect( screen.queryByText( 'Test button 2' ) ).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'false'
|
'false'
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should navigate to a new URL when a tab is clicked', () => {
|
it( 'should navigate to a new URL when a tab is clicked', () => {
|
||||||
const { getByText } = render( <MockTabs /> );
|
render( <MockTabs /> );
|
||||||
const button = getByText( 'Test button 2' );
|
|
||||||
|
const button = screen.getByText( 'Test button 2' );
|
||||||
fireEvent.click( button );
|
fireEvent.click( button );
|
||||||
|
|
||||||
expect( navigateTo ).toHaveBeenLastCalledWith( {
|
expect( navigateTo ).toHaveBeenLastCalledWith( {
|
||||||
|
@ -116,16 +151,16 @@ describe( 'Tabs', () => {
|
||||||
tab: 'test2',
|
tab: 'test2',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
const { getByText } = render( <MockTabs /> );
|
render( <MockTabs /> );
|
||||||
|
|
||||||
expect( getByText( 'Test button 2' ) ).toHaveAttribute(
|
expect( screen.getByText( 'Test button 2' ) ).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should select the tab provided on URL change', () => {
|
it( 'should select the tab provided on URL change', () => {
|
||||||
const { getByText, rerender } = render( <MockTabs /> );
|
const { rerender } = render( <MockTabs /> );
|
||||||
|
|
||||||
( getQuery as jest.Mock ).mockReturnValue( {
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
tab: 'test3',
|
tab: 'test3',
|
||||||
|
@ -133,7 +168,7 @@ describe( 'Tabs', () => {
|
||||||
|
|
||||||
rerender( <MockTabs /> );
|
rerender( <MockTabs /> );
|
||||||
|
|
||||||
expect( getByText( 'Test button 3' ) ).toHaveAttribute(
|
expect( screen.getByText( 'Test button 3' ) ).toHaveAttribute(
|
||||||
'aria-selected',
|
'aria-selected',
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
|
@ -155,20 +190,30 @@ describe( 'Tabs', () => {
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should add a class to the initially selected tab panel', async () => {
|
it( 'should add a class to the initially selected tab panel', async () => {
|
||||||
const { getByRole } = render( <MockTabs /> );
|
render( <MockTabs /> );
|
||||||
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',
|
||||||
|
} );
|
||||||
|
|
||||||
expect( panel1.classList ).toContain( 'is-selected' );
|
expect( panel1.classList ).toContain( 'is-selected' );
|
||||||
expect( panel2.classList ).not.toContain( 'is-selected' );
|
expect( panel2.classList ).not.toContain( 'is-selected' );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
it( 'should add a class to the newly selected tab panel', async () => {
|
it( 'should add a class to the newly selected tab panel', async () => {
|
||||||
const { getByText, getByRole, rerender } = render( <MockTabs /> );
|
const { rerender } = render( <MockTabs /> );
|
||||||
const button = getByText( 'Test button 2' );
|
|
||||||
|
const button = screen.getByText( 'Test button 2' );
|
||||||
fireEvent.click( button );
|
fireEvent.click( button );
|
||||||
const panel1 = getByRole( 'tabpanel', { name: 'Test button 1' } );
|
const panel1 = screen.getByRole( 'tabpanel', {
|
||||||
const panel2 = getByRole( 'tabpanel', { name: 'Test button 2' } );
|
name: 'Test button 1',
|
||||||
|
} );
|
||||||
|
const panel2 = screen.getByRole( 'tabpanel', {
|
||||||
|
name: 'Test button 2',
|
||||||
|
} );
|
||||||
|
|
||||||
( getQuery as jest.Mock ).mockReturnValue( {
|
( getQuery as jest.Mock ).mockReturnValue( {
|
||||||
tab: 'test2',
|
tab: 'test2',
|
||||||
|
|
|
@ -19,8 +19,9 @@ export type ValidationContextProps< T > = {
|
||||||
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
|
validateAll( newData?: Partial< T > ): Promise< ValidationErrors >;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValidationProviderProps< T > = {
|
export type ValidationProviderProps = {
|
||||||
initialValue?: T;
|
postType: string;
|
||||||
|
productId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValidationError = string | undefined;
|
export type ValidationError = string | undefined;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { useEntityRecord } from '@wordpress/core-data';
|
||||||
import { createElement, useRef, useState } from '@wordpress/element';
|
import { createElement, useRef, useState } from '@wordpress/element';
|
||||||
import { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -17,12 +18,18 @@ import { ValidationContext } from './validation-context';
|
||||||
import { findFirstInvalidElement } from './helpers';
|
import { findFirstInvalidElement } from './helpers';
|
||||||
|
|
||||||
export function ValidationProvider< T >( {
|
export function ValidationProvider< T >( {
|
||||||
initialValue,
|
postType,
|
||||||
|
productId,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren< ValidationProviderProps< T > > ) {
|
}: PropsWithChildren< ValidationProviderProps > ) {
|
||||||
const validatorsRef = useRef< Record< string, Validator< T > > >( {} );
|
const validatorsRef = useRef< Record< string, Validator< T > > >( {} );
|
||||||
const fieldRefs = useRef< Record< string, HTMLElement > >( {} );
|
const fieldRefs = useRef< Record< string, HTMLElement > >( {} );
|
||||||
const [ errors, setErrors ] = useState< ValidationErrors >( {} );
|
const [ errors, setErrors ] = useState< ValidationErrors >( {} );
|
||||||
|
const { record: initialValue } = useEntityRecord< T >(
|
||||||
|
'postType',
|
||||||
|
postType,
|
||||||
|
productId
|
||||||
|
);
|
||||||
|
|
||||||
function registerValidator(
|
function registerValidator(
|
||||||
validatorId: string,
|
validatorId: string,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { useEntityProp } from '@wordpress/core-data';
|
import { useEntityProp } from '@wordpress/core-data';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from '@wordpress/element';
|
||||||
|
|
||||||
export function useProductURL( productType: string ) {
|
export function useProductURL( productType: string ) {
|
||||||
const [ permalink ] = useEntityProp< string >(
|
const [ permalink ] = useEntityProp< string >(
|
||||||
|
@ -10,15 +10,19 @@ export function useProductURL( productType: string ) {
|
||||||
productType,
|
productType,
|
||||||
'permalink'
|
'permalink'
|
||||||
);
|
);
|
||||||
|
|
||||||
const getProductURL = useCallback(
|
const getProductURL = useCallback(
|
||||||
( isPreview: boolean ) => {
|
( isPreview: boolean ) => {
|
||||||
const productURL = new URL( permalink ) as URL | undefined;
|
if ( ! permalink ) return undefined;
|
||||||
|
|
||||||
|
const productURL = new URL( permalink );
|
||||||
if ( isPreview ) {
|
if ( isPreview ) {
|
||||||
productURL?.searchParams.append( 'preview', 'true' );
|
productURL.searchParams.append( 'preview', 'true' );
|
||||||
}
|
}
|
||||||
return productURL?.toString() || '';
|
return productURL.toString();
|
||||||
},
|
},
|
||||||
[ permalink ]
|
[ permalink ]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { getProductURL };
|
return { getProductURL };
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,40 +2,39 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
|
import { AUTO_DRAFT_NAME } from '@woocommerce/product-editor';
|
||||||
import { Product } from '@woocommerce/data';
|
import { type Product } from '@woocommerce/data';
|
||||||
import { useDispatch, resolveSelect } from '@wordpress/data';
|
import { dispatch } from '@wordpress/data';
|
||||||
import { useEffect, useState } from '@wordpress/element';
|
import { useEffect, useState } from '@wordpress/element';
|
||||||
|
|
||||||
export function useProductEntityRecord(
|
export function useProductEntityRecord(
|
||||||
productId: string | undefined
|
productId: string | undefined
|
||||||
): Product | undefined {
|
): number | undefined {
|
||||||
const { saveEntityRecord } = useDispatch( 'core' );
|
const [ id, setId ] = useState< number | undefined >( undefined );
|
||||||
const [ product, setProduct ] = useState< Product | undefined >(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
const getRecordPromise: Promise< Product > = productId
|
if ( productId ) {
|
||||||
? resolveSelect( 'core' ).getEntityRecord< Product >(
|
setId( Number.parseInt( productId, 10 ) );
|
||||||
'postType',
|
return;
|
||||||
'product',
|
}
|
||||||
Number.parseInt( productId, 10 )
|
|
||||||
)
|
const createProductPromise = dispatch( 'core' ).saveEntityRecord(
|
||||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
'postType',
|
||||||
// @ts-ignore Incorrect types.
|
'product',
|
||||||
( saveEntityRecord( 'postType', 'product', {
|
{
|
||||||
title: AUTO_DRAFT_NAME,
|
title: AUTO_DRAFT_NAME,
|
||||||
status: 'auto-draft',
|
status: 'auto-draft',
|
||||||
} ) as Promise< Product > );
|
}
|
||||||
getRecordPromise
|
) as never as Promise< Product >;
|
||||||
.then( ( autoDraftProduct: Product ) => {
|
|
||||||
setProduct( autoDraftProduct );
|
createProductPromise
|
||||||
} )
|
.then( ( autoDraftProduct: Product ) =>
|
||||||
|
setId( autoDraftProduct.id )
|
||||||
|
)
|
||||||
.catch( ( e ) => {
|
.catch( ( e ) => {
|
||||||
setProduct( undefined );
|
setId( undefined );
|
||||||
throw e;
|
throw e;
|
||||||
} );
|
} );
|
||||||
}, [ productId ] );
|
}, [ productId ] );
|
||||||
|
|
||||||
return product;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import React, { lazy, Suspense, useContext, useEffect } from 'react';
|
||||||
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
|
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { WooFooterItem } from '@woocommerce/admin-layout';
|
import { WooFooterItem } from '@woocommerce/admin-layout';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -39,9 +40,7 @@ const ProductMVPFeedbackModalContainer = lazy( () =>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function ProductPage() {
|
export default function ProductPage() {
|
||||||
const { productId } = useParams();
|
const { productId: productIdSearchParam } = useParams();
|
||||||
|
|
||||||
const product = useProductEntityRecord( productId );
|
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
document.body.classList.add( 'is-product-editor' );
|
document.body.classList.add( 'is-product-editor' );
|
||||||
|
@ -70,8 +69,11 @@ export default function ProductPage() {
|
||||||
<Suspense fallback={ <Spinner /> }>
|
<Suspense fallback={ <Spinner /> }>
|
||||||
<ProductMVPFeedbackModalContainer
|
<ProductMVPFeedbackModalContainer
|
||||||
productId={
|
productId={
|
||||||
productId
|
productIdSearchParam
|
||||||
? parseInt( productId, 10 )
|
? Number.parseInt(
|
||||||
|
productIdSearchParam,
|
||||||
|
10
|
||||||
|
)
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -91,17 +93,17 @@ export default function ProductPage() {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.body.classList.remove( 'is-product-editor' );
|
document.body.classList.remove( 'is-product-editor' );
|
||||||
unregisterPlugin( 'wc-admin-more-menu' );
|
unregisterPlugin( 'wc-admin-product-editor' );
|
||||||
unregisterBlocks();
|
unregisterBlocks();
|
||||||
};
|
};
|
||||||
}, [ productId ] );
|
}, [ productIdSearchParam ] );
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
function trackViewEvents() {
|
function trackViewEvents() {
|
||||||
if ( productId ) {
|
if ( productIdSearchParam ) {
|
||||||
recordEvent( 'product_edit_view', {
|
recordEvent( 'product_edit_view', {
|
||||||
source: TRACKS_SOURCE,
|
source: TRACKS_SOURCE,
|
||||||
product_id: productId,
|
product_id: productIdSearchParam,
|
||||||
} );
|
} );
|
||||||
} else {
|
} else {
|
||||||
recordEvent( 'product_add_view', {
|
recordEvent( 'product_add_view', {
|
||||||
|
@ -109,12 +111,20 @@ export default function ProductPage() {
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[ productId ]
|
[ productIdSearchParam ]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const productId = useProductEntityRecord( productIdSearchParam );
|
||||||
<>
|
|
||||||
<Editor product={ product } />
|
if ( ! productId ) {
|
||||||
</>
|
return (
|
||||||
);
|
<div className="woocommerce-layout__loading">
|
||||||
|
<Spinner
|
||||||
|
aria-label={ __( 'Creating the product', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Editor productId={ productId } />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import { useEffect } from '@wordpress/element';
|
||||||
import { WooFooterItem } from '@woocommerce/admin-layout';
|
import { WooFooterItem } from '@woocommerce/admin-layout';
|
||||||
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
|
import { registerPlugin, unregisterPlugin } from '@wordpress/plugins';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { Spinner } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
|
@ -78,9 +80,19 @@ export default function ProductPage() {
|
||||||
[ productId ]
|
[ productId ]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ( ! variation ) {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-layout__loading">
|
||||||
|
<Spinner
|
||||||
|
aria-label={ __( 'Creating the product', 'woocommerce' ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Editor product={ variation } productType="product_variation" />
|
<Editor productId={ variation.id } postType="product_variation" />
|
||||||
<WooFooterItem order={ 0 }>
|
<WooFooterItem order={ 0 }>
|
||||||
<>
|
<>
|
||||||
<VariationSwitcherFooter
|
<VariationSwitcherFooter
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: minor
|
||||||
|
Type: enhancement
|
||||||
|
|
||||||
|
Enhancement editor loading speed
|
Loading…
Reference in New Issue