add/lys xstate scaffolding (#45548)
* dev: added xstate 5 * add xstate scaffolding for launch your store
This commit is contained in:
parent
5a54dd6527
commit
dbd577cbd6
|
@ -0,0 +1,9 @@
|
|||
const fs = require( 'fs-extra' );
|
||||
const path = require( 'path' );
|
||||
|
||||
const rootNodeModules = path.join( __dirname, '..', 'node_modules' );
|
||||
|
||||
fs.ensureSymlinkSync(
|
||||
path.join( rootNodeModules, 'xstate5' ),
|
||||
path.join( rootNodeModules, '@xstate5', 'react', 'node_modules', 'xstate' )
|
||||
);
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMachine } from '@xstate5/react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useFullScreen } from '~/utils';
|
||||
import { useComponentFromXStateService } from '~/utils/xstate/useComponentFromService';
|
||||
|
||||
import './styles.scss';
|
||||
import {
|
||||
SidebarMachineEvents,
|
||||
sidebarMachine,
|
||||
SidebarComponentProps,
|
||||
SidebarContainer,
|
||||
} from './sidebar/xstate';
|
||||
import {
|
||||
MainContentMachineEvents,
|
||||
mainContentMachine,
|
||||
MainContentComponentProps,
|
||||
MainContentContainer,
|
||||
} from './main-content/xstate';
|
||||
|
||||
export type LaunchYourStoreComponentProps = {
|
||||
sendEventToSidebar: ( arg0: SidebarMachineEvents ) => void;
|
||||
sendEventToMainContent: ( arg0: MainContentMachineEvents ) => void;
|
||||
className?: string;
|
||||
};
|
||||
const LaunchStoreController = () => {
|
||||
useFullScreen( [ 'woocommerce-launch-your-store' ] );
|
||||
|
||||
const [ mainContentState, sendToMainContent, mainContentMachineService ] =
|
||||
useMachine( mainContentMachine );
|
||||
|
||||
const [ sidebarState, sendToSidebar, sidebarMachineService ] = useMachine(
|
||||
sidebarMachine,
|
||||
{
|
||||
input: {
|
||||
mainContentMachineRef: mainContentMachineService,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const isSidebarVisible = ! sidebarState.hasTag( 'fullscreen' );
|
||||
|
||||
const [ CurrentSidebarComponent ] =
|
||||
useComponentFromXStateService< SidebarComponentProps >(
|
||||
sidebarMachineService
|
||||
);
|
||||
|
||||
const [ CurrentMainContentComponent ] =
|
||||
useComponentFromXStateService< MainContentComponentProps >(
|
||||
mainContentMachineService
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={ 'launch-your-store-layout__container' }>
|
||||
<SidebarContainer
|
||||
className={ classnames( {
|
||||
'is-sidebar-hidden': ! isSidebarVisible,
|
||||
} ) }
|
||||
>
|
||||
{ CurrentSidebarComponent && (
|
||||
<CurrentSidebarComponent
|
||||
sendEventToSidebar={ sendToSidebar }
|
||||
sendEventToMainContent={ sendToMainContent }
|
||||
context={ sidebarState.context }
|
||||
/>
|
||||
) }
|
||||
{ JSON.stringify( sidebarState.toJSON() ) }
|
||||
{ JSON.stringify( sidebarState.getMeta() ) }
|
||||
</SidebarContainer>
|
||||
<MainContentContainer>
|
||||
{ CurrentMainContentComponent && (
|
||||
<CurrentMainContentComponent
|
||||
sendEventToSidebar={ sendToSidebar }
|
||||
sendEventToMainContent={ sendToMainContent }
|
||||
context={ mainContentState.context }
|
||||
/>
|
||||
) }
|
||||
{ JSON.stringify( mainContentState.getMeta() ) }
|
||||
{ JSON.stringify( mainContentState.toJSON() ) }
|
||||
</MainContentContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default LaunchStoreController;
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { MainContentComponentProps } from '../xstate';
|
||||
export const LaunchYourStoreSuccess = ( props: MainContentComponentProps ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'launch-store-success-page__container',
|
||||
props.className
|
||||
) }
|
||||
>
|
||||
<p>Main Content - Site Launch Store Success Page</p>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { MainContentComponentProps } from '../xstate';
|
||||
export const LoadingPage = ( props: MainContentComponentProps ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'launch-store-loading-page__container',
|
||||
props.className
|
||||
) }
|
||||
>
|
||||
<p>Main Content - Loading page</p>
|
||||
<button
|
||||
onClick={ () => {
|
||||
props.sendEventToSidebar( {
|
||||
type: 'LAUNCH_STORE_SUCCESS',
|
||||
} );
|
||||
} }
|
||||
>
|
||||
Launch Store Success
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { MainContentComponentProps } from '../xstate';
|
||||
|
||||
export const SitePreviewPage = ( props: MainContentComponentProps ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'launch-store-site-preview-page__container',
|
||||
props.className
|
||||
) }
|
||||
>
|
||||
<p>Main Content - Site Preview</p>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { setup } from 'xstate5';
|
||||
import React from 'react';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { LoadingPage } from './pages/loading';
|
||||
import { LaunchYourStoreSuccess } from './pages/launch-store-success';
|
||||
import { SitePreviewPage } from './pages/site-preview';
|
||||
import type { LaunchYourStoreComponentProps } from '..';
|
||||
|
||||
export type MainContentMachineContext = {
|
||||
placeholder?: string; // remove this when we have some types to put here
|
||||
};
|
||||
|
||||
export type MainContentComponentProps = LaunchYourStoreComponentProps & {
|
||||
context: MainContentMachineContext;
|
||||
};
|
||||
export type MainContentMachineEvents =
|
||||
| { type: 'SHOW_LAUNCH_STORE_SUCCESS' }
|
||||
| { type: 'SHOW_LOADING' };
|
||||
|
||||
export const mainContentMachine = setup( {
|
||||
types: {} as {
|
||||
context: MainContentMachineContext;
|
||||
events: MainContentMachineEvents;
|
||||
},
|
||||
} ).createMachine( {
|
||||
id: 'mainContent',
|
||||
initial: 'init',
|
||||
context: {},
|
||||
states: {
|
||||
init: {
|
||||
always: [
|
||||
{
|
||||
target: '#sitePreview',
|
||||
},
|
||||
],
|
||||
},
|
||||
sitePreview: {
|
||||
id: 'sitePreview',
|
||||
meta: {
|
||||
component: SitePreviewPage,
|
||||
},
|
||||
},
|
||||
launchStoreSuccess: {
|
||||
id: 'launchStoreSuccess',
|
||||
meta: {
|
||||
component: LaunchYourStoreSuccess,
|
||||
},
|
||||
},
|
||||
loading: {
|
||||
id: 'loading',
|
||||
meta: {
|
||||
component: LoadingPage,
|
||||
},
|
||||
},
|
||||
},
|
||||
on: {
|
||||
SHOW_LAUNCH_STORE_SUCCESS: {
|
||||
target: '#launchStoreSuccess',
|
||||
},
|
||||
SHOW_LOADING: {
|
||||
target: '#loading',
|
||||
},
|
||||
},
|
||||
} );
|
||||
export const MainContentContainer = ( {
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
} ) => {
|
||||
return (
|
||||
<div className="launch-your-store-layout__content">{ children }</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import classnames from 'classnames';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { SidebarComponentProps } from '../xstate';
|
||||
export const LaunchYourStoreHubSidebar: React.FC< SidebarComponentProps > = (
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'launch-store-sidebar__container',
|
||||
props.className
|
||||
) }
|
||||
>
|
||||
<p>Sidebar</p>
|
||||
<button
|
||||
onClick={ () => {
|
||||
props.sendEventToSidebar( { type: 'LAUNCH_STORE' } );
|
||||
// when we send LAUNCH_STORE to the sidebar machine, the sidebar machine sends the appropriate event to the main content machine to show the launching state
|
||||
} }
|
||||
>
|
||||
Launch Store
|
||||
</button>
|
||||
<button
|
||||
onClick={ () => {
|
||||
props.sendEventToSidebar( {
|
||||
type: 'OPEN_EXTERNAL_URL',
|
||||
url: 'https://example.com',
|
||||
} );
|
||||
} }
|
||||
>
|
||||
Open external URL
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { ActorRefFrom, sendTo, setup } from 'xstate5';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { LaunchYourStoreHubSidebar } from './components/launch-store-hub';
|
||||
import type { LaunchYourStoreComponentProps } from '..';
|
||||
import type { mainContentMachine } from '../main-content/xstate';
|
||||
|
||||
export type SidebarMachineContext = {
|
||||
externalUrl: string | null;
|
||||
mainContentMachineRef: ActorRefFrom< typeof mainContentMachine >;
|
||||
};
|
||||
export type SidebarComponentProps = LaunchYourStoreComponentProps & {
|
||||
context: SidebarMachineContext;
|
||||
};
|
||||
export type SidebarMachineEvents =
|
||||
| { type: 'OPEN_EXTERNAL_URL'; url: string }
|
||||
| { type: 'OPEN_WC_ADMIN_URL'; url: string }
|
||||
| { type: 'OPEN_WC_ADMIN_URL_IN_CONTENT_AREA'; url: string }
|
||||
| { type: 'LAUNCH_STORE' }
|
||||
| { type: 'LAUNCH_STORE_SUCCESS' };
|
||||
export const sidebarMachine = setup( {
|
||||
types: {} as {
|
||||
context: SidebarMachineContext;
|
||||
events: SidebarMachineEvents;
|
||||
input: {
|
||||
mainContentMachineRef: ActorRefFrom< typeof mainContentMachine >;
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
openExternalUrl: ( { event } ) => {
|
||||
if ( event.type === 'OPEN_EXTERNAL_URL' ) {
|
||||
window.open( event.url, '_self' );
|
||||
}
|
||||
},
|
||||
showLaunchStoreSuccessPage: sendTo(
|
||||
( { context } ) => context.mainContentMachineRef,
|
||||
{ type: 'SHOW_LAUNCH_STORE_SUCCESS' }
|
||||
),
|
||||
showLoadingPage: sendTo(
|
||||
( { context } ) => context.mainContentMachineRef,
|
||||
{ type: 'SHOW_LOADING' }
|
||||
),
|
||||
},
|
||||
} ).createMachine( {
|
||||
id: 'sidebar',
|
||||
initial: 'init',
|
||||
context: ( { input } ) => ( {
|
||||
externalUrl: null,
|
||||
mainContentMachineRef: input.mainContentMachineRef,
|
||||
} ),
|
||||
states: {
|
||||
init: {
|
||||
always: {
|
||||
target: 'launchYourStoreHub',
|
||||
},
|
||||
},
|
||||
launchYourStoreHub: {
|
||||
initial: 'preLaunchYourStoreHub',
|
||||
states: {
|
||||
preLaunchYourStoreHub: {
|
||||
always: 'launchYourStoreHub',
|
||||
// do async stuff here such as retrieving task statuses
|
||||
},
|
||||
launchYourStoreHub: {
|
||||
tags: 'sidebar-visible',
|
||||
meta: {
|
||||
component: LaunchYourStoreHubSidebar,
|
||||
},
|
||||
on: {
|
||||
LAUNCH_STORE: {
|
||||
target: '#storeLaunching',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
storeLaunching: {
|
||||
id: 'storeLaunching',
|
||||
initial: 'launching',
|
||||
states: {
|
||||
launching: {
|
||||
tags: 'fullscreen',
|
||||
entry: { type: 'showLoadingPage' },
|
||||
on: {
|
||||
LAUNCH_STORE_SUCCESS: {
|
||||
target: '#storeLaunchSuccessful',
|
||||
actions: { type: 'showLaunchStoreSuccessPage' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
storeLaunchSuccessful: {
|
||||
id: 'storeLaunchSuccessful',
|
||||
tags: 'fullscreen',
|
||||
},
|
||||
openExternalUrl: {
|
||||
id: 'openExternalUrl',
|
||||
tags: 'sidebar-visible', // unintuitive but it prevents a layout shift just before leaving
|
||||
entry: [ 'openExternalUrl' ],
|
||||
},
|
||||
},
|
||||
on: {
|
||||
OPEN_EXTERNAL_URL: {
|
||||
target: '#openExternalUrl',
|
||||
},
|
||||
OPEN_WC_ADMIN_URL: {},
|
||||
OPEN_WC_ADMIN_URL_IN_CONTENT_AREA: {},
|
||||
},
|
||||
} );
|
||||
export const SidebarContainer = ( {
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
} ) => {
|
||||
return (
|
||||
<div
|
||||
className={ classnames(
|
||||
'launch-your-store-layout__sidebar',
|
||||
className
|
||||
) }
|
||||
>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
.woocommerce-layout__main .launch-your-store-layout__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.launch-your-store-layout__sidebar {
|
||||
width: 380px;
|
||||
|
||||
&.is-sidebar-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.launch-your-store-layout__content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
|
@ -86,6 +86,10 @@ const CustomizeStore = lazy( () =>
|
|||
import( /* webpackChunkName: "customize-store" */ '../customize-store' )
|
||||
);
|
||||
|
||||
const LaunchStore = lazy( () =>
|
||||
import( /* webpackChunkName: "launch-store" */ '../launch-store' )
|
||||
);
|
||||
|
||||
export const PAGES_FILTER = 'woocommerce_admin_pages_list';
|
||||
|
||||
export const getPages = () => {
|
||||
|
@ -343,6 +347,25 @@ export const getPages = () => {
|
|||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures[ 'launch-your-store' ] ) {
|
||||
pages.push( {
|
||||
container: LaunchStore,
|
||||
path: '/launch-your-store/*',
|
||||
breadcrumbs: [
|
||||
...initialBreadcrumbs,
|
||||
__( 'Launch Your Store', 'woocommerce' ),
|
||||
],
|
||||
layout: {
|
||||
header: false,
|
||||
footer: true,
|
||||
showNotices: true,
|
||||
showStoreAlerts: false,
|
||||
showPluginArea: false,
|
||||
},
|
||||
capability: 'manage_woocommerce',
|
||||
} );
|
||||
}
|
||||
|
||||
if ( window.wcAdminFeatures.settings ) {
|
||||
pages.push( {
|
||||
container: SettingsGroup,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useSelector as xstateUseSelector } from '@xstate5/react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { findComponentMeta } from '~/utils/xstate/find-component';
|
||||
|
||||
type ComponentMetaType< ComponentPropsType > = {
|
||||
component: ( arg0: ComponentPropsType ) => React.ReactElement;
|
||||
};
|
||||
export function useComponentFromXStateService< ComponentProps >(
|
||||
service: Parameters< typeof xstateUseSelector >[ 0 ]
|
||||
): [ ComponentMetaType< ComponentProps >[ 'component' ] | null ] {
|
||||
const componentMeta = xstateUseSelector( service, ( state ) =>
|
||||
findComponentMeta< ComponentMetaType< ComponentProps > >(
|
||||
state.getMeta() ?? undefined
|
||||
)
|
||||
);
|
||||
|
||||
const [ Component, setComponent ] = useState<
|
||||
ComponentMetaType< ComponentProps >[ 'component' ] | null
|
||||
>( null );
|
||||
|
||||
useEffect( () => {
|
||||
if ( componentMeta?.component ) {
|
||||
setComponent( () => componentMeta.component );
|
||||
}
|
||||
}, [ componentMeta?.component ] );
|
||||
|
||||
return [ Component ? Component : null ];
|
||||
}
|
|
@ -29,7 +29,8 @@
|
|||
"watch:build": "pnpm --if-present --workspace-concurrency=Infinity --filter=\"$npm_package_name...\" --parallel '/^watch:build:project:.*$/'",
|
||||
"watch:build:project": "pnpm --if-present run '/^watch:build:project:.*$/'",
|
||||
"watch:build:project:bundle": "wireit",
|
||||
"watch:build:project:feature-config": "WC_ADMIN_PHASE=development php ../woocommerce/bin/generate-feature-config.php"
|
||||
"watch:build:project:feature-config": "WC_ADMIN_PHASE=development php ../woocommerce/bin/generate-feature-config.php",
|
||||
"postinstall": "node ./bin/xstate5-react-script.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.scss": [
|
||||
|
@ -77,6 +78,7 @@
|
|||
"@wordpress/viewport": "wp-6.0",
|
||||
"@wordpress/warning": "wp-6.0",
|
||||
"@xstate/react": "3.2.1",
|
||||
"@xstate5/react": "npm:@xstate/react@4",
|
||||
"classnames": "^2.3.2",
|
||||
"core-js": "^3.34.0",
|
||||
"debug": "^4.3.4",
|
||||
|
@ -96,6 +98,7 @@
|
|||
"react-visibility-sensor": "^5.1.1",
|
||||
"redux": "^4.2.1",
|
||||
"xstate": "4.37.1",
|
||||
"xstate5": "npm:xstate@^5.9.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -193,6 +196,7 @@
|
|||
"eslint-plugin-xstate": "^1.1.3",
|
||||
"expose-loader": "^3.1.0",
|
||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"jest": "~27.5.1",
|
||||
"jest-environment-jsdom": "~27.5.1",
|
||||
"jest-environment-node": "~27.5.1",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: add
|
||||
|
||||
Add xstate scaffold for Launch your store feature
|
785
pnpm-lock.yaml
785
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue