/** * WordPress dependencies */ import { useDispatch, useSelect } from '@safe-wordpress/data'; import { useCallback, useEffect } from '@safe-wordpress/element'; import { _x } from '@safe-wordpress/i18n'; /** * External dependencies */ import { find, keyBy } from 'lodash'; import { store as NAB_ACTIONS } from '@nab/conversion-actions'; import { isInTheFuture } from '@nab/date'; import { store as NAB_SEGMENTS } from '@nab/segmentation-rules'; import type { Alternative, ConversionAction, Experiment, Goal, GoalId, Segment, SegmentationRule, SegmentId, } from '@nab/types'; /** * Internal dependencies */ import { shouldExperimentBeDraft } from './utils'; import { useExperimentAttribute, useExperimentType } from '../hooks'; import { store as NAB_EDITOR } from '../../store'; import { EMPTY_ARRAY } from '@nab/utils'; export const StatusManager = (): null => { useStatusUpdateEffect(); useInvalidControlEffect(); return null; }; // ===== // HOOKS // ===== const useStatusUpdateEffect = () => { const startDate = useExperimentAttribute( 'startDate' )[ 0 ] || ''; const [ status, setStatus ] = useExperimentAttribute( 'status' ); const rationale = useSelect( ( select ) => select( NAB_EDITOR ).getDraftStatusRationale(), [] ); const { setDraftStatusRationale } = useDispatch( NAB_EDITOR ); const setStatusAndRationale = useCallback( ( newStatus: Experiment[ 'status' ], newRationale?: string ): void => { if ( status.includes( 'paused' ) ) { newStatus = 'draft' === newStatus ? 'paused_draft' : 'paused'; } newRationale = newRationale ?? ''; if ( status === newStatus && rationale === newRationale ) { return; } setStatus( newStatus ); void setDraftStatusRationale( newRationale ); }, [ rationale, status, setStatus, setDraftStatusRationale ] ); const proposedRationale = useProposedRationale(); useEffect( () => { if ( 'running' === status || 'trash' === status ) { return; } if ( proposedRationale ) { setStatusAndRationale( 'draft', proposedRationale ); return; } if ( 'ready' === status ) { return; } if ( 'scheduled' === status && ! isInTheFuture( startDate ) ) { setStatusAndRationale( 'ready' ); } if ( 'scheduled' !== status ) { setStatusAndRationale( 'ready' ); } }, [ proposedRationale, startDate, status, setStatusAndRationale ] ); }; const useInvalidControlEffect = () => { const [ status ] = useExperimentAttribute( 'status' ); const alternatives = useAlternatives(); const type = useExperimentType(); const control = find( alternatives, { id: 'control' } ); const isInvalid = useSelect( ( select ) => 'running' !== status && !! control && !! type && !! type.checks.getControlError( control.attributes, select ), [ control, status, type ] ); const { setTestedElementAsInvalid } = useDispatch( NAB_EDITOR ); useEffect( () => void setTestedElementAsInvalid( isInvalid ), [ isInvalid, setTestedElementAsInvalid ] ); }; const useProposedRationale = () => { const name = useExperimentAttribute( 'name' )[ 0 ] || ''; const type = useExperimentType(); const alternatives = useAlternatives(); const goals = useGoals(); const segments = useSegments(); const scope = useScope(); const conversionActionTypes = useConversionActionTypes(); const segmentationRuleTypes = useSegmentationRuleTypes(); const control = find( alternatives, { id: 'control' } ) as Alternative; const newRationale = ! type ? _x( 'Type not found', 'text', 'nelio-ab-testing' ) : shouldExperimentBeDraft( { name, control, alternatives, scope, goals, segments, type }, conversionActionTypes, segmentationRuleTypes ); return newRationale || ''; }; const useAlternatives = (): ReadonlyArray< Alternative > => useSelect( ( select ) => select( NAB_EDITOR ).getAlternatives(), [] ); const useGoals = (): ReadonlyArray< Goal > => { const goals = useSelect( ( select ) => select( NAB_EDITOR ).getGoals(), [] ); const actions = useSelect( ( select ) => goals.reduce( ( r, g ) => ( { ...r, [ g.id ]: select( NAB_EDITOR ).getConversionActions( g.id ), } ), {} as Record< GoalId, ReadonlyArray< ConversionAction > > ), [ goals ] ); return goals.map( ( g ) => ( { ...g, conversionActions: actions[ g.id ] ?? EMPTY_ARRAY, } ) ); }; const useSegments = (): ReadonlyArray< Segment > => { const segments = useSelect( ( select ) => select( NAB_EDITOR ).getSegments(), [] ); const rules = useSelect( ( select ) => segments.reduce( ( r, s ) => ( { ...r, [ s.id ]: select( NAB_EDITOR ).getSegmentationRules( s.id ), } ), {} as Record< SegmentId, ReadonlyArray< SegmentationRule > > ), [ segments ] ); return segments.map( ( s ) => ( { ...s, segmentationRules: rules[ s.id ] ?? EMPTY_ARRAY, } ) ); }; const useScope = () => useSelect( ( select ) => select( NAB_EDITOR ).getScope(), [] ); const useConversionActionTypes = () => useSelect( ( select ) => keyBy( select( NAB_ACTIONS ).getConversionActionTypes() || [], 'name' ), [] ); const useSegmentationRuleTypes = () => useSelect( ( select ) => keyBy( select( NAB_SEGMENTS ).getSegmentationRuleTypes() || [], 'name' ), [] );