diff --git a/src/app/components/Step/StepValidationWrapper.js b/src/app/components/Step/StepValidationWrapper.js new file mode 100644 index 000000000..ef26ee2f9 --- /dev/null +++ b/src/app/components/Step/StepValidationWrapper.js @@ -0,0 +1,40 @@ +import { useEffect } from '@wordpress/element'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { shouldRedirectToFirstStep } from '../../utils/helpers/stepValidation'; + +/** + * Higher-order component that validates step data and redirects if necessary + * + * @param {React.Component} WrappedComponent - The step component to wrap + * @param {string} stepKey - The key of the step for validation + * @return {React.Component} - The wrapped component with validation logic + */ +export const withStepValidation = ( WrappedComponent, stepKey ) => { + const StepValidationWrapper = ( props ) => { + const navigate = useNavigate(); + const location = useLocation(); + + useEffect( () => { + // Check if we should redirect to the first step + if ( shouldRedirectToFirstStep( stepKey ) ) { + // Only redirect if we're not already on the first step + if ( location.pathname !== '/' ) { + navigate( '/', { replace: true } ); + } + } + }, [ navigate, location.pathname ] ); + + // If we should redirect, don't render the component + if ( shouldRedirectToFirstStep( stepKey ) ) { + return null; + } + + // Otherwise, render the wrapped component + return ; + }; + + // Set display name for debugging + StepValidationWrapper.displayName = `withStepValidation(${ WrappedComponent.displayName || WrappedComponent.name || 'Component' })`; + + return StepValidationWrapper; +}; diff --git a/src/app/steps/Migration/MigrationStep.js b/src/app/steps/Migration/MigrationStep.js index 10da48501..6c3c3100e 100644 --- a/src/app/steps/Migration/MigrationStep.js +++ b/src/app/steps/Migration/MigrationStep.js @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { Container, Title, Spinner } from '@newfold/ui-component-library'; import { ExclamationCircleIcon } from '@heroicons/react/24/solid'; @@ -25,6 +25,7 @@ const MigrationStep = () => { /** * Track migration initiated event + * * @param { string } instaWpMigrationUrl The migration url * @return { Promise } */ @@ -46,10 +47,6 @@ const MigrationStep = () => { ); }; - useEffect( () => { - prepareMigration(); - }, [ prepareMigration ] ); - const prepareMigration = useCallback( async () => { try { if ( ! canMigrateSite ) { @@ -89,6 +86,10 @@ const MigrationStep = () => { } }, [ canMigrateSite, setInstaWpMigrationUrl ] ); + useEffect( () => { + prepareMigration(); + }, [ prepareMigration ] ); + /** * @return { string } The title content */ diff --git a/src/app/steps/index.js b/src/app/steps/index.js index d574885d9..128a6afa3 100644 --- a/src/app/steps/index.js +++ b/src/app/steps/index.js @@ -5,49 +5,50 @@ import { GeneratingStep } from './Generating'; import { PreviewsStep } from './Previews'; import { CanvasStep } from './Canvas'; import { MigrationStep } from './Migration'; +import { withStepValidation } from '../components/Step/StepValidationWrapper'; const STEPS = { fork: { path: '/', order: 10, isRequired: true, - Component: ForkStep, + Component: withStepValidation( ForkStep, 'fork' ), }, intake: { path: '/intake', order: 20, isRequired: true, - Component: IntakeStep, + Component: withStepValidation( IntakeStep, 'intake' ), }, logo: { path: '/logo', order: 30, isRequired: true, - Component: LogoStep, + Component: withStepValidation( LogoStep, 'logo' ), }, generating: { path: '/generating', order: 50, isRequired: true, - Component: GeneratingStep, + Component: withStepValidation( GeneratingStep, 'generating' ), }, previews: { path: '/previews', order: 60, isRequired: true, - Component: PreviewsStep, + Component: withStepValidation( PreviewsStep, 'previews' ), }, design: { path: '/canvas', order: 70, isRequired: true, - Component: CanvasStep, + Component: withStepValidation( CanvasStep, 'design' ), }, migration: { path: '/migration', order: 80, isRequired: false, - Component: MigrationStep, + Component: withStepValidation( MigrationStep, 'migration' ), }, }; diff --git a/src/app/utils/helpers/stepValidation.js b/src/app/utils/helpers/stepValidation.js new file mode 100644 index 000000000..e833743a7 --- /dev/null +++ b/src/app/utils/helpers/stepValidation.js @@ -0,0 +1,67 @@ +import { select } from '@wordpress/data'; +import { nfdOnboardingStore } from '../../data/store'; + +/** + * Check if the required data is available for a specific step + * + * @param {string} stepKey - The key of the step to validate + * @return {boolean} - True if the step has required data, false otherwise + */ +export const hasRequiredDataForStep = ( stepKey ) => { + switch ( stepKey ) { + case 'fork': + // Fork step doesn't require any specific data - it's the entry point + return true; + + case 'intake': + // Intake step doesn't require any previous data - it's where data collection starts + return true; + + case 'logo': { + // Logo step requires site title and prompt from intake + const inputSlice = select( nfdOnboardingStore ).getInputSlice(); + return !! ( inputSlice.siteTitle && inputSlice.prompt ); + } + + case 'generating': { + // Generating step requires site title and prompt + const inputSlice = select( nfdOnboardingStore ).getInputSlice(); + return !! ( inputSlice.siteTitle && inputSlice.prompt ); + } + + case 'previews': { + // Previews step requires homepages to be generated + const sitegenSlice = select( nfdOnboardingStore ).getSiteGenSlice(); + return !! ( sitegenSlice.homepages && Object.keys( sitegenSlice.homepages ).length > 0 ); + } + + case 'design': { + // Canvas step requires a selected homepage + const sitegenSlice = select( nfdOnboardingStore ).getSiteGenSlice(); + return !! ( sitegenSlice.selectedHomepage && sitegenSlice.homepages && sitegenSlice.homepages[ sitegenSlice.selectedHomepage ] ); + } + + case 'migration': + // Migration step is optional and doesn't require specific data + return true; + + default: + return true; + } +}; + +/** + * Check if a step should redirect due to missing data + * + * @param {string} stepKey - The key of the step to check + * @return {boolean} - True if should redirect, false otherwise + */ +export const shouldRedirectToFirstStep = ( stepKey ) => { + // Don't redirect if we're already on the first step + if ( stepKey === 'fork' ) { + return false; + } + + // Check if the step has required data + return ! hasRequiredDataForStep( stepKey ); +};