add/lys xstate scaffolding (#45548)

* dev: added xstate 5

* add xstate scaffolding for launch your store
This commit is contained in:
RJ 2024-03-18 15:44:32 +08:00 committed by GitHub
parent 5a54dd6527
commit dbd577cbd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 914 additions and 377 deletions

View File

@ -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' )
);

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;
}
}

View File

@ -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,

View File

@ -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 ];
}

View File

@ -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",

View File

@ -0,0 +1,4 @@
Significance: patch
Type: add
Add xstate scaffold for Launch your store feature

File diff suppressed because it is too large Load Diff