/**
* WordPress dependencies
*/
import { Button, CheckboxControl } from '@safe-wordpress/components';
import { useDispatch, useSelect } from '@safe-wordpress/data';
import {
createInterpolateElement,
useEffect,
useState,
} from '@safe-wordpress/element';
import { _x } from '@safe-wordpress/i18n';
/**
* External dependencies
*/
import clsx from 'clsx';
import { HelpIcon } from '@nab/components';
import { store as NAB_EXPERIMENTS } from '@nab/experiments';
import {
createAlternative,
duplicateAlternative,
getLetter,
omitUndefineds,
} from '@nab/utils';
import type {
Alternative,
AlternativeCustomAction,
AlternativeId,
Dict,
ExperimentEditProps,
Maybe,
} from '@nab/types';
/**
* Internal dependencies
*/
import { useCanAddAlternatives, useExperimentAttribute } from '../hooks';
import { store as NAB_EDITOR } from '../../store';
export type AlternativeContainerProps = {
readonly alternativeId: AlternativeId;
readonly disabled?: boolean;
readonly index: number;
};
export const AlternativeContainer = ( {
alternativeId,
disabled,
index,
}: AlternativeContainerProps ): JSX.Element | null => {
const control = useAlternative( 'control' );
const alternative = useAlternative( alternativeId );
const AlternativeView = useView( index );
const canBeRemoved = useCanBeRemoved();
const experimentType = useExperimentType();
const help = useHelp( index );
const isBeingSaved = useIsBeingSaved();
const isExternalEdit = useIsExternalEdit();
const isControl = ! index;
const canAddAlternatives = useCanAddAlternatives();
const duplicate = useDuplicator( alternative );
const isPreviewDisabled = useIsPreviewDisabled( alternative, control );
const customActions = useCustomActions( alternative, control );
const {
saveExperimentAndEditAlternative,
saveExperimentAndPreviewAlternative,
removeAlternatives,
replaceAlternatives,
setAlternative,
showAiAlternativeModal,
} = useDispatch( NAB_EDITOR );
if ( ! alternative || ! control ) {
return null;
}
const setAttributes = ( attributes: Dict ) =>
setAlternative( alternative.id, {
...alternative,
attributes: omitUndefineds( {
...alternative.attributes,
...attributes,
} ),
} );
const resetAlternative = () =>
replaceAlternatives( alternativeId, {
...createAlternative(),
attributes: {
name: alternative.ai?.name || '',
chance: alternative.attributes.chance,
},
ai: alternative.ai
? { ...alternative.ai, isReady: false }
: undefined,
} );
return (
{ getLetter( index ) }
{ !! alternative.ai && (
setAlternative( alternative.id, {
...alternative,
ai: alternative.ai
? { ...alternative.ai, isReady }
: undefined,
} )
}
label={ createInterpolateElement(
_x(
'I’ve already applied all Nelio AI’s recommended changes to this variant',
'text',
'nelio-ab-testing'
),
{
a: (
) }
{ 0 !== index && (
{ customActions.map( ( action ) =>
! action.isVisible ||
action.isVisible(
alternative.attributes,
control.attributes
) ? (
-
) : null
) }
{ isExternalEdit && (
-
) }
-
{ isExternalEdit &&
! alternative.base &&
canAddAlternatives && (
-
) }
{ isExternalEdit && (
-
) }
{ canBeRemoved && (
-
) }
) }
{ !! help && }
);
};
// =====
// HOOKS
// =====
const useExperimentType = () =>
useSelect( ( select ) => select( NAB_EDITOR ).getExperimentType(), [] );
const useDefaults = () => {
const type = useExperimentType();
return useSelect(
( select ) =>
select( NAB_EXPERIMENTS ).getExperimentType( type )?.defaults,
[ type ]
);
};
const useDuplicator = ( alternative: Maybe< Alternative > ) => {
const alternatives = useSelect(
( select ) => select( NAB_EDITOR ).getAlternatives(),
[]
);
const { replaceAlternatives } = useDispatch( NAB_EDITOR );
return alternative
? () =>
void replaceAlternatives(
alternatives.map( ( a ) => a.id ),
[ ...alternatives, duplicateAlternative( alternative ) ]
)
: () => void null;
};
const useAlternative = ( alternativeId: AlternativeId ) => {
const [ didApplyDefaults, markApplyDefaults ] = useState( false );
const defaults = useDefaults();
const alternative = useSelect(
( select ) => select( NAB_EDITOR ).getAlternative( alternativeId ),
[ alternativeId ]
);
const { setAlternative } = useDispatch( NAB_EDITOR );
useEffect( () => {
if ( didApplyDefaults ) {
return;
}
if ( ! defaults || ! alternative ) {
return;
}
const defaultAttributes =
'control' === alternative.id
? defaults.original
: defaults.alternative;
void setAlternative( alternative.id, {
...alternative,
attributes: {
...defaultAttributes,
...alternative.attributes,
},
} );
markApplyDefaults( true );
}, [ defaults, alternative, didApplyDefaults, setAlternative ] );
return didApplyDefaults ? alternative : undefined;
};
const useCanBeRemoved = () => {
const [ status ] = useExperimentAttribute( 'status' );
return useSelect(
( select ) => {
const { getAlternativeIds } = select( NAB_EDITOR );
const isPaused = ( status || '' ).includes( 'paused' );
return getAlternativeIds().length > 2 && ! isPaused;
},
[ status ]
);
};
const useIsBeingSaved = () =>
useSelect(
( select ) => select( NAB_EDITOR ).isExperimentBeingSaved(),
[]
);
type View = ( props: ExperimentEditProps< Dict > ) => JSX.Element;
const EMPTY_VIEW: View = () => <>>;
const useView = ( index: number ): View =>
useSelect(
( select ) => {
const { getExperimentView } = select( NAB_EXPERIMENTS );
const { getExperimentType } = select( NAB_EDITOR );
const view = ! index
? getExperimentView( getExperimentType(), 'original' )
: getExperimentView( getExperimentType(), 'alternative' );
return view ?? EMPTY_VIEW;
},
[ index ]
);
const useHelp = ( index: number ) =>
useSelect(
( select ) => {
const { getExperimentType } = select( NAB_EDITOR );
const { getHelpString } = select( NAB_EXPERIMENTS );
if ( 0 === index ) {
return getHelpString( getExperimentType(), 'original' );
}
if ( 1 === index ) {
return getHelpString( getExperimentType(), 'alternative' );
}
return '';
},
[ index ]
);
const useIsExternalEdit = () => {
const type = useExperimentType();
const control = useSelect(
( select ) => select( NAB_EDITOR ).getAlternative( 'control' ),
[]
);
return useSelect(
( select ) => {
const { getExperimentSupport } = select( NAB_EXPERIMENTS );
const alternativeEdition = getExperimentSupport(
type,
'alternativeEdition'
);
if ( 'external' !== alternativeEdition?.type ) {
return false;
}
if ( 'function' !== typeof alternativeEdition.enabled ) {
return true;
}
return (
!! control && alternativeEdition.enabled( control.attributes )
);
},
[ type, control ]
);
};
const useIsPreviewDisabled = (
alternative: Maybe< Alternative >,
control: Maybe< Alternative >
) =>
useSelect(
( select ): boolean => {
const { getExperimentType } = select( NAB_EXPERIMENTS );
const type = getExperimentType(
select( NAB_EDITOR ).getExperimentType()
);
if ( ! type || ! alternative || ! control ) {
return false;
}
return !! type.checks.isAlternativePreviewDisabled?.(
alternative.attributes,
control.attributes,
select
);
},
[ control, alternative ]
);
const useCustomActions = (
alternative: Maybe< Alternative >,
control: Maybe< Alternative >
) =>
useSelect(
( select ): ReadonlyArray< AlternativeCustomAction > => {
const { getExperimentType } = select( NAB_EXPERIMENTS );
const type = getExperimentType(
select( NAB_EDITOR ).getExperimentType()
);
if ( ! type || ! alternative || ! control ) {
return [];
}
return type.supports.alternativeCustomActions ?? [];
},
[ control, alternative ]
);