add: customize your store AI wizard xstate scaffolding (#39863)

* add: customize your store AI wizard xstate scaffolding

* Update plugins/woocommerce-admin/client/customize-store/design-with-ai/index.tsx

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>

---------

Co-authored-by: Ilyas Foo <foo.ilyas@gmail.com>
This commit is contained in:
RJ 2023-08-29 16:00:54 +10:00 committed by GitHub
parent 3c25538f35
commit af9ef856e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 521 additions and 5 deletions

View File

@ -0,0 +1,28 @@
/**
* External dependencies
*/
import { assign } from 'xstate';
/**
* Internal dependencies
*/
import {
designWithAiStateMachineContext,
designWithAiStateMachineEvents,
} from './types';
import { businessInfoDescriptionCompleteEvent } from './pages';
const assignBusinessInfoDescription = assign<
designWithAiStateMachineContext,
designWithAiStateMachineEvents
>( {
businessInfoDescription: ( context, event: unknown ) => {
return {
descriptionText: ( event as businessInfoDescriptionCompleteEvent )
.payload,
};
},
} );
export const actions = {
assignBusinessInfoDescription,
};

View File

@ -1,16 +1,87 @@
/**
* External dependencies
*/
import { useMachine, useSelector } from '@xstate/react';
import { useEffect, useState } from '@wordpress/element';
import { Sender } from 'xstate';
/**
* Internal dependencies
*/
import { CustomizeStoreComponent } from '../types';
import { designWithAiStateMachineDefinition } from './state-machine';
import { findComponentMeta } from '~/utils/xstate/find-component';
import {
BusinessInfoDescription,
ApiCallLoader,
LookAndFeel,
ToneOfVoice,
} from './pages';
import { customizeStoreStateMachineEvents } from '..';
export type events = { type: 'THEME_SUGGESTED' };
export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => {
export type DesignWithAiComponent =
| typeof BusinessInfoDescription
| typeof ApiCallLoader
| typeof LookAndFeel
| typeof ToneOfVoice;
export type DesignWithAiComponentMeta = {
component: DesignWithAiComponent;
};
export const DesignWithAiController = ( {}: {
sendEventToParent: Sender< customizeStoreStateMachineEvents >;
} ) => {
const [ state, send, service ] = useMachine(
designWithAiStateMachineDefinition,
{
devTools: process.env.NODE_ENV === 'development',
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps -- false positive due to function name match, this isn't from react std lib
const currentNodeMeta = useSelector( service, ( currentState ) =>
findComponentMeta< DesignWithAiComponentMeta >(
currentState?.meta ?? undefined
)
);
const [ CurrentComponent, setCurrentComponent ] =
useState< DesignWithAiComponent | null >( null );
useEffect( () => {
if ( currentNodeMeta?.component ) {
setCurrentComponent( () => currentNodeMeta?.component );
}
}, [ CurrentComponent, currentNodeMeta?.component ] );
const currentNodeCssLabel =
state.value instanceof Object
? Object.keys( state.value )[ 0 ]
: state.value;
return (
<>
<h1>Design with AI</h1>
<button onClick={ () => sendEvent( { type: 'THEME_SUGGESTED' } ) }>
Assembler Hub
</button>
<div
className={ `woocommerce-design-with-ai-__container woocommerce-design-with-ai-wizard__step-${ currentNodeCssLabel }` }
>
{ CurrentComponent ? (
<CurrentComponent
sendEvent={ send }
context={ state.context }
/>
) : (
<div />
) }
</div>
</>
);
};
//loader should send event 'THEME_SUGGESTED' when it's done
export const DesignWithAi: CustomizeStoreComponent = ( { sendEvent } ) => {
return (
<>
<DesignWithAiController sendEventToParent={ sendEvent } />
</>
);
};

View File

@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
export const ApiCallLoader = ( {
context,
}: {
context: designWithAiStateMachineContext;
} ) => {
return (
<div>
<h1>Loader</h1>
<div>{ JSON.stringify( context ) }</div>
</div>
);
};

View File

@ -0,0 +1,50 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
export type businessInfoDescriptionCompleteEvent = {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE';
payload: string;
};
export const BusinessInfoDescription = ( {
sendEvent,
context,
}: {
sendEvent: ( event: businessInfoDescriptionCompleteEvent ) => void;
context: designWithAiStateMachineContext;
} ) => {
const [ businessInfoDescription, setBusinessInfoDescription ] = useState(
context.businessInfoDescription.descriptionText
);
return (
<div>
<h1>Business Info Description</h1>
<div>{ JSON.stringify( context ) }</div>
{ /* add a controlled text area that saves to state */ }
<input
type="text"
value={ businessInfoDescription }
onChange={ ( e ) =>
setBusinessInfoDescription( e.target.value )
}
/>
<button
onClick={ () =>
sendEvent( {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE',
payload: businessInfoDescription,
} )
}
>
complete
</button>
</div>
);
};

View File

@ -0,0 +1,26 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
export const LookAndFeel = ( {
sendEvent,
context,
}: {
sendEvent: ( event: { type: 'LOOK_AND_FEEL_COMPLETE' } ) => void;
context: designWithAiStateMachineContext;
} ) => {
return (
<div>
<h1>Look and Feel</h1>
<div>{ JSON.stringify( context ) }</div>
<button
onClick={ () =>
sendEvent( { type: 'LOOK_AND_FEEL_COMPLETE' } )
}
>
complete
</button>
</div>
);
};

View File

@ -0,0 +1,26 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
export const ToneOfVoice = ( {
sendEvent,
context,
}: {
sendEvent: ( event: { type: 'TONE_OF_VOICE_COMPLETE' } ) => void;
context: designWithAiStateMachineContext;
} ) => {
return (
<div>
<h1>Tone of Voice</h1>
<div>{ JSON.stringify( context ) }</div>
<button
onClick={ () =>
sendEvent( { type: 'TONE_OF_VOICE_COMPLETE' } )
}
>
complete
</button>
</div>
);
};

View File

@ -0,0 +1,4 @@
export * from './BusinessInfoDescription';
export * from './LookAndFeel';
export * from './ToneOfVoice';
export * from './ApiCallLoader';

View File

@ -0,0 +1,152 @@
/**
* External dependencies
*/
import { createMachine } from 'xstate';
/**
* Internal dependencies
*/
import {
designWithAiStateMachineContext,
designWithAiStateMachineEvents,
} from './types';
import {
BusinessInfoDescription,
LookAndFeel,
ToneOfVoice,
ApiCallLoader,
} from './pages';
import { actions } from './actions';
export const designWithAiStateMachineDefinition = createMachine(
{
id: 'designWithAi',
predictableActionArguments: true,
preserveActionOrder: true,
schema: {
context: {} as designWithAiStateMachineContext,
events: {} as designWithAiStateMachineEvents,
},
context: {
businessInfoDescription: {
descriptionText: '',
},
lookAndFeel: {
choice: '',
},
toneOfVoice: {
choice: '',
},
},
initial: 'businessInfoDescription',
states: {
businessInfoDescription: {
id: 'businessInfoDescription',
initial: 'preBusinessInfoDescription',
states: {
preBusinessInfoDescription: {
// if we need to prefetch options, other settings previously populated from core profiler, do it here
always: {
target: 'businessInfoDescription',
},
},
businessInfoDescription: {
meta: {
component: BusinessInfoDescription,
},
on: {
BUSINESS_INFO_DESCRIPTION_COMPLETE: {
actions: [ 'assignBusinessInfoDescription' ],
target: 'postBusinessInfoDescription',
},
},
},
postBusinessInfoDescription: {
always: {
target: '#lookAndFeel',
},
},
},
},
lookAndFeel: {
id: 'lookAndFeel',
initial: 'preLookAndFeel',
states: {
preLookAndFeel: {
always: {
target: 'lookAndFeel',
},
},
lookAndFeel: {
meta: {
component: LookAndFeel,
},
on: {
LOOK_AND_FEEL_COMPLETE: {
target: 'postLookAndFeel',
},
},
},
postLookAndFeel: {
always: {
target: '#toneOfVoice',
},
},
},
},
toneOfVoice: {
id: 'toneOfVoice',
initial: 'preToneOfVoice',
states: {
preToneOfVoice: {
always: {
target: 'toneOfVoice',
},
},
toneOfVoice: {
meta: {
component: ToneOfVoice,
},
on: {
TONE_OF_VOICE_COMPLETE: {
target: 'postToneOfVoice',
},
},
},
postToneOfVoice: {
always: {
target: '#apiCallLoader',
},
},
},
},
apiCallLoader: {
id: 'apiCallLoader',
initial: 'preApiCallLoader',
states: {
preApiCallLoader: {
always: {
target: 'apiCallLoader',
},
},
apiCallLoader: {
meta: {
component: ApiCallLoader,
},
},
postApiCallLoader: {},
},
},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
// TODO: handle this event when the 'x' is clicked at any point
// probably bail (to where?) and log the tracks for which step it is in plus
// whatever details might be helpful to know why they bailed
},
},
},
{
actions,
}
);

View File

@ -0,0 +1,18 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { ApiCallLoader } from '../pages';
import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const ApiCallLoaderPage = () => (
<ApiCallLoader
context={ {} as designWithAiStateMachineContext }
/>
);
export default {
title: 'WooCommerce Admin/Application/Customize Store/Design with AI/API Call Loader',
component: ApiCallLoader,
decorators: [ WithCustomizeYourStoreLayout ],
};

View File

@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { BusinessInfoDescription } from '../pages';
import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const BusinessInfoDescriptionPage = () => (
<BusinessInfoDescription
context={ {} as designWithAiStateMachineContext }
sendEvent={ () => {} }
/>
);
export default {
title: 'WooCommerce Admin/Application/Customize Store/Design with AI/Business Info Description',
component: BusinessInfoDescription,
decorators: [ WithCustomizeYourStoreLayout ],
};

View File

@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { LookAndFeel } from '../pages';
import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const LookAndFeelPage = () => (
<LookAndFeel
context={ {} as designWithAiStateMachineContext }
sendEvent={ () => {} }
/>
);
export default {
title: 'WooCommerce Admin/Application/Customize Store/Design with AI/Look and Feel',
component: LookAndFeel,
decorators: [ WithCustomizeYourStoreLayout ],
};

View File

@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
import { designWithAiStateMachineContext } from '../types';
import { ToneOfVoice } from '../pages';
import { WithCustomizeYourStoreLayout } from './WithCustomizeYourStoreLayout';
export const ToneOfVoicePage = () => (
<ToneOfVoice
context={ {} as designWithAiStateMachineContext }
sendEvent={ () => {} }
/>
);
export default {
title: 'WooCommerce Admin/Application/Customize Store/Design with AI/Tone of Voice',
component: ToneOfVoice,
decorators: [ WithCustomizeYourStoreLayout ],
};

View File

@ -0,0 +1,12 @@
/**
* Internal dependencies
*/
import '../../style.scss';
export const WithCustomizeYourStoreLayout = ( Story: React.ComponentType ) => {
return (
<div className="woocommerce-customize-store woocommerce-admin-full-screen">
<Story />
</div>
);
};

View File

@ -0,0 +1,31 @@
export type designWithAiStateMachineContext = {
businessInfoDescription: {
descriptionText: string;
};
lookAndFeel: {
choice: string;
};
toneOfVoice: {
choice: string;
};
// If we require more data from options, previously provided core profiler details,
// we can retrieve them in preBusinessInfoDescription and then assign them here
};
export type designWithAiStateMachineEvents =
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION' }
| {
type: 'BUSINESS_INFO_DESCRIPTION_COMPLETE';
payload: string;
}
| {
type: 'LOOK_AND_FEEL_COMPLETE';
}
| {
type: 'TONE_OF_VOICE_COMPLETE';
}
| {
type: 'API_CALL_TO_AI_SUCCCESSFUL';
}
| {
type: 'API_CALL_TO_AI_FAILED';
};

View File

@ -25,6 +25,8 @@ import {
} from './types';
import { ThemeCard } from './intro/theme-cards';
import './style.scss';
export type customizeStoreStateMachineEvents =
| introEvents
| designWithAiEvents

View File

@ -0,0 +1,18 @@
.woocommerce-layout .woocommerce-layout__main {
@include breakpoint( '<782px' ) {
padding-top: 0 !important;
}
}
.woocommerce-customize-store {
background-color: #fff;
#woocommerce-layout__primary {
margin: 0;
width: 100%;
}
.woocommerce-layout__main {
padding-right: 0;
}
}

View File

@ -0,0 +1,4 @@
Significance: minor
Type: update
Added xstate scaffolding for AI Wizard in customize your store feature