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
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { CustomizeStoreComponent } from '../types';
|
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>Design with AI</h1>
|
<div
|
||||||
<button onClick={ () => sendEvent( { type: 'THEME_SUGGESTED' } ) }>
|
className={ `woocommerce-design-with-ai-__container woocommerce-design-with-ai-wizard__step-${ currentNodeCssLabel }` }
|
||||||
Assembler Hub
|
>
|
||||||
</button>
|
{ 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';
|
} from './types';
|
||||||
import { ThemeCard } from './intro/theme-cards';
|
import { ThemeCard } from './intro/theme-cards';
|
||||||
|
|
||||||
|
import './style.scss';
|
||||||
|
|
||||||
export type customizeStoreStateMachineEvents =
|
export type customizeStoreStateMachineEvents =
|
||||||
| introEvents
|
| introEvents
|
||||||
| designWithAiEvents
|
| 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