Add customize store assembler hub (#39843)

* Add @wordpress dependencies for customize store task

* Update webpack config to bundle wp edit-site package instead of using external

* Add customize-store task list item fill

* Update CustomizeStore task to load editor scripts and settings

* Update customize store routing path

Use /* since we want to match any path that starts with customize-store

* Add assembler-hub

* Ignore some wp packages from syncpack for customize store assembler hub

We need to use specific versions of these packages for the customize store
"@wordpress/interface", "@wordpress/router", "@wordpress/edit-site"

* Add changefile(s) from automation for the following project(s): woocommerce

* Tweak style

* Use CustomizeStoreContext and send xstate event

* Update assembler-hub style

* Fix nav width

---------

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chi-Hsuan Huang 2023-08-28 09:28:05 +08:00 committed by GitHub
parent 4dc745cc37
commit 56f4ad623f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 4475 additions and 355 deletions

View File

@ -112,7 +112,10 @@
{
"dependencies": [
"@wordpress/block**",
"@wordpress/viewport"
"@wordpress/viewport",
"@wordpress/interface",
"@wordpress/router",
"@wordpress/edit-site"
],
"packages": [
"@woocommerce/product-editor",

View File

@ -0,0 +1,253 @@
// Reference: https://github.com/WordPress/gutenberg/blob/release/16.4/packages/block-editor/src/components/block-preview/auto.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose';
import { useMemo } from '@wordpress/element';
import { Disabled } from '@wordpress/components';
import {
__unstableEditorStyles as EditorStyles,
__unstableIframe as Iframe,
BlockList,
MemoizedBlockList,
// @ts-ignore No types for this exist yet.
} from '@wordpress/block-editor';
const MAX_HEIGHT = 2000;
// @ts-ignore No types for this exist yet.
const { Provider: DisabledProvider } = Disabled.Context;
export type ScaledBlockPreviewProps = {
viewportWidth?: number;
containerWidth: number;
minHeight?: number;
settings: {
styles: string[];
[ key: string ]: unknown;
};
additionalStyles: string;
onClickNavigationItem: ( event: MouseEvent ) => void;
};
function ScaledBlockPreview( {
viewportWidth,
containerWidth,
minHeight,
settings,
additionalStyles,
onClickNavigationItem,
}: ScaledBlockPreviewProps ) {
if ( ! viewportWidth ) {
viewportWidth = containerWidth;
}
const [ contentResizeListener, { height: contentHeight } ] =
useResizeObserver();
// Avoid scrollbars for pattern previews.
const editorStyles = useMemo( () => {
return [
{
css: 'body{height:auto;overflow:hidden;border:none;padding:0;}',
__unstableType: 'presets',
},
...settings.styles,
];
}, [ settings.styles ] );
// Initialize on render instead of module top level, to avoid circular dependency issues.
const RenderedBlockList = MemoizedBlockList || pure( BlockList );
const scale = containerWidth / viewportWidth;
return (
<DisabledProvider value={ true }>
<Iframe
contentRef={ useRefEffect( ( bodyElement: HTMLBodyElement ) => {
const {
ownerDocument: { documentElement },
} = bodyElement;
documentElement.classList.add(
'block-editor-block-preview__content-iframe'
);
documentElement.style.position = 'absolute';
documentElement.style.width = '100%';
// Necessary for contentResizeListener to work.
bodyElement.style.boxSizing = 'border-box';
bodyElement.style.position = 'absolute';
bodyElement.style.width = '100%';
let navigationContainers: NodeListOf< HTMLDivElement >;
let siteTitles: NodeListOf< HTMLAnchorElement >;
const onClickNavigation = ( event: MouseEvent ) => {
event.preventDefault();
onClickNavigationItem( event );
};
const onMouseMove = ( event: MouseEvent ) => {
event.stopImmediatePropagation();
};
const possiblyRemoveAllListeners = () => {
bodyElement.removeEventListener(
'mousemove',
onMouseMove,
false
);
if ( navigationContainers ) {
navigationContainers.forEach( ( element ) => {
element.removeEventListener(
'click',
onClickNavigation
);
} );
}
if ( siteTitles ) {
siteTitles.forEach( ( element ) => {
element.removeEventListener(
'click',
onClickNavigation
);
} );
}
};
const onChange = () => {
// Remove contenteditable and inert attributes from editable elements so that users can click on navigation links.
bodyElement
.querySelectorAll(
'.block-editor-rich-text__editable[contenteditable="true"]'
)
.forEach( ( element ) => {
element.removeAttribute( 'contenteditable' );
} );
bodyElement
.querySelectorAll( '*[inert="true"]' )
.forEach( ( element ) => {
element.removeAttribute( 'inert' );
} );
possiblyRemoveAllListeners();
navigationContainers = bodyElement.querySelectorAll(
'.wp-block-navigation__container'
);
navigationContainers.forEach( ( element ) => {
element.addEventListener(
'click',
onClickNavigation,
true
);
} );
siteTitles = bodyElement.querySelectorAll(
'.wp-block-site-title a'
);
siteTitles.forEach( ( element ) => {
element.addEventListener(
'click',
onClickNavigation,
true
);
} );
};
// Stop mousemove event listener to disable block tool insertion feature.
bodyElement.addEventListener(
'mousemove',
onMouseMove,
true
);
const observer = new window.MutationObserver( onChange );
observer.observe( bodyElement, {
attributes: true,
characterData: false,
subtree: true,
childList: true,
} );
return () => {
observer.disconnect();
possiblyRemoveAllListeners();
};
}, [] ) }
aria-hidden
tabIndex={ -1 }
style={ {
width: viewportWidth,
height: contentHeight,
// This is a catch-all max-height for patterns.
// Reference: https://github.com/WordPress/gutenberg/pull/38175.
maxHeight: MAX_HEIGHT,
minHeight:
scale !== 0 && scale < 1 && minHeight
? minHeight / scale
: minHeight,
} }
>
<EditorStyles styles={ editorStyles } />
<style>
{ `
.block-editor-block-list__block::before,
.is-selected::after,
.is-hovered::after,
.block-list-appender {
display: none !important;
}
.block-editor-block-list__block.is-selected {
box-shadow: none !important;
}
.block-editor-rich-text__editable {
pointer-events: none !important;
}
.wp-block-site-title .block-editor-rich-text__editable {
pointer-events: all !important;
}
.wp-block-navigation .wp-block-pages-list__item__link {
pointer-events: all !important;
cursor: pointer !important;
}
${ additionalStyles }
` }
</style>
{ contentResizeListener }
<RenderedBlockList renderAppender={ false } />
</Iframe>
</DisabledProvider>
);
}
export const AutoHeightBlockPreview = (
props: Omit< ScaledBlockPreviewProps, 'containerWidth' >
) => {
const [ containerResizeListener, { width: containerWidth } ] =
useResizeObserver();
return (
<>
<div style={ { position: 'relative', width: '100%', height: 0 } }>
{ containerResizeListener }
</div>
<div className="auto-block-preview__container">
{ !! containerWidth && (
<ScaledBlockPreview
{ ...props }
containerWidth={ containerWidth }
/>
) }
</div>
</>
);
};

View File

@ -0,0 +1,138 @@
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classNames from 'classnames';
import { useSelect } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { useEntityRecords, useEntityBlockEditor } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import useSiteEditorSettings from '@wordpress/edit-site/build-module/components/block-editor/use-site-editor-settings';
import { BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import BlockPreview from './block-preview';
const { useHistory, useLocation } = unlock( routerPrivateApis );
type Page = {
link: string;
title: { rendered: string; raw: string };
[ key: string ]: unknown;
};
// We only show the edit option when page count is <= MAX_PAGE_COUNT
// Performance of Navigation Links is not good past this value.
const MAX_PAGE_COUNT = 100;
export const BlockEditor = ( {} ) => {
const history = useHistory();
const location = useLocation();
const settings = useSiteEditorSettings();
const { templateType } = useSelect( ( select ) => {
const { getEditedPostType } = unlock( select( editSiteStore ) );
return {
templateType: getEditedPostType(),
};
}, [] );
const [ blocks ]: [ BlockInstance[] ] = useEntityBlockEditor(
'postType',
templateType
);
// // See packages/block-library/src/page-list/edit.js.
const { records: pages } = useEntityRecords( 'postType', 'page', {
per_page: MAX_PAGE_COUNT,
_fields: [ 'id', 'link', 'menu_order', 'parent', 'title', 'type' ],
// TODO: When https://core.trac.wordpress.org/ticket/39037 REST API support for multiple orderby
// values is resolved, update 'orderby' to [ 'menu_order', 'post_title' ] to provide a consistent
// sort.
orderby: 'menu_order',
order: 'asc',
} );
const onClickNavigationItem = ( event: MouseEvent ) => {
const clickedPage =
pages.find(
( page: Page ) =>
page.link === ( event.target as HTMLAnchorElement ).href
) ||
// Fallback to page title if the link is not found. This is needed for a bug in the block library
// See https://github.com/woocommerce/team-ghidorah/issues/253#issuecomment-1665106817
pages.find(
( page: Page ) =>
page.title.rendered ===
( event.target as HTMLAnchorElement ).innerText
);
if ( clickedPage ) {
history.push( {
...location.params,
postId: clickedPage.id,
postType: 'page',
} );
} else {
// Home page
const { postId, postType, ...params } = location.params;
history.push( {
...params,
} );
}
};
return (
<div className="woocommerce-customize-store__block-editor">
{ blocks.map( ( block, index ) => {
// Add padding to the top and bottom of the block preview.
let additionalStyles = '';
let hasActionBar = false;
switch ( true ) {
case index === 0:
// header
additionalStyles = `
.editor-styles-wrapper{ padding-top: var(--wp--style--root--padding-top) };'
`;
break;
case index === blocks.length - 1:
// footer
additionalStyles = `
.editor-styles-wrapper{ padding-bottom: var(--wp--style--root--padding-bottom) };
`;
break;
default:
hasActionBar = true;
}
return (
<div
key={ block.clientId }
className={ classNames(
'woocommerce-block-preview-container',
{
'has-action-menu': hasActionBar,
}
) }
>
<BlockPreview
blocks={ block }
settings={ settings }
additionalStyles={ additionalStyles }
onClickNavigationItem={ onClickNavigationItem }
/>
</div>
);
} ) }
</div>
);
};

View File

@ -0,0 +1,40 @@
// Reference: https://github.com/WordPress/gutenberg/blob/release/16.4/packages/block-editor/src/components/block-preview/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
// @ts-ignore No types for this exist yet.
import { BlockEditorProvider } from '@wordpress/block-editor';
import { memo, useMemo } from '@wordpress/element';
import { BlockInstance } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import {
AutoHeightBlockPreview,
ScaledBlockPreviewProps,
} from './auto-block-preview';
export const BlockPreview = ( {
blocks,
settings,
...props
}: {
blocks: BlockInstance | BlockInstance[];
settings: Record< string, unknown >;
} & Omit< ScaledBlockPreviewProps, 'containerWidth' > ) => {
const renderedBlocks = useMemo(
() => ( Array.isArray( blocks ) ? blocks : [ blocks ] ),
[ blocks ]
);
return (
<BlockEditorProvider value={ renderedBlocks } settings={ settings }>
<AutoHeightBlockPreview settings={ settings } { ...props } />
</BlockEditorProvider>
);
};
export default memo( BlockPreview );

View File

@ -0,0 +1,100 @@
// Reference: https://github.com/WordPress/gutenberg/tree/v16.4.0/packages/edit-site/src/components/editor/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classnames from 'classnames';
import { useMemo } from '@wordpress/element';
// @ts-ignore No types for this exist yet.
import { EntityProvider } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { InterfaceSkeleton } from '@wordpress/interface';
import { useSelect, useDispatch } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { BlockContextProvider } from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
// @ts-ignore No types for this exist yet.
import CanvasSpinner from '@wordpress/edit-site/build-module/components/canvas-spinner';
// @ts-ignore No types for this exist yet.
import useEditedEntityRecord from '@wordpress/edit-site/build-module/components/use-edited-entity-record';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { GlobalStylesRenderer } from '@wordpress/edit-site/build-module/components/global-styles-renderer';
/**
* Internal dependencies
*/
import { BlockEditor } from './block-editor';
export const Editor = ( { isLoading }: { isLoading: boolean } ) => {
const { record: template } = useEditedEntityRecord();
const { id: templateId, type: templateType } = template;
const { context, hasPageContentFocus } = useSelect( ( select ) => {
const {
getEditedPostContext,
hasPageContentFocus: _hasPageContentFocus,
} = unlock( select( editSiteStore ) );
// The currently selected entity to display.
// Typically template or template part in the site editor.
return {
context: getEditedPostContext(),
hasPageContentFocus: _hasPageContentFocus,
};
}, [] );
// @ts-ignore No types for this exist yet.
const { setEditedPostContext } = useDispatch( editSiteStore );
const blockContext = useMemo( () => {
const { postType, postId, ...nonPostFields } = context ?? {};
return {
...( hasPageContentFocus ? context : nonPostFields ),
queryContext: [
context?.queryContext || { page: 1 },
( newQueryContext: Record< string, unknown > ) =>
setEditedPostContext( {
...context,
queryContext: {
...context?.queryContext,
...newQueryContext,
},
} ),
],
};
}, [ hasPageContentFocus, context, setEditedPostContext ] );
return (
<>
{ isLoading ? <CanvasSpinner /> : null }
<EntityProvider kind="root" type="site">
<EntityProvider
kind="postType"
type={ templateType }
id={ templateId }
>
<BlockContextProvider value={ blockContext }>
<InterfaceSkeleton
enableRegionNavigation={ false }
className={ classnames(
'woocommerce-customize-store__edit-site-editor',
'edit-site-editor__interface-skeleton',
{
'show-icon-labels': false,
'is-loading': isLoading,
}
) }
content={
<>
<GlobalStylesRenderer />
<BlockEditor />
</>
}
/>
</BlockContextProvider>
</EntityProvider>
</EntityProvider>
</>
);
};

View File

@ -1 +1,142 @@
export type events = { type: 'FINISH_CUSTOMIZATION' };
// Reference: https://github.com/WordPress/gutenberg/tree/v16.4.0/packages/edit-site/src/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { useEffect, createContext } from '@wordpress/element';
import { dispatch, useDispatch } from '@wordpress/data';
import {
__experimentalFetchLinkSuggestions as fetchLinkSuggestions,
__experimentalFetchUrlData as fetchUrlData,
// @ts-ignore No types for this exist yet.
} from '@wordpress/core-data';
// eslint-disable-next-line @woocommerce/dependency-group
import {
registerCoreBlocks,
__experimentalGetCoreBlocks,
// @ts-ignore No types for this exist yet.
} from '@wordpress/block-library';
// @ts-ignore No types for this exist yet.
import { getBlockType, store as blocksStore } from '@wordpress/blocks';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { ShortcutProvider } from '@wordpress/keyboard-shortcuts';
// @ts-ignore No types for this exist yet.
import { store as preferencesStore } from '@wordpress/preferences';
// @ts-ignore No types for this exist yet.
import { store as editorStore } from '@wordpress/editor';
// @ts-ignore No types for this exist yet.
import { store as editSiteStore } from '@wordpress/edit-site/build-module/store';
// @ts-ignore No types for this exist yet.
import { GlobalStylesProvider } from '@wordpress/edit-site/build-module/components/global-styles/global-styles-provider';
/**
* Internal dependencies
*/
import { CustomizeStoreComponent } from '../types';
import { Layout } from './layout';
import './style.scss';
const { RouterProvider } = unlock( routerPrivateApis );
type CustomizeStoreComponentProps = Parameters< CustomizeStoreComponent >[ 0 ];
export const CustomizeStoreContext = createContext< {
sendEvent: CustomizeStoreComponentProps[ 'sendEvent' ];
context: Partial< CustomizeStoreComponentProps[ 'context' ] >;
} >( {
sendEvent: () => {},
context: {},
} );
export type events =
| { type: 'FINISH_CUSTOMIZATION' }
| { type: 'GO_BACK_TO_DESIGN_WITH_AI' };
export const AssemblerHub: CustomizeStoreComponent = ( props ) => {
const { setCanvasMode } = unlock( useDispatch( editSiteStore ) );
useEffect( () => {
if ( ! window.wcBlockSettings ) {
// eslint-disable-next-line no-console
console.warn(
'window.blockSettings not found. Skipping initialization.'
);
return;
}
// Set up the block editor settings.
const settings = window.wcBlockSettings;
settings.__experimentalFetchLinkSuggestions = (
search: string,
searchOptions: {
isInitialSuggestions: boolean;
type: 'attachment' | 'post' | 'term' | 'post-format';
subtype: string;
page: number;
perPage: number;
}
) => fetchLinkSuggestions( search, searchOptions, settings );
settings.__experimentalFetchRichUrlData = fetchUrlData;
// @ts-ignore No types for this exist yet.
dispatch( blocksStore ).__experimentalReapplyBlockTypeFilters();
const coreBlocks = __experimentalGetCoreBlocks().filter(
( { name }: { name: string } ) =>
name !== 'core/freeform' && ! getBlockType( name )
);
registerCoreBlocks( coreBlocks );
// @ts-ignore No types for this exist yet.
dispatch( blocksStore ).setFreeformFallbackBlockName( 'core/html' );
// @ts-ignore No types for this exist yet.
dispatch( preferencesStore ).setDefaults( 'core/edit-site', {
editorMode: 'visual',
fixedToolbar: false,
focusMode: false,
distractionFree: false,
keepCaretInsideBlock: false,
welcomeGuide: false,
welcomeGuideStyles: false,
welcomeGuidePage: false,
welcomeGuideTemplate: false,
showListViewByDefault: false,
showBlockBreadcrumbs: true,
} );
// @ts-ignore No types for this exist yet.
dispatch( editSiteStore ).updateSettings( settings );
// @ts-ignore No types for this exist yet.
dispatch( editorStore ).updateEditorSettings( {
defaultTemplateTypes: settings.defaultTemplateTypes,
defaultTemplatePartAreas: settings.defaultTemplatePartAreas,
} );
// Prevent the default browser action for files dropped outside of dropzones.
window.addEventListener(
'dragover',
( e ) => e.preventDefault(),
false
);
window.addEventListener( 'drop', ( e ) => e.preventDefault(), false );
setCanvasMode( 'view' );
}, [ setCanvasMode ] );
return (
<CustomizeStoreContext.Provider value={ props }>
<ShortcutProvider style={ { height: '100%' } }>
<GlobalStylesProvider>
<RouterProvider>
<Layout />
</RouterProvider>
</GlobalStylesProvider>
</ShortcutProvider>
</CustomizeStoreContext.Provider>
);
};

View File

@ -0,0 +1,162 @@
// Reference: https://github.com/WordPress/gutenberg/tree/v16.4.0/packages/edit-site/src/components/layout/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classnames from 'classnames';
import { useState } from '@wordpress/element';
import {
useReducedMotion,
useResizeObserver,
useViewportMatch,
} from '@wordpress/compose';
import { __ } from '@wordpress/i18n';
import {
// @ts-ignore No types for this exist yet.
__unstableMotion as motion,
} from '@wordpress/components';
import {
privateApis as blockEditorPrivateApis,
// @ts-ignore No types for this exist yet.
} from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import ResizableFrame from '@wordpress/edit-site/build-module/components/resizable-frame';
// @ts-ignore No types for this exist yet.
import useInitEditedEntityFromURL from '@wordpress/edit-site/build-module/components/sync-state-with-url/use-init-edited-entity-from-url';
// @ts-ignore No types for this exist yet.
import { useIsSiteEditorLoading } from '@wordpress/edit-site/build-module/components/layout/hooks';
// @ts-ignore No types for this exist yet.
import ErrorBoundary from '@wordpress/edit-site/build-module/components/error-boundary';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { NavigableRegion } from '@wordpress/interface';
/**
* Internal dependencies
*/
import { Editor } from './editor';
import Sidebar from './sidebar';
import { SiteHub } from './site-hub';
const { useGlobalStyle } = unlock( blockEditorPrivateApis );
const ANIMATION_DURATION = 0.5;
export const Layout = () => {
// This ensures the edited entity id and type are initialized properly.
useInitEditedEntityFromURL();
const isMobileViewport = useViewportMatch( 'medium', '<' );
const disableMotion = useReducedMotion();
const [ canvasResizer, canvasSize ] = useResizeObserver();
const isEditorLoading = useIsSiteEditorLoading();
const [ isResizableFrameOversized, setIsResizableFrameOversized ] =
useState( false );
const [ backgroundColor ] = useGlobalStyle( 'color.background' );
const [ gradientValue ] = useGlobalStyle( 'color.gradient' );
return (
<div className={ classnames( 'edit-site-layout' ) }>
<motion.div
className="edit-site-layout__header-container"
animate={ 'view' }
>
<SiteHub
as={ motion.div }
variants={ {
view: { x: 0 },
} }
isTransparent={ isResizableFrameOversized }
className="edit-site-layout__hub"
/>
</motion.div>
<div className="edit-site-layout__content">
<NavigableRegion
ariaLabel={ __( 'Navigation', 'woocommerce' ) }
className="edit-site-layout__sidebar-region"
>
<motion.div
animate={ { opacity: 1 } }
transition={ {
type: 'tween',
duration:
// Disable transitiont in mobile to emulate a full page transition.
disableMotion || isMobileViewport
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
className="edit-site-layout__sidebar"
>
<Sidebar />
</motion.div>
</NavigableRegion>
{ ! isMobileViewport && (
<div
className={ classnames(
'edit-site-layout__canvas-container'
) }
>
{ canvasResizer }
{ !! canvasSize.width && (
<motion.div
whileHover={ {
scale: 1.005,
transition: {
duration: disableMotion ? 0 : 0.5,
ease: 'easeOut',
},
} }
initial={ false }
layout="position"
className={ classnames(
'edit-site-layout__canvas',
{
'is-right-aligned':
isResizableFrameOversized,
}
) }
transition={ {
type: 'tween',
duration: disableMotion
? 0
: ANIMATION_DURATION,
ease: 'easeOut',
} }
>
<ErrorBoundary>
<ResizableFrame
isReady={ ! isEditorLoading }
isFullWidth={ false }
defaultSize={ {
width:
canvasSize.width -
24 /* $canvas-padding */,
height: canvasSize.height,
} }
isOversized={
isResizableFrameOversized
}
setIsOversized={
setIsResizableFrameOversized
}
innerContentStyle={ {
background:
gradientValue ??
backgroundColor,
} }
>
<Editor isLoading={ isEditorLoading } />
</ResizableFrame>
</ErrorBoundary>
</motion.div>
) }
</div>
) }
</div>
</div>
);
};

View File

@ -0,0 +1,144 @@
// Reference: https://github.com/WordPress/gutenberg/blob/v16.4.0/packages/edit-site/src/components/sidebar/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { memo, useRef, useEffect } from '@wordpress/element';
import {
// @ts-ignore No types for this exist yet.
__experimentalNavigatorProvider as NavigatorProvider,
// @ts-ignore No types for this exist yet.
__experimentalNavigatorScreen as NavigatorScreen,
// @ts-ignore No types for this exist yet.
__experimentalUseNavigator as useNavigator,
} from '@wordpress/components';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
*/
import { SidebarNavigationScreenMain } from './sidebar-navigation-screen-main';
import { SidebarNavigationScreenColorPalette } from './sidebar-navigation-screen-color-palette';
import { SidebarNavigationScreenTypography } from './sidebar-navigation-screen-typography';
import { SidebarNavigationScreenHeader } from './sidebar-navigation-screen-header';
import { SidebarNavigationScreenHomepage } from './sidebar-navigation-screen-homepage';
import { SidebarNavigationScreenFooter } from './sidebar-navigation-screen-footer';
import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages';
import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
import { SaveHub } from './save-hub';
const { useLocation, useHistory } = unlock( routerPrivateApis );
function isSubset(
subset: {
[ key: string ]: string | undefined;
},
superset: {
[ key: string ]: string | undefined;
}
) {
return Object.entries( subset ).every( ( [ key, value ] ) => {
return superset[ key ] === value;
} );
}
function useSyncPathWithURL() {
const history = useHistory();
const { params: urlParams } = useLocation();
const { location: navigatorLocation, params: navigatorParams } =
useNavigator();
const isMounting = useRef( true );
useEffect(
() => {
// The navigatorParams are only initially filled properly when the
// navigator screens mount. so we ignore the first synchronisation.
if ( isMounting.current ) {
isMounting.current = false;
return;
}
function updateUrlParams( newUrlParams: {
[ key: string ]: string | undefined;
} ) {
if ( isSubset( newUrlParams, urlParams ) ) {
return;
}
const updatedParams = {
...urlParams,
...newUrlParams,
};
history.push( updatedParams );
}
updateUrlParams( {
postType: undefined,
postId: undefined,
categoryType: undefined,
categoryId: undefined,
path:
navigatorLocation.path === '/'
? undefined
: navigatorLocation.path,
} );
},
// Trigger only when navigator changes to prevent infinite loops.
// eslint-disable-next-line react-hooks/exhaustive-deps
[ navigatorLocation?.path, navigatorParams ]
);
}
function SidebarScreens() {
useSyncPathWithURL();
return (
<>
<NavigatorScreen path="/customize-store">
<SidebarNavigationScreenMain />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/color-palette">
<SidebarNavigationScreenColorPalette />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/typography">
<SidebarNavigationScreenTypography />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/header">
<SidebarNavigationScreenHeader />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/homepage">
<SidebarNavigationScreenHomepage />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/footer">
<SidebarNavigationScreenFooter />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/pages">
<SidebarNavigationScreenPages />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/logo">
<SidebarNavigationScreenLogo />
</NavigatorScreen>
</>
);
}
function Sidebar() {
const { params: urlParams } = useLocation();
const initialPath = useRef( urlParams.path ?? '/customize-store' );
return (
<>
<NavigatorProvider
className="edit-site-sidebar__content"
initialPath={ initialPath.current }
>
<SidebarScreens />
</NavigatorProvider>
<SaveHub />
</>
);
}
export default memo( Sidebar );

View File

@ -0,0 +1,228 @@
// Reference: https://github.com/WordPress/gutenberg/blob/v16.4.0/packages/edit-site/src/components/save-hub/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import { useContext } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { Button, __experimentalHStack as HStack } from '@wordpress/components';
import { __, sprintf, _n } from '@wordpress/i18n';
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { store as blockEditorStore } from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { check } from '@wordpress/icons';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { store as noticesStore } from '@wordpress/notices';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import SaveButton from '@wordpress/edit-site/build-module/components/save-button';
/**
* Internal dependencies
*/
import { CustomizeStoreContext } from '../';
const { useLocation } = unlock( routerPrivateApis );
const PUBLISH_ON_SAVE_ENTITIES = [
{
kind: 'postType',
name: 'wp_navigation',
},
];
export const SaveHub = () => {
const saveNoticeId = 'site-edit-save-notice';
const { params } = useLocation();
const { sendEvent } = useContext( CustomizeStoreContext );
// @ts-ignore No types for this exist yet.
const { __unstableMarkLastChangeAsPersistent } =
useDispatch( blockEditorStore );
const { createSuccessNotice, createErrorNotice, removeNotice } =
useDispatch( noticesStore );
const { dirtyCurrentEntity, countUnsavedChanges, isDirty, isSaving } =
useSelect(
( select ) => {
const {
// @ts-ignore No types for this exist yet.
__experimentalGetDirtyEntityRecords,
// @ts-ignore No types for this exist yet.
isSavingEntityRecord,
} = select( coreStore );
const dirtyEntityRecords =
__experimentalGetDirtyEntityRecords();
let calcDirtyCurrentEntity = null;
if ( dirtyEntityRecords.length === 1 ) {
// if we are on global styles
if (
params.path?.includes( 'color-palette' ) ||
params.path?.includes( 'fonts' )
) {
calcDirtyCurrentEntity = dirtyEntityRecords.find(
// @ts-ignore No types for this exist yet.
( record ) => record.name === 'globalStyles'
);
}
// if we are on pages
else if ( params.postId ) {
calcDirtyCurrentEntity = dirtyEntityRecords.find(
// @ts-ignore No types for this exist yet.
( record ) =>
record.name === params.postType &&
String( record.key ) === params.postId
);
}
}
return {
dirtyCurrentEntity: calcDirtyCurrentEntity,
isDirty: dirtyEntityRecords.length > 0,
isSaving: dirtyEntityRecords.some(
( record: {
kind: string;
name: string;
key: string;
} ) =>
isSavingEntityRecord(
record.kind,
record.name,
record.key
)
),
countUnsavedChanges: dirtyEntityRecords.length,
};
},
[ params.path, params.postType, params.postId ]
);
const {
// @ts-ignore No types for this exist yet.
editEntityRecord,
// @ts-ignore No types for this exist yet.
saveEditedEntityRecord,
// @ts-ignore No types for this exist yet.
__experimentalSaveSpecifiedEntityEdits: saveSpecifiedEntityEdits,
} = useDispatch( coreStore );
const saveCurrentEntity = async () => {
if ( ! dirtyCurrentEntity ) return;
removeNotice( saveNoticeId );
const { kind, name, key, property } = dirtyCurrentEntity;
try {
if ( dirtyCurrentEntity.kind === 'root' && name === 'site' ) {
await saveSpecifiedEntityEdits( 'root', 'site', undefined, [
property,
] );
} else {
if (
PUBLISH_ON_SAVE_ENTITIES.some(
( typeToPublish ) =>
typeToPublish.kind === kind &&
typeToPublish.name === name
)
) {
editEntityRecord( kind, name, key, { status: 'publish' } );
}
await saveEditedEntityRecord( kind, name, key );
}
__unstableMarkLastChangeAsPersistent();
createSuccessNotice( __( 'Site updated.', 'woocommerce' ), {
type: 'snackbar',
id: saveNoticeId,
} );
} catch ( error ) {
createErrorNotice(
`${ __( 'Saving failed.', 'woocommerce' ) } ${ error }`
);
}
};
const renderButton = () => {
// if we have only one unsaved change and it matches current context, we can show a more specific label
let label = dirtyCurrentEntity
? __( 'Save', 'woocommerce' )
: sprintf(
// translators: %d: number of unsaved changes (number).
_n(
'Review %d change…',
'Review %d changes…',
countUnsavedChanges,
'woocommerce'
),
countUnsavedChanges
);
if ( isSaving ) {
label = __( 'Saving', 'woocommerce' );
}
if ( dirtyCurrentEntity ) {
return (
<Button
variant="primary"
onClick={ saveCurrentEntity }
isBusy={ isSaving }
disabled={ isSaving }
aria-disabled={ isSaving }
className="edit-site-save-hub__button"
// @ts-ignore No types for this exist yet.
__next40pxDefaultSize
>
{ label }
</Button>
);
}
const disabled = isSaving || ! isDirty;
if ( ! isSaving && ! isDirty ) {
return (
<Button
variant="primary"
onClick={ () => {
sendEvent( 'FINISH_CUSTOMIZATION' );
} }
className="edit-site-save-hub__button"
// @ts-ignore No types for this exist yet.
__next40pxDefaultSize
>
{ __( 'Done', 'woocommerce' ) }
</Button>
);
}
return (
<SaveButton
className="edit-site-save-hub__button"
variant={ disabled ? null : 'primary' }
showTooltip={ false }
icon={ disabled && ! isSaving ? check : null }
defaultLabel={ label }
__next40pxDefaultSize
/>
);
};
return (
<HStack className="edit-site-save-hub" alignment="right" spacing={ 4 }>
{ renderButton() }
</HStack>
);
};

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenColorPalette = () => {
return (
<SidebarNavigationScreen
title={ __( 'Change the color palette', 'woocommerce' ) }
description={ createInterpolateElement(
__(
'Based on the info you shared, our AI tool recommends using this color palette. Want to change it? You can select or add new colors below, or update them later in <EditorLink>Editor</EditorLink> | <StyleLink>Styles</StyleLink>.',
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
StyleLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenFooter = () => {
return (
<SidebarNavigationScreen
title={ __( 'Change your footer', 'woocommerce' ) }
description={ createInterpolateElement(
__(
"Select a new header from the options below. Your header includes your site's navigation and will be added to every page. You can continue customizing this via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenHeader = () => {
return (
<SidebarNavigationScreen
title={ __( 'Change your header', 'woocommerce' ) }
description={ createInterpolateElement(
__(
"Select a new header from the options below. Your header includes your site's navigation and will be added to every page. You can continue customizing this via the <EditorLink>Editor</EditorLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,38 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenHomepage = () => {
return (
<SidebarNavigationScreen
title={ __( 'Change your homepage', 'woocommerce' ) }
description={ createInterpolateElement(
__(
'Based on the most successful stores in your industry and location, our AI tool has recommended this template for your business. Prefer a different layout? Choose from the templates below now, or later via the <EditorLink>Editor</EditorLink>.',
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
export const SidebarNavigationScreenLogo = () => {
return (
<SidebarNavigationScreen
title={ __( 'Add your logo', 'woocommerce' ) }
description={ __(
"Ensure your store is on-brand by adding your logo. For best results, upload a SVG or PNG that's a minimum of 300px wide.",
'woocommerce'
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,130 @@
/**
* WordPress dependencies
*/
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { createInterpolateElement } from '@wordpress/element';
import {
// @ts-ignore No types for this exist yet.
__experimentalItemGroup as ItemGroup,
// @ts-ignore No types for this exist yet.
__experimentalNavigatorButton as NavigatorButton,
// @ts-ignore No types for this exist yet.
__experimentalHeading as Heading,
} from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import {
siteLogo,
color,
typography,
header,
home,
footer,
pages,
} from '@wordpress/icons';
// @ts-ignore No types for this exist yet.
import SidebarNavigationItem from '@wordpress/edit-site/build-module/components/sidebar-navigation-item';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenMain = () => {
return (
<SidebarNavigationScreen
isRoot
title={ __( "Let's get creative", 'woocommerce' ) }
description={ createInterpolateElement(
__(
'Use our style and layout tools to customize the design of your store. Content and images can be added or changed via the <EditorLink>Editor</EditorLink> later.',
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header">
<Heading level={ 2 }>
{ __( 'Style', 'woocommerce' ) }
</Heading>
</div>
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/logo"
withChevron
icon={ siteLogo }
>
{ __( 'Add your logo', 'woocommerce' ) }
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/color-palette"
withChevron
icon={ color }
>
{ __( 'Change the color palette', 'woocommerce' ) }
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/typography"
withChevron
icon={ typography }
>
{ __( 'Change fonts', 'woocommerce' ) }
</NavigatorButton>
</ItemGroup>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header">
<Heading level={ 2 }>
{ __( 'Layout', 'woocommerce' ) }
</Heading>
</div>
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/header"
withChevron
icon={ header }
>
{ __( 'Change your header', 'woocommerce' ) }
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/homepage"
withChevron
icon={ home }
>
{ __( 'Change your homepage', 'woocommerce' ) }
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/footer"
withChevron
icon={ footer }
>
{ __( 'Change your footer', 'woocommerce' ) }
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/pages"
withChevron
icon={ pages }
>
{ __( 'Add and edit other pages', 'woocommerce' ) }
</NavigatorButton>
</ItemGroup>
</>
}
/>
);
};

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Link } from '@woocommerce/components';
import { createInterpolateElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenPages = () => {
return (
<SidebarNavigationScreen
title={ __( 'Add more pages', 'woocommerce' ) }
description={ createInterpolateElement(
__(
"Enhance your customers' experience by customizing existing pages or adding new ones. You can continue customizing and adding pages later in <EditorLink>Editor</EditorLink> | <PageLink>Pages</PageLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
PageLink: (
<Link
href={ `${ ADMIN_URL }/edit.php?post_type=page` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,45 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createInterpolateElement } from '@wordpress/element';
import { Link } from '@woocommerce/components';
/**
* Internal dependencies
*/
import { SidebarNavigationScreen } from './sidebar-navigation-screen';
import { ADMIN_URL } from '~/utils/admin-settings';
export const SidebarNavigationScreenTypography = () => {
return (
<SidebarNavigationScreen
title={ __( 'Change your font', 'woocommerce' ) }
description={ createInterpolateElement(
__(
"AI has selected a font pairing that's the best fit for your business. If you'd like to change them, select a new option below now, or later in <EditorLink>Editor</EditorLink> | <StyleLink>Styles</StyleLink>.",
'woocommerce'
),
{
EditorLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php` }
type="external"
/>
),
StyleLink: (
<Link
href={ `${ ADMIN_URL }/site-editor.php?path=%2Fwp_global_styles&canvas=edit` }
type="external"
/>
),
}
) }
content={
<>
<div className="edit-site-sidebar-navigation-screen-patterns__group-header"></div>
</>
}
/>
);
};

View File

@ -0,0 +1,141 @@
// Reference: https://github.com/WordPress/gutenberg/blob/v16.4.0/packages/edit-site/src/components/sidebar-navigation-screen/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classnames from 'classnames';
import { useContext } from '@wordpress/element';
import {
// @ts-ignore No types for this exist yet.
__experimentalHStack as HStack,
// @ts-ignore No types for this exist yet.
__experimentalHeading as Heading,
// @ts-ignore No types for this exist yet.
__experimentalUseNavigator as useNavigator,
// @ts-ignore No types for this exist yet.
__experimentalVStack as VStack,
} from '@wordpress/components';
import { isRTL, __ } from '@wordpress/i18n';
import { chevronRight, chevronLeft } from '@wordpress/icons';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import SidebarButton from '@wordpress/edit-site/build-module/components/sidebar-button';
/**
* Internal dependencies
*/
import { CustomizeStoreContext } from '../';
const { useLocation } = unlock( routerPrivateApis );
export const SidebarNavigationScreen = ( {
isRoot,
title,
actions,
meta,
content,
footer,
description,
backPath: backPathProp,
}: {
isRoot?: boolean;
title: string;
actions?: React.ReactNode;
meta?: React.ReactNode;
content: React.ReactNode;
footer?: React.ReactNode;
description?: React.ReactNode;
backPath?: string;
} ) => {
const { sendEvent } = useContext( CustomizeStoreContext );
const location = useLocation();
const navigator = useNavigator();
const icon = isRTL() ? chevronRight : chevronLeft;
return (
<>
<VStack
className={ classnames(
'edit-site-sidebar-navigation-screen__main',
{
'has-footer': !! footer,
}
) }
spacing={ 0 }
justify="flex-start"
>
<HStack
spacing={ 4 }
alignment="flex-start"
className="edit-site-sidebar-navigation-screen__title-icon"
>
{ ! isRoot && (
<SidebarButton
onClick={ () => {
const backPath =
backPathProp ?? location.state?.backPath;
if ( backPath ) {
navigator.goTo( backPath, {
isBack: true,
} );
} else {
navigator.goToParent();
}
} }
icon={ icon }
label={ __( 'Back', 'woocommerce' ) }
showTooltip={ false }
/>
) }
{ isRoot && (
<SidebarButton
onClick={ () => {
sendEvent( 'GO_BACK_TO_DESIGN_WITH_AI' );
} }
icon={ icon }
label={ __( 'Back', 'woocommerce' ) }
showTooltip={ false }
/>
) }
<Heading
className="edit-site-sidebar-navigation-screen__title"
color={ '#e0e0e0' /* $gray-200 */ }
level={ 1 }
size={ 20 }
>
{ title }
</Heading>
{ actions && (
<div className="edit-site-sidebar-navigation-screen__actions">
{ actions }
</div>
) }
</HStack>
{ meta && (
<>
<div className="edit-site-sidebar-navigation-screen__meta">
{ meta }
</div>
</>
) }
<div className="edit-site-sidebar-navigation-screen__content">
{ description && (
<p className="edit-site-sidebar-navigation-screen__description">
{ description }
</p>
) }
{ content }
</div>
</VStack>
{ footer && (
<footer className="edit-site-sidebar-navigation-screen__footer">
{ footer }
</footer>
) }
</>
);
};

View File

@ -0,0 +1,127 @@
// Reference: https://github.com/WordPress/gutenberg/blob/v16.4.0/packages/edit-site/src/components/site-hub/index.js
/* eslint-disable @woocommerce/dependency-group */
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* External dependencies
*/
import classnames from 'classnames';
import { useSelect } from '@wordpress/data';
import {
// @ts-ignore No types for this exist yet.
__unstableMotion as motion,
// @ts-ignore No types for this exist yet.
__unstableAnimatePresence as AnimatePresence,
// @ts-ignore No types for this exist yet.
__experimentalHStack as HStack,
} from '@wordpress/components';
import { useReducedMotion } from '@wordpress/compose';
// @ts-ignore No types for this exist yet.
import { store as coreStore } from '@wordpress/core-data';
import { decodeEntities } from '@wordpress/html-entities';
import { forwardRef } from '@wordpress/element';
// @ts-ignore No types for this exist yet.
import SiteIcon from '@wordpress/edit-site/build-module/components/site-icon';
/**
* Internal dependencies
*/
const HUB_ANIMATION_DURATION = 0.3;
export const SiteHub = forwardRef(
(
{
isTransparent,
...restProps
}: {
isTransparent: boolean;
className: string;
as: string;
variants: motion.Variants;
},
ref
) => {
const { siteTitle } = useSelect( ( select ) => {
// @ts-ignore No types for this exist yet.
const { getSite } = select( coreStore );
return {
siteTitle: getSite()?.title,
};
}, [] );
const disableMotion = useReducedMotion();
return (
<motion.div
ref={ ref }
{ ...restProps }
className={ classnames(
'edit-site-site-hub',
restProps.className
) }
initial={ false }
transition={ {
type: 'tween',
duration: disableMotion ? 0 : HUB_ANIMATION_DURATION,
ease: 'easeOut',
} }
>
<HStack
justify="space-between"
alignment="center"
className="edit-site-site-hub__container"
>
<HStack
justify="flex-start"
className="edit-site-site-hub__text-content"
spacing="0"
>
<motion.div
className={ classnames(
'edit-site-site-hub__view-mode-toggle-container',
{
'has-transparent-background': isTransparent,
}
) }
layout
transition={ {
type: 'tween',
duration: disableMotion
? 0
: HUB_ANIMATION_DURATION,
ease: 'easeOut',
} }
>
<SiteIcon className="edit-site-layout__view-mode-toggle-icon" />
</motion.div>
<AnimatePresence>
<motion.div
layout={ false }
animate={ {
opacity: 1,
} }
exit={ {
opacity: 0,
} }
className={ classnames(
'edit-site-site-hub__site-title',
{ 'is-transparent': isTransparent }
) }
transition={ {
type: 'tween',
duration: disableMotion ? 0 : 0.2,
ease: 'easeOut',
delay: 0.1,
} }
>
{ decodeEntities( siteTitle ) }
</motion.div>
</AnimatePresence>
</HStack>
</HStack>
</motion.div>
);
}
);

View File

@ -0,0 +1,220 @@
@mixin custom-scrollbars-on-hover($handle-color, $handle-color-hover) {
// WebKit
&::-webkit-scrollbar {
width: 12px;
height: 12px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: $handle-color;
border-radius: 8px;
border: 3px solid transparent;
background-clip: padding-box;
}
&:hover::-webkit-scrollbar-thumb, // This needs specificity.
&:focus::-webkit-scrollbar-thumb,
&:focus-within::-webkit-scrollbar-thumb {
background-color: $handle-color-hover;
}
// Firefox 109+ and Chrome 111+
scrollbar-width: thin;
scrollbar-gutter: stable both-edges;
scrollbar-color: $handle-color transparent; // Syntax, "dark", "light", or "#handle-color #track-color"
&:hover,
&:focus,
&:focus-within {
scrollbar-color: $handle-color-hover transparent;
}
// Needed to fix a Safari rendering issue.
will-change: transform;
// Always show scrollbar on Mobile devices.
@media (hover: none) {
& {
scrollbar-color: $handle-color-hover transparent;
}
}
}
.woocommerce-profile-wizard__step-assemblerHub {
a {
text-decoration: none;
}
.edit-site-layout {
bottom: 0;
left: 0;
min-height: 100vh;
position: fixed;
right: 0;
top: 0;
background-color: #fcfcfc;
}
/* Sidebar Header */
.edit-site-layout__hub {
width: 380px;
height: 64px;
}
.edit-site-site-hub__view-mode-toggle-container {
height: 64px;
}
.edit-site-sidebar-navigation-screen__title-icon {
align-items: center;
padding-top: 80px;
padding-bottom: 0;
gap: 0;
}
.edit-site-sidebar-navigation-screen__title-icon,
.edit-site-site-hub__view-mode-toggle-container,
.edit-site-layout__view-mode-toggle-icon.edit-site-site-icon {
background-color: #fcfcfc;
}
.edit-site-site-hub__site-title {
color: $gray-900;
font-size: 0.8125rem;
font-style: normal;
font-weight: 500;
line-height: 20px; /* 153.846% */
margin: 0;
}
.edit-site-site-icon__image {
border-radius: 2px;
}
.edit-site-site-hub__view-mode-toggle-container {
padding: 16px 12px 0 16px;
.edit-site-layout__view-mode-toggle,
.edit-site-layout__view-mode-toggle-icon.edit-site-site-icon,
.edit-site-site-icon__icon {
width: 32px;
height: 32px;
}
}
/* Sidebar */
.edit-site-layout__sidebar-region {
width: 380px;
}
.edit-site-layout__sidebar {
.edit-site-sidebar__content > div {
padding: 0 16px;
overflow-x: hidden;
}
.edit-site-sidebar-button {
color: $gray-900;
height: 40px;
}
.edit-site-sidebar-navigation-screen__title {
font-size: 1rem;
color: $gray-900;
text-overflow: ellipsis;
white-space: nowrap;
font-style: normal;
font-weight: 600;
line-height: 24px; /* 150% */
padding: 0;
}
.edit-site-sidebar-navigation-screen__description {
color: $gray-700;
font-size: 0.8125rem;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 153.846% */
}
.edit-site-sidebar-navigation-screen__content .components-heading {
color: $gray-700;
font-size: 0.6875rem;
font-style: normal;
font-weight: 600;
line-height: 16px; /* 145.455% */
text-transform: uppercase;
}
.edit-site-sidebar-navigation-item {
border-radius: 4px;
padding: 8px 8px 8px 16px;
align-items: center;
gap: 8px;
align-self: stretch;
width: 348px;
&:hover {
background: #ededed;
color: $gray-600;
}
&:active {
color: #171717;
background: #fcfcfc;
}
&:focus {
color: #171717;
background: #fcfcfc;
border: 1.5px solid var(--wp-admin-theme-color);
}
.components-flex-item {
color: $gray-900;
font-size: 0.8125rem;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 123.077% */
letter-spacing: -0.078px;
}
}
.edit-site-sidebar-navigation-item.components-item .edit-site-sidebar-navigation-item__drilldown-indicator {
fill: #ccc;
}
.edit-site-save-hub {
border-top: 0;
padding: 32px 29px 32px 35px;
}
}
/* Preview Canvas */
.edit-site-layout__canvas {
bottom: 16px;
top: 16px;
width: calc(100% - 16px);
}
.edit-site-layout__canvas .components-resizable-box__container {
border-radius: 20px;
box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.25), 0 6px 10px 0 rgba(0, 0, 0, 0.02), 0 13px 15px 0 rgba(0, 0, 0, 0.03), 0 15px 20px 0 rgba(0, 0, 0, 0.04);
}
.woocommerce-customize-store__block-editor,
.edit-site-layout:not(.is-full-canvas) .edit-site-layout__canvas > div .interface-interface-skeleton__content {
border-radius: 20px;
}
.interface-interface-skeleton__content {
@include custom-scrollbars-on-hover(transparent, $gray-600);
}
.edit-site-resizable-frame__inner-content {
border-radius: 20px !important;
}
}

View File

@ -9,7 +9,7 @@ export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => {
<>
<h1>Design with AI</h1>
<button onClick={ () => sendEvent( { type: 'THEME_SUGGESTED' } ) }>
Back to intro
Assembler Hub
</button>
</>
);

View File

@ -0,0 +1,10 @@
declare global {
interface Window {
wcBlockSettings: {
[ key: string ]: unknown;
};
}
}
/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */
export {};

View File

@ -16,7 +16,7 @@ import {
actions as introActions,
} from './intro';
import { DesignWithAi, events as designWithAiEvents } from './design-with-ai';
import { events as assemblerHubEvents } from './assembler-hub';
import { AssemblerHub, events as assemblerHubEvents } from './assembler-hub';
import { findComponentMeta } from '~/utils/xstate/find-component';
import {
CustomizeStoreComponentMeta,
@ -86,10 +86,10 @@ export const customizeStoreStateMachineDefinition = createMachine( {
target: 'backToHomescreen',
},
SELECTED_NEW_THEME: {
target: '? Appearance Task ?',
target: 'appearanceTask',
},
SELECTED_BROWSE_ALL_THEMES: {
target: '? Appearance Task ?',
target: 'appearanceTask',
},
},
},
@ -114,14 +114,20 @@ export const customizeStoreStateMachineDefinition = createMachine( {
},
},
assemblerHub: {
meta: {
component: AssemblerHub,
},
on: {
FINISH_CUSTOMIZATION: {
target: 'backToHomescreen',
},
GO_BACK_TO_DESIGN_WITH_AI: {
target: 'designWithAi',
},
},
},
backToHomescreen: {},
'? Appearance Task ?': {},
appearanceTask: {},
},
} );

View File

@ -37,6 +37,11 @@ export const Intro: CustomizeStoreComponent = ( { sendEvent, context } ) => {
<button onClick={ () => sendEvent( { type: 'DESIGN_WITH_AI' } ) }>
Design with AI
</button>
<button
onClick={ () => sendEvent( { type: 'SELECTED_ACTIVE_THEME' } ) }
>
Assembler Hub
</button>
</>
);
};

View File

@ -319,7 +319,7 @@ export const getPages = () => {
if ( window.wcAdminFeatures[ 'customize-store' ] ) {
pages.push( {
container: CustomizeStore,
path: '/customize-store',
path: '/customize-store/*',
breadcrumbs: [
...initialBreadcrumbs,
__( 'Customize Your Store', 'woocommerce' ),

View File

@ -0,0 +1,31 @@
/**
* External dependencies
*/
import { registerPlugin } from '@wordpress/plugins';
import { WooOnboardingTaskListItem } from '@woocommerce/onboarding';
import { getAdminLink } from '@woocommerce/settings';
const CustomizeStoreTaskItem = () => (
<WooOnboardingTaskListItem id="customize-store">
{ ( {
defaultTaskItem: DefaultTaskItem,
}: {
defaultTaskItem: ( props: { onClick: () => void } ) => JSX.Element;
} ) => (
<DefaultTaskItem
onClick={ () => {
// We need to use window.location.href instead of navigateTo because we need to initiate a full page refresh to ensure that all dependencies are loaded.
window.location.href = getAdminLink(
'admin.php?page=wc-admin&path=%2Fcustomize-store'
);
} }
/>
) }
</WooOnboardingTaskListItem>
);
registerPlugin( 'woocommerce-admin-task-customize-store', {
// @ts-expect-error scope is not defined in the type definition but it is a valid property
scope: 'woocommerce-tasks',
render: CustomizeStoreTaskItem,
} );

View File

@ -10,6 +10,7 @@ import './tax';
import './woocommerce-payments';
import './purchase';
import './deprecated-tasks';
import './customize-store-tasklist-item';
const possiblyImportProductTask = async () => {
if ( isImportProduct() ) {

View File

@ -66,16 +66,19 @@
"@wordpress/date": "wp-6.0",
"@wordpress/dom": "wp-6.0",
"@wordpress/dom-ready": "wp-6.0",
"@wordpress/edit-site": "5.15.0",
"@wordpress/element": "wp-6.0",
"@wordpress/hooks": "wp-6.0",
"@wordpress/html-entities": "wp-6.0",
"@wordpress/i18n": "wp-6.0",
"@wordpress/icons": "wp-6.0",
"@wordpress/interface": "^5.15.0",
"@wordpress/keycodes": "wp-6.0",
"@wordpress/media-utils": "wp-6.0",
"@wordpress/notices": "wp-6.0",
"@wordpress/plugins": "wp-6.0",
"@wordpress/primitives": "wp-6.0",
"@wordpress/router": "0.7.0",
"@wordpress/url": "wp-6.0",
"@wordpress/viewport": "wp-6.0",
"@wordpress/warning": "wp-6.0",

View File

@ -216,6 +216,12 @@ const webpackConfig = {
// The external wp.components does not include ui components, so we need to skip requesting to external here.
return null;
}
if ( request.startsWith( '@wordpress/edit-site' ) ) {
// The external wp.editSite does not include edit-site components, so we need to skip requesting to external here. We can remove this once the edit-site components are exported in the external wp.editSite.
// We use the edit-site components in the customize store.
return null;
}
},
} ),
// Reduces data for moment-timezone.

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add customize store assembler hub

View File

@ -8,6 +8,16 @@ use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
* Customize Your Store Task
*/
class CustomizeStore extends Task {
/**
* Constructor
*
* @param TaskList $task_list Parent task list.
*/
public function __construct( $task_list ) {
parent::__construct( $task_list );
add_action( 'admin_enqueue_scripts', array( $this, 'possibly_add_site_editor_scripts' ) );
}
/**
* ID.
*
@ -63,11 +73,114 @@ class CustomizeStore extends Task {
}
/**
* Action URL.
*
* @return string
* Possibly add site editor scripts.
*/
public function get_action_url() {
return admin_url( 'wp-admin/admin.php?page=wc-admin&path=%2Fcustomize-store' );
public function possibly_add_site_editor_scripts() {
$is_customize_store_pages = (
isset( $_GET['page'] ) &&
'wc-admin' === $_GET['page'] &&
isset( $_GET['path'] ) &&
str_starts_with( wc_clean( wp_unslash( $_GET['path'] ) ), '/customize-store' )
);
if ( ! $is_customize_store_pages ) {
return;
}
// See: https://github.com/WordPress/WordPress/blob/master/wp-admin/site-editor.php.
if ( ! wp_is_block_theme() ) {
wp_die( esc_html__( 'The theme you are currently using is not compatible.', 'woocommerce' ) );
}
global $editor_styles;
// Flag that we're loading the block editor.
$current_screen = get_current_screen();
$current_screen->is_block_editor( true );
// Default to is-fullscreen-mode to avoid jumps in the UI.
add_filter(
'admin_body_class',
static function( $classes ) {
return "$classes is-fullscreen-mode";
}
);
$block_editor_context = new \WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) );
$indexed_template_types = array();
foreach ( get_default_block_template_types() as $slug => $template_type ) {
$template_type['slug'] = (string) $slug;
$indexed_template_types[] = $template_type;
}
$custom_settings = array(
'siteUrl' => site_url(),
'postsPerPage' => get_option( 'posts_per_page' ),
'styles' => get_block_editor_theme_styles(),
'defaultTemplateTypes' => $indexed_template_types,
'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(),
'supportsLayout' => wp_theme_has_theme_json(),
'supportsTemplatePartsMode' => ! wp_is_block_theme() && current_theme_supports( 'block-template-parts' ),
);
// Add additional back-compat patterns registered by `current_screen` et al.
$custom_settings['__experimentalAdditionalBlockPatterns'] = \WP_Block_Patterns_Registry::get_instance()->get_all_registered( true );
$custom_settings['__experimentalAdditionalBlockPatternCategories'] = \WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered( true );
$editor_settings = get_block_editor_settings( $custom_settings, $block_editor_context );
$active_global_styles_id = \WP_Theme_JSON_Resolver::get_user_global_styles_post_id();
$active_theme = get_stylesheet();
$preload_paths = array(
array( '/wp/v2/media', 'OPTIONS' ),
'/wp/v2/types?context=view',
'/wp/v2/types/wp_template?context=edit',
'/wp/v2/types/wp_template-part?context=edit',
'/wp/v2/templates?context=edit&per_page=-1',
'/wp/v2/template-parts?context=edit&per_page=-1',
'/wp/v2/themes?context=edit&status=active',
'/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit',
'/wp/v2/global-styles/' . $active_global_styles_id,
'/wp/v2/global-styles/themes/' . $active_theme,
);
block_editor_rest_api_preload( $preload_paths, $block_editor_context );
wp_add_inline_script(
'wp-blocks',
sprintf(
'window.wcBlockSettings = %s;',
wp_json_encode( $editor_settings )
)
);
// Preload server-registered block schemas.
wp_add_inline_script(
'wp-blocks',
'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');'
);
wp_add_inline_script(
'wp-blocks',
sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( isset( $editor_settings['blockCategories'] ) ? $editor_settings['blockCategories'] : array() ) ),
'after'
);
wp_enqueue_script( 'wp-editor' );
wp_enqueue_script( 'wp-format-library' ); // Not sure if this is needed.
wp_enqueue_script( 'wp-router' );
wp_enqueue_style( 'wp-editor' );
wp_enqueue_style( 'wp-edit-site' );
wp_enqueue_style( 'wp-format-library' );
wp_enqueue_media();
if (
current_theme_supports( 'wp-block-styles' ) &&
( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 )
) {
wp_enqueue_style( 'wp-block-library-theme' );
}
/** This action is documented in wp-admin/edit-form-blocks.php
*
* @since 8.0.3
*/
do_action( 'enqueue_block_editor_assets' );
}
}

File diff suppressed because it is too large Load Diff