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:
parent
3c25538f35
commit
af9ef856e5
|
@ -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,
|
||||
};
|
|
@ -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 } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export * from './BusinessInfoDescription';
|
||||
export * from './LookAndFeel';
|
||||
export * from './ToneOfVoice';
|
||||
export * from './ApiCallLoader';
|
|
@ -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,
|
||||
}
|
||||
);
|
|
@ -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 ],
|
||||
};
|
|
@ -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 ],
|
||||
};
|
|
@ -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 ],
|
||||
};
|
|
@ -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 ],
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
};
|
|
@ -25,6 +25,8 @@ import {
|
|||
} from './types';
|
||||
import { ThemeCard } from './intro/theme-cards';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
export type customizeStoreStateMachineEvents =
|
||||
| introEvents
|
||||
| designWithAiEvents
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Added xstate scaffolding for AI Wizard in customize your store feature
|
Loading…
Reference in New Issue