Skip to content

Commit

Permalink
Merge pull request #3198 from Automattic/add/saved-progress
Browse files Browse the repository at this point in the history
Integrate store state with stepper navigation
  • Loading branch information
yscik authored Jun 2, 2020
2 parents e802c45 + 31e634c commit 1afc25d
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 122 deletions.
8 changes: 5 additions & 3 deletions assets/onboarding/data/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@ export const startSubmit = ( step, stepData ) => ( {
/**
* Success submit action creator.
*
* @return {{type: string}} Success submit action.
* @param {string} step Completed step.
* @return {{type: string, step: string}} Success submit action.
*/
export const successSubmit = () => ( {
export const successSubmit = ( step ) => ( {
type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA,
step,
} );

/**
Expand Down Expand Up @@ -146,7 +148,7 @@ export function* submitStep( step, stepData, { onSuccess, onError } = {} ) {
method: 'POST',
data: stepData,
} );
yield successSubmit();
yield successSubmit( step );
yield setStepData( step, stepData );

if ( onSuccess ) {
Expand Down
8 changes: 6 additions & 2 deletions assets/onboarding/data/actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,12 @@ describe( 'Setup wizard actions', () => {
} );

it( 'Should return the success submit action', () => {
const expectedAction = { type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA };
const expectedAction = {
type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA,
step: 'test',
};

expect( successSubmit() ).toEqual( expectedAction );
expect( successSubmit( 'test' ) ).toEqual( expectedAction );
} );

it( 'Should return the error submit action', () => {
Expand Down Expand Up @@ -189,6 +192,7 @@ describe( 'Setup wizard actions', () => {
// Success action.
const expectedSuccessAction = {
type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA,
step: 'welcome',
};
expect( gen.next().value ).toEqual( expectedSuccessAction );

Expand Down
10 changes: 9 additions & 1 deletion assets/onboarding/data/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import {
import { INSTALLING_STATUS } from '../features/feature-status';

const DEFAULT_STATE = {
isFetching: false,
isFetching: true,
fetchError: false,
isSubmitting: false,
submitError: false,
data: {
completedSteps: [],
welcome: {
usage_tracking: false,
},
Expand Down Expand Up @@ -122,6 +123,13 @@ export default ( state = DEFAULT_STATE, action ) => {
return {
...state,
isSubmitting: false,
data: {
...state.data,
completedSteps: [
...state.data.completedSteps,
action.step,
],
},
};

case ERROR_SUBMIT_SETUP_WIZARD_DATA:
Expand Down
17 changes: 17 additions & 0 deletions assets/onboarding/data/reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,32 @@ describe( 'Setup wizard reducer', () => {
const state = reducer(
{
isSubmitting: true,
data: { completedSteps: [] },
},
{
type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA,
step: 'test',
}
);

expect( state.isSubmitting ).toBeFalsy();
} );

it( 'Should mark step as completed on SUCCESS_SUBMIT_SETUP_WIZARD_DATA action', () => {
const state = reducer(
{
isSubmitting: true,
data: { completedSteps: [] },
},
{
type: SUCCESS_SUBMIT_SETUP_WIZARD_DATA,
step: 'test',
}
);

expect( state.data.completedSteps ).toContain( 'test' );
} );

it( 'Should set error on ERROR_SUBMIT_SETUP_WIZARD_DATA action', () => {
const error = { msg: 'Error' };
const state = reducer(
Expand Down
22 changes: 22 additions & 0 deletions assets/onboarding/data/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ export const getSubmitError = ( state ) => state.submitError;
*/
/* eslint-enable */
export const getStepData = ( state, step ) => state.data[ step ];

/**
* Get navigation steps with their state.
*
* @param {Object} state current state.
* @param {Array} steps List of steps.
*
* @return {Array} Navigation steps.
*/
export const getNavigationSteps = ( { data: { completedSteps } }, steps ) => {
const navSteps = steps.map( ( step ) => ( {
...step,
isComplete: completedSteps.includes( step.key ),
isNext: false,
} ) );

const nextStep =
navSteps.find( ( step ) => ! step.isComplete ) || navSteps[ 0 ];
nextStep.isNext = true;

return navSteps;
};
19 changes: 19 additions & 0 deletions assets/onboarding/data/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isSubmitting,
getSubmitError,
getStepData,
getNavigationSteps,
} from './selectors';

describe( 'Setup wizard selectors', () => {
Expand Down Expand Up @@ -54,4 +55,22 @@ describe( 'Setup wizard selectors', () => {
usage_tracking: true,
} );
} );

it( 'Should get navigation data', () => {
const state = {
data: {
completedSteps: [ 'welcome' ],
},
};

expect(
getNavigationSteps( state, [
{ key: 'welcome' },
{ key: 'features' },
] )
).toEqual( [
{ key: 'welcome', isComplete: true, isNext: false },
{ key: 'features', isComplete: false, isNext: true },
] );
} );
} );
8 changes: 4 additions & 4 deletions assets/onboarding/features/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ describe( '<Features />', () => {

it( 'Should continue to the ready step when nothing is selected', () => {
const { container, queryByText } = render(
<QueryStringRouter paramName="step">
<Route route="features" defaultRoute>
<QueryStringRouter paramName="step" defaultRoute="features">
<Route route="features">
<Features />
</Route>
<Route route="ready">Ready</Route>
Expand All @@ -67,8 +67,8 @@ describe( '<Features />', () => {

it( 'Should continue to the ready step when the user chooses to install later', () => {
const { container, queryByText } = render(
<QueryStringRouter paramName="step">
<Route route="features" defaultRoute>
<QueryStringRouter paramName="step" defaultRoute="features">
<Route route="features">
<Features />
</Route>
<Route route="ready">Ready</Route>
Expand Down
34 changes: 19 additions & 15 deletions assets/onboarding/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { __ } from '@wordpress/i18n';

import registerSetupWizardStore from './data';
import { useWpAdminFullscreen } from '../react-hooks';
import { steps } from './steps';

import QueryStringRouter, { Route } from './query-string-router';
import Navigation from './navigation';
import { steps } from './steps';

/**
* Register setup wizard store.
Expand All @@ -26,12 +27,16 @@ const PARAM_NAME = 'step';
const SenseiSetupWizardPage = () => {
useWpAdminFullscreen( [ 'sensei-color', 'sensei-onboarding__page' ] );

const { isFetching, error } = useSelect(
( select ) => ( {
isFetching: select( 'sensei/setup-wizard' ).isFetching(),
error: select( 'sensei/setup-wizard' ).getFetchError(),
} ),
[]
const { isFetching, error, navigationSteps } = useSelect(
( select ) => {
const store = select( 'sensei/setup-wizard' );
return {
isFetching: store.isFetching(),
error: store.getFetchError(),
navigationSteps: store.getNavigationSteps( steps ),
};
},
[ steps ]
);
const { fetchSetupWizardData } = useDispatch( 'sensei/setup-wizard' );

Expand All @@ -58,17 +63,16 @@ const SenseiSetupWizardPage = () => {
}

return (
<QueryStringRouter paramName={ PARAM_NAME }>
<QueryStringRouter
paramName={ PARAM_NAME }
defaultRoute={ navigationSteps.find( ( step ) => step.isNext ).key }
>
<div className="sensei-onboarding__header">
<Navigation steps={ steps } />
<Navigation steps={ navigationSteps } />
</div>
<div className="sensei-onboarding__container">
{ steps.map( ( step, i ) => (
<Route
key={ step.key }
route={ step.key }
defaultRoute={ 0 === i }
>
{ navigationSteps.map( ( step ) => (
<Route key={ step.key } route={ step.key }>
{ step.container }
</Route>
) ) }
Expand Down
69 changes: 13 additions & 56 deletions assets/onboarding/navigation/index.jsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,30 @@
import { useState, useEffect, useMemo } from '@wordpress/element';
import { Stepper } from '@woocommerce/components';
import { get, uniq } from 'lodash';

import { useQueryStringRouter } from '../query-string-router';

/**
* @typedef {Object} Step
* @property {string} key Step key.
*/
/**
* @typedef {Object} StepWithNavigationState
* @property {boolean} [isComplete] Flag if it is complete.
* @property {Function} [onClick] Function to navigate to the step (Enables the click on the Stepper).
*/
/**
* Merge the navigation state into the steps.
* Add isComplete and onClick - when visited.
* Go to route when clicking steps that can be active (completed or next).
*
* @param {Step[]} steps Steps list.
* @param {string[]} visitedSteps Key of the visited steps.
* @param {Function} goTo Function that update the step.
* @param {Array} steps
* @param {Object} deps
* @param {Function} deps.goTo
*
* @return {StepWithNavigationState} Steps with navigation state merged.
* @return {Array} Steps with click handlers.
*/
const getStepsWithNavigationState = ( steps, visitedSteps, goTo ) =>
steps.map( ( step, index ) => {
const nextKey = get( steps, [ index + 1, 'key' ], null );

const stepWithNavigationState = {
...step,
isComplete: nextKey && visitedSteps.includes( nextKey ),
};

if ( visitedSteps.includes( step.key ) ) {
stepWithNavigationState.onClick = () => {
goTo( step.key );
};
}

return stepWithNavigationState;
} );
const addClickHandlers = ( steps, { goTo } ) =>
steps.map( ( step ) => ( {
...step,
onClick:
step.isComplete || step.isNext ? () => goTo( step.key ) : undefined,
} ) );

/**
* Navigation component.
*/
const Navigation = ( { steps } ) => {
const { currentRoute, goTo } = useQueryStringRouter();
steps = addClickHandlers( steps, { goTo } );

// Visited steps.
const [ visitedSteps, setVisitedSteps ] = useState( [] );

useEffect( () => {
setVisitedSteps( ( prevState ) =>
uniq( [ ...prevState, currentRoute ] )
);
}, [ currentRoute ] );

// Update steps with navigation state.
const stepsWithNavigationState = useMemo(
() => getStepsWithNavigationState( steps, visitedSteps, goTo ),
[ steps, visitedSteps, goTo ]
);

return (
<Stepper
steps={ stepsWithNavigationState }
currentStep={ currentRoute || steps[ 0 ].key }
/>
);
return <Stepper steps={ steps } currentStep={ currentRoute } />;
};

export default Navigation;
13 changes: 9 additions & 4 deletions assets/onboarding/query-string-router/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ const QueryStringRouterContext = createContext();
* - `goTo`: Functions that send the user to another route.
*
* @param {Object} props
* @param {string} props.paramName Param used as reference in the query string.
* @param {Object} props.children Render this children if it matches the route.
* @param {string} props.paramName Param used as reference in the query string.
* @param {string} props.defaultRoute Default route to open if there is nothing in the URL.
* @param {Object} props.children Render this children if it matches the route.
*/
const QueryStringRouter = ( { paramName, children } ) => {
const QueryStringRouter = ( { paramName, defaultRoute, children } ) => {
// Current route.
const [ currentRoute, setRoute ] = useState(
getCurrentRouteFromURL( paramName )
Expand All @@ -43,11 +44,15 @@ const QueryStringRouter = ( { paramName, children } ) => {
setRoute( newRoute );
};

if ( ! currentRoute ) {
goTo( defaultRoute, true );
}

return {
currentRoute,
goTo,
};
}, [ currentRoute, paramName ] );
}, [ currentRoute, paramName, defaultRoute ] );

// Handle history changes through popstate.
useEventListener(
Expand Down
8 changes: 4 additions & 4 deletions assets/onboarding/query-string-router/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const NextButton = ( { nextKey } ) => {
describe( '<QueryStringRouter />', () => {
it( 'Should navigate to the next route', () => {
const { queryByText, queryByTestId } = render(
<QueryStringRouter paramName="route">
<Route route="one" defaultRoute>
<QueryStringRouter paramName="route" defaultRoute="one">
<Route route="one">
One <NextButton nextKey="two" />
</Route>
<Route route="two">
Expand All @@ -36,8 +36,8 @@ describe( '<QueryStringRouter />', () => {

it( 'Should go to the correct route after on popstate', () => {
const { queryByText } = render(
<QueryStringRouter paramName="route">
<Route route="one" defaultRoute>
<QueryStringRouter paramName="route" defaultRoute="one">
<Route route="one">
One <NextButton nextKey="two" />
</Route>
<Route route="two">
Expand Down
Loading

0 comments on commit 1afc25d

Please sign in to comment.