add: url navigation to cys (#40068)

* add: url navigation to cys

* bugfix for url not updating

* url handling for design-with-ai

* fixed url syncing so that it's working with @woocommerce/navigation

* changed useLocation to useQuery in assembler-hub save-hub
This commit is contained in:
RJ 2023-09-12 16:32:50 +10:00 committed by GitHub
parent c721159129
commit e52d11a87e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 275 additions and 66 deletions

View File

@ -13,10 +13,6 @@ import {
// @ts-ignore No types for this exist yet.
__experimentalUseNavigator as useNavigator,
} from '@wordpress/components';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
/**
* Internal dependencies
@ -31,8 +27,12 @@ import { SidebarNavigationScreenPages } from './sidebar-navigation-screen-pages'
import { SidebarNavigationScreenLogo } from './sidebar-navigation-screen-logo';
import { SaveHub } from './save-hub';
const { useLocation, useHistory } = unlock( routerPrivateApis );
import {
addHistoryListener,
getQuery,
updateQueryString,
useQuery,
} from '@woocommerce/navigation';
function isSubset(
subset: {
@ -48,18 +48,27 @@ function isSubset(
}
function useSyncPathWithURL() {
const history = useHistory();
const { params: urlParams } = useLocation();
const { location: navigatorLocation, params: navigatorParams } =
useNavigator();
const urlParams = useQuery();
const {
location: navigatorLocation,
params: navigatorParams,
goTo,
} = useNavigator();
const isMounting = useRef( true );
useEffect(
() => {
// The navigatorParams are only initially filled properly when the
// navigator screens mount. so we ignore the first synchronisation.
// The navigatorParams are only initially filled properly after the
// navigator screens mounts. so we don't do the query string update initially.
// however we also do want to add an event listener for popstate so that we can
// update the navigator when the user navigates using the browser back button
if ( isMounting.current ) {
isMounting.current = false;
addHistoryListener( ( event: PopStateEvent ) => {
if ( event.type === 'popstate' ) {
goTo( ( getQuery() as Record< string, string > ).path );
}
} );
return;
}
@ -73,7 +82,7 @@ function useSyncPathWithURL() {
...urlParams,
...newUrlParams,
};
history.push( updatedParams );
updateQueryString( {}, updatedParams.path );
}
updateUrlParams( {
@ -97,28 +106,28 @@ function SidebarScreens() {
useSyncPathWithURL();
return (
<>
<NavigatorScreen path="/customize-store">
<NavigatorScreen path="/customize-store/assembler-hub">
<SidebarNavigationScreenMain />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/color-palette">
<NavigatorScreen path="/customize-store/assembler-hub/color-palette">
<SidebarNavigationScreenColorPalette />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/typography">
<NavigatorScreen path="/customize-store/assembler-hub/typography">
<SidebarNavigationScreenTypography />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/header">
<NavigatorScreen path="/customize-store/assembler-hub/header">
<SidebarNavigationScreenHeader />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/homepage">
<NavigatorScreen path="/customize-store/assembler-hub/homepage">
<SidebarNavigationScreenHomepage />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/footer">
<NavigatorScreen path="/customize-store/assembler-hub/footer">
<SidebarNavigationScreenFooter />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/pages">
<NavigatorScreen path="/customize-store/assembler-hub/pages">
<SidebarNavigationScreenPages />
</NavigatorScreen>
<NavigatorScreen path="/customize-store/logo">
<NavigatorScreen path="/customize-store/assembler-hub/logo">
<SidebarNavigationScreenLogo />
</NavigatorScreen>
</>
@ -126,8 +135,10 @@ function SidebarScreens() {
}
function Sidebar() {
const { params: urlParams } = useLocation();
const initialPath = useRef( urlParams.path ?? '/customize-store' );
const urlParams = getQuery() as Record< string, string >;
const initialPath = useRef(
urlParams.path ?? '/customize-store/assembler-hub'
);
return (
<>
<NavigatorProvider

View File

@ -5,7 +5,7 @@
* External dependencies
*/
import { useContext, useEffect } from '@wordpress/element';
import { useQuery } from '@woocommerce/navigation';
import { useSelect, useDispatch } from '@wordpress/data';
// @ts-ignore No types for this exist yet.
import { Button, __experimentalHStack as HStack } from '@wordpress/components';
@ -14,14 +14,9 @@ import { __ } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
// @ts-ignore No types for this exist yet.
import { store as blockEditorStore } from '@wordpress/block-editor';
// @ts-ignore No types for this exist yet.
import { privateApis as routerPrivateApis } from '@wordpress/router';
// @ts-ignore No types for this exist yet.
import { store as noticesStore } from '@wordpress/notices';
// @ts-ignore No types for this exist yet.
import { unlock } from '@wordpress/edit-site/build-module/lock-unlock';
// @ts-ignore No types for this exist yet.
import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
/**
@ -29,8 +24,6 @@ import { useEntitiesSavedStatesIsDirty as useIsDirty } from '@wordpress/editor';
*/
import { CustomizeStoreContext } from '../';
const { useLocation } = unlock( routerPrivateApis );
const PUBLISH_ON_SAVE_ENTITIES = [
{
kind: 'postType',
@ -40,7 +33,7 @@ const PUBLISH_ON_SAVE_ENTITIES = [
export const SaveHub = () => {
const saveNoticeId = 'site-edit-save-notice';
const { params } = useLocation();
const urlParams = useQuery();
const { sendEvent } = useContext( CustomizeStoreContext );
// @ts-ignore No types for this exist yet.
@ -141,7 +134,7 @@ export const SaveHub = () => {
} );
// Only run when path changes.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ params.path ] );
}, [ urlParams.path ] );
const save = async () => {
removeNotice( saveNoticeId );
@ -182,7 +175,7 @@ export const SaveHub = () => {
};
const renderButton = () => {
if ( params.path === '/customize-store' ) {
if ( urlParams.path === '/customize-store/assembler-hub' ) {
return (
<Button
variant="primary"

View File

@ -61,7 +61,7 @@ export const SidebarNavigationScreenMain = () => {
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/logo"
path="/customize-store/assembler-hub/logo"
withChevron
icon={ siteLogo }
>
@ -69,7 +69,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/color-palette"
path="/customize-store/assembler-hub/color-palette"
withChevron
icon={ color }
>
@ -77,7 +77,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/typography"
path="/customize-store/assembler-hub/typography"
withChevron
icon={ typography }
>
@ -92,7 +92,7 @@ export const SidebarNavigationScreenMain = () => {
<ItemGroup>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/header"
path="/customize-store/assembler-hub/header"
withChevron
icon={ header }
>
@ -100,7 +100,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/homepage"
path="/customize-store/assembler-hub/homepage"
withChevron
icon={ home }
>
@ -108,7 +108,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/footer"
path="/customize-store/assembler-hub/footer"
withChevron
icon={ footer }
>
@ -116,7 +116,7 @@ export const SidebarNavigationScreenMain = () => {
</NavigatorButton>
<NavigatorButton
as={ SidebarNavigationItem }
path="/customize-store/pages"
path="/customize-store/assembler-hub/pages"
withChevron
icon={ pages }
>

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { assign } from 'xstate';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -75,10 +76,34 @@ const logAIAPIRequestError = () => {
console.log( 'API Request error' );
};
const updateQueryStep = (
_context: unknown,
_evt: unknown,
{ action }: { action: unknown }
) => {
const { path } = getQuery() as { path: string };
const step = ( action as { step: string } ).step;
const pathFragments = path.split( '/' ); // [0] '', [1] 'customize-store', [2] cys step slug [3] design-with-ai step slug
if (
pathFragments[ 1 ] === 'customize-store' &&
pathFragments[ 2 ] === 'design-with-ai'
) {
if ( pathFragments[ 3 ] !== step ) {
// this state machine is only concerned with [2], so we ignore changes to [3]
// [1] is handled by router at root of wc-admin
updateQueryString(
{},
`/customize-store/design-with-ai/${ step }`
);
}
}
};
export const actions = {
assignBusinessInfoDescription,
assignLookAndFeel,
assignToneOfVoice,
assignLookAndTone,
logAIAPIRequestError,
updateQueryStep,
};

View File

@ -4,6 +4,7 @@
import { __experimentalRequestJetpackToken as requestJetpackToken } from '@woocommerce/ai';
import apiFetch from '@wordpress/api-fetch';
import { recordEvent } from '@woocommerce/tracks';
import { Sender } from 'xstate';
/**
* Internal dependencies
@ -101,6 +102,18 @@ export const getLookAndTone = async (
return parseLookAndToneCompletionResponse( data );
};
const browserPopstateHandler =
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
const popstateHandler = () => {
sendBack( { type: 'EXTERNAL_URL_UPDATE' } );
};
window.addEventListener( 'popstate', popstateHandler );
return () => {
window.removeEventListener( 'popstate', popstateHandler );
};
};
export const services = {
getLookAndTone,
browserPopstateHandler,
};

View File

@ -2,6 +2,7 @@
* External dependencies
*/
import { createMachine, sendParent } from 'xstate';
import { getQuery } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -18,6 +19,20 @@ import {
} from './pages';
import { actions } from './actions';
import { services } from './services';
export const hasStepInUrl = (
_ctx: unknown,
_evt: unknown,
{ cond }: { cond: unknown }
) => {
const { path = '' } = getQuery() as { path: string };
const pathFragments = path.split( '/' );
return (
pathFragments[ 3 ] === // [0] '', [1] 'customize-store', [2] cys step slug [3] design-with-ai step slug
( cond as { step: string | undefined } ).step
);
};
export const designWithAiStateMachineDefinition = createMachine(
{
id: 'designWithAi',
@ -27,6 +42,17 @@ export const designWithAiStateMachineDefinition = createMachine(
context: {} as designWithAiStateMachineContext,
events: {} as designWithAiStateMachineEvents,
},
invoke: {
src: 'browserPopstateHandler',
},
on: {
EXTERNAL_URL_UPDATE: {
target: 'navigate',
},
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: sendParent( ( _context, event ) => event ),
},
},
context: {
businessInfoDescription: {
descriptionText: '',
@ -39,8 +65,43 @@ export const designWithAiStateMachineDefinition = createMachine(
choice: '',
},
},
initial: 'businessInfoDescription',
initial: 'navigate',
states: {
navigate: {
always: [
{
target: 'businessInfoDescription',
cond: {
type: 'hasStepInUrl',
step: 'business-info-description',
},
},
{
target: 'lookAndFeel',
cond: {
type: 'hasStepInUrl',
step: 'look-and-feel',
},
},
{
target: 'toneOfVoice',
cond: {
type: 'hasStepInUrl',
step: 'tone-of-voice',
},
},
{
target: 'apiCallLoader',
cond: {
type: 'hasStepInUrl',
step: 'api-call-loader',
},
},
{
target: 'businessInfoDescription',
},
],
},
businessInfoDescription: {
id: 'businessInfoDescription',
initial: 'preBusinessInfoDescription',
@ -90,6 +151,12 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: LookAndFeel,
},
entry: [
{
type: 'updateQueryStep',
step: 'look-and-feel',
},
],
on: {
LOOK_AND_FEEL_COMPLETE: {
actions: [ 'assignLookAndFeel' ],
@ -117,6 +184,12 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: ToneOfVoice,
},
entry: [
{
type: 'updateQueryStep',
step: 'tone-of-voice',
},
],
on: {
TONE_OF_VOICE_COMPLETE: {
actions: [ 'assignToneOfVoice' ],
@ -144,19 +217,23 @@ export const designWithAiStateMachineDefinition = createMachine(
meta: {
component: ApiCallLoader,
},
entry: [
{
type: 'updateQueryStep',
step: 'api-call-loader',
},
],
},
postApiCallLoader: {},
},
},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: sendParent( ( _context, event ) => event ),
},
},
},
{
actions,
services,
guards: {
hasStepInUrl,
},
}
);

View File

@ -24,10 +24,7 @@ export type designWithAiStateMachineEvents =
type: 'TONE_OF_VOICE_COMPLETE';
}
| {
type: 'API_CALL_TO_AI_SUCCCESSFUL';
}
| {
type: 'API_CALL_TO_AI_FAILED';
type: 'EXTERNAL_URL_UPDATE';
};
export const VALID_LOOKS = [ 'Contemporary', 'Classic', 'Bold' ] as const;

View File

@ -1,9 +1,10 @@
/**
* External dependencies
*/
import { createMachine } from 'xstate';
import { Sender, createMachine } from 'xstate';
import { useEffect, useMemo, useState } from '@wordpress/element';
import { useMachine, useSelector } from '@xstate/react';
import { getQuery, updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
@ -30,18 +31,53 @@ export type customizeStoreStateMachineEvents =
| introEvents
| designWithAiEvents
| assemblerHubEvents
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } };
| { type: 'AI_WIZARD_CLOSED_BEFORE_COMPLETION'; payload: { step: string } }
| { type: 'EXTERNAL_URL_UPDATE' };
export const customizeStoreStateMachineServices = {
...introServices,
const updateQueryStep = (
_context: unknown,
_evt: unknown,
{ action }: { action: unknown }
) => {
const { path } = getQuery() as { path: string };
const step = ( action as { step: string } ).step;
const pathFragments = path.split( '/' ); // [0] '', [1] 'customize-store', [2] step slug [3] design-with-ai, assembler-hub path fragments
if ( pathFragments[ 1 ] === 'customize-store' ) {
if ( pathFragments[ 2 ] !== step ) {
// this state machine is only concerned with [2], so we ignore changes to [3]
// [1] is handled by router at root of wc-admin
updateQueryString( {}, `/customize-store/${ step }` );
}
}
};
const browserPopstateHandler =
() => ( sendBack: Sender< { type: 'EXTERNAL_URL_UPDATE' } > ) => {
const popstateHandler = () => {
sendBack( { type: 'EXTERNAL_URL_UPDATE' } );
};
window.addEventListener( 'popstate', popstateHandler );
return () => {
window.removeEventListener( 'popstate', popstateHandler );
};
};
export const machineActions = {
updateQueryStep,
};
export const customizeStoreStateMachineActions = {
...introActions,
...machineActions,
};
export const customizeStoreStateMachineServices = {
...introServices,
browserPopstateHandler,
};
export const customizeStoreStateMachineDefinition = createMachine( {
id: 'customizeStore',
initial: 'intro',
initial: 'navigate',
predictableActionArguments: true,
preserveActionOrder: true,
schema: {
@ -57,7 +93,47 @@ export const customizeStoreStateMachineDefinition = createMachine( {
activeTheme: '',
},
} as customizeStoreStateMachineContext,
invoke: {
src: 'browserPopstateHandler',
},
on: {
EXTERNAL_URL_UPDATE: {
target: 'navigate',
},
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
target: 'intro',
actions: [ { type: 'updateQueryStep', step: 'intro' } ],
},
},
states: {
navigate: {
always: [
{
target: 'intro',
cond: {
type: 'hasStepInUrl',
step: 'intro',
},
},
{
target: 'designWithAi',
cond: {
type: 'hasStepInUrl',
step: 'design-with-ai',
},
},
{
target: 'assemblerHub',
cond: {
type: 'hasStepInUrl',
step: 'assembler-hub',
},
},
{
target: 'intro',
},
],
},
intro: {
id: 'intro',
initial: 'preIntro',
@ -107,6 +183,9 @@ export const customizeStoreStateMachineDefinition = createMachine( {
meta: {
component: DesignWithAi,
},
entry: [
{ type: 'updateQueryStep', step: 'design-with-ai' },
],
},
},
on: {
@ -116,9 +195,17 @@ export const customizeStoreStateMachineDefinition = createMachine( {
},
},
assemblerHub: {
initial: 'assemblerHub',
states: {
assemblerHub: {
entry: [
{ type: 'updateQueryStep', step: 'assembler-hub' },
],
meta: {
component: AssemblerHub,
},
},
},
on: {
FINISH_CUSTOMIZATION: {
target: 'backToHomescreen',
@ -131,13 +218,6 @@ export const customizeStoreStateMachineDefinition = createMachine( {
backToHomescreen: {},
appearanceTask: {},
},
on: {
AI_WIZARD_CLOSED_BEFORE_COMPLETION: {
actions: () => {
// TODO: when navigation has been implemented, this should navigate back to the Intro screen
},
},
},
} );
export const CustomizeStoreController = ( {
@ -159,7 +239,16 @@ export const CustomizeStoreController = ( {
...customizeStoreStateMachineActions,
...actionOverrides,
},
guards: {},
guards: {
hasStepInUrl: ( _ctx, _evt, { cond }: { cond: unknown } ) => {
const { path = '' } = getQuery() as { path: string };
const pathFragments = path.split( '/' );
return (
pathFragments[ 2 ] === // [0] '', [1] 'customize-store', [2] step slug
( cond as { step: string | undefined } ).step
);
},
},
} );
}, [ actionOverrides, servicesOverrides ] );

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Added URL navigation support to customize-store feature