Merge pull request #46 from woocommerce/update/allow-manual-experiment-input

Allow adding a new experiment manually
This commit is contained in:
Moon 2022-05-23 14:30:40 -07:00 committed by GitHub
commit 69f89dc11c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 218 additions and 45 deletions

View File

@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { withDispatch } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function NewExperimentForm( { addExperiment } ) {
const [ experimentName, setExperimentName ] = useState( null );
const [ variation, setVariation ] = useState( 'treatment' );
const getInputValue = ( event ) => {
setExperimentName( event.target.value );
};
const getVariationInput = ( event ) => {
setVariation( event.target.value );
};
const AddNewExperiment = () => {
addExperiment( experimentName, variation );
};
return (
<div className="manual-input">
<div className="description">
Don&apos;t see an experiment you want to test? Add it manually.
</div>
<input type="text" onChange={ getInputValue } />
<select value={ variation } onChange={ getVariationInput }>
<option value="treatment">treatment</option>
<option value="control">control</option>
</select>
<Button isPrimary onClick={ AddNewExperiment }>
Add
</Button>
</div>
);
}
export default compose(
withDispatch( ( dispatch ) => {
const { addExperiment } = dispatch( STORE_KEY );
return {
addExperiment,
};
} )
)( NewExperimentForm );

View File

@ -1,6 +1,8 @@
const TYPES = { const TYPES = {
TOGGLE_EXPERIMENT: 'TOGGLE_EXPERIMENT', TOGGLE_EXPERIMENT: 'TOGGLE_EXPERIMENT',
SET_EXPERIMENTS: 'SET_EXPERIMENTS', SET_EXPERIMENTS: 'SET_EXPERIMENTS',
ADD_EXPERIMENT: 'ADD_EXPERIMENT',
DELETE_EXPERIMENT: 'DELETE_EXPERIMENT',
}; };
export default TYPES; export default TYPES;

View File

@ -14,10 +14,18 @@ import {
} from './constants'; } from './constants';
function toggleFrontendExperiment( experimentName, newVariation ) { function toggleFrontendExperiment( experimentName, newVariation ) {
const storageItem = JSON.parse( let storageItem = JSON.parse(
window.localStorage.getItem( EXPERIMENT_NAME_PREFIX + experimentName ) window.localStorage.getItem( EXPERIMENT_NAME_PREFIX + experimentName )
); );
// If the experiment is not in localStorage, consider it as a new.
if ( storageItem === null ) {
storageItem = {
experimentName,
retrievedTimestamp: Date.now(),
};
}
storageItem.variationName = newVariation; storageItem.variationName = newVariation;
storageItem.ttl = 3600; storageItem.ttl = 3600;
@ -45,21 +53,17 @@ function* toggleBackendExperiment( experimentName, newVariation ) {
} }
} }
export function* toggleExperiment( experimentName, currentVariation, source ) { export function* toggleExperiment( experimentName, currentVariation ) {
const newVariation = const newVariation =
currentVariation === 'control' ? 'treatment' : 'control'; currentVariation === 'control' ? 'treatment' : 'control';
if ( source === 'frontend' ) { toggleFrontendExperiment( experimentName, newVariation );
toggleFrontendExperiment( experimentName, newVariation ); yield toggleBackendExperiment( experimentName, newVariation );
} else {
yield toggleBackendExperiment( experimentName, newVariation );
}
return { return {
type: TYPES.TOGGLE_EXPERIMENT, type: TYPES.TOGGLE_EXPERIMENT,
experimentName, experimentName,
newVariation, newVariation,
source,
}; };
} }
@ -69,3 +73,33 @@ export function setExperiments( experiments ) {
experiments, experiments,
}; };
} }
export function* addExperiment( experimentName, variation ) {
toggleFrontendExperiment( experimentName, variation );
yield toggleBackendExperiment( experimentName, variation );
return {
type: TYPES.ADD_EXPERIMENT,
experimentName,
variation,
};
}
export function* deleteExperiment( experimentName ) {
window.localStorage.removeItem( EXPERIMENT_NAME_PREFIX + experimentName );
const optionNames = [
TRANSIENT_NAME_PREFIX + experimentName,
TRANSIENT_TIMEOUT_NAME_PREFIX + experimentName,
];
yield apiFetch( {
method: 'DELETE',
path: '/wc-admin-test-helper/options/' + optionNames.join( ',' ),
} );
return {
type: TYPES.DELETE_EXPERIMENT,
experimentName,
};
}

View File

@ -9,14 +9,52 @@ const DEFAULT_STATE = {
const reducer = ( state = DEFAULT_STATE, action ) => { const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) { switch ( action.type ) {
case TYPES.DELETE_EXPERIMENT:
return {
...state,
experiments: state.experiments.filter( ( experiment ) => {
return experiment.name !== action.experimentName;
} ),
};
case TYPES.ADD_EXPERIMENT:
const existingExperimentIndex = state.experiments.findIndex(
( element ) => {
return element.name === action.experimentName;
}
);
const newExperiment = {
name: action.experimentName,
variation: action.variation,
};
const newExperiments =
existingExperimentIndex !== -1
? state.experiments
.slice( 0, existingExperimentIndex )
.concat( newExperiment )
.concat(
state.experiments.slice(
existingExperimentIndex + 1
)
)
: [
...state.experiments,
{
name: action.experimentName,
variation: action.variation,
},
];
return {
...state,
experiments: newExperiments,
};
case TYPES.TOGGLE_EXPERIMENT: case TYPES.TOGGLE_EXPERIMENT:
return { return {
...state, ...state,
experiments: state.experiments.map( ( experiment ) => ( { experiments: state.experiments.map( ( experiment ) => ( {
...experiment, ...experiment,
variation: variation:
experiment.name === action.experimentName && experiment.name === action.experimentName
experiment.source === action.source
? action.newVariation ? action.newVariation
: experiment.variation, : experiment.variation,
} ) ), } ) ),

View File

@ -26,7 +26,6 @@ function getExperimentsFromFrontend() {
return { return {
name: key.replace( EXPERIMENT_NAME_PREFIX, '' ), name: key.replace( EXPERIMENT_NAME_PREFIX, '' ),
variation: objectValue.variationName || 'control', variation: objectValue.variationName || 'control',
source: 'frontend',
}; };
} ); } );
} }
@ -47,12 +46,22 @@ export function* getExperiments() {
experiment.option_value === 'control' experiment.option_value === 'control'
? 'control' ? 'control'
: 'treatment', : 'treatment',
source: 'backend',
}; };
} ); } );
yield setExperiments(
getExperimentsFromFrontend().concat( experimentsFromBackend ) // Remove duplicate.
); const experiments = getExperimentsFromFrontend()
.concat( experimentsFromBackend )
.filter(
( value, index, self ) =>
index ===
self.findIndex(
( t ) =>
t.place === value.place && t.name === value.name
)
);
yield setExperiments( experiments );
} catch ( error ) { } catch ( error ) {
throw new Error(); throw new Error();
} }

View File

@ -10,16 +10,19 @@ import { OPTIONS_STORE_NAME } from '@woocommerce/data';
*/ */
import { STORE_KEY } from './data/constants'; import { STORE_KEY } from './data/constants';
import './data'; import './data';
import NewExperimentForm from './NewExperimentForm';
function Experiments( { function Experiments( {
experiments, experiments,
toggleExperiment, toggleExperiment,
deleteExperiment,
isTrackingEnabled, isTrackingEnabled,
isResolving, isResolving,
} ) { } ) {
if ( isResolving ) { if ( isResolving ) {
return null; return null;
} }
return ( return (
<div id="wc-admin-test-helper-experiments"> <div id="wc-admin-test-helper-experiments">
<h2>Experiments</h2> <h2>Experiments</h2>
@ -43,43 +46,42 @@ function Experiments( {
<b>Allow usage of WooCommerce to be tracked</b>. <b>Allow usage of WooCommerce to be tracked</b>.
</p> </p>
) } ) }
<NewExperimentForm />
<table className="experiments wp-list-table striped table-view-list widefat"> <table className="experiments wp-list-table striped table-view-list widefat">
<thead> <thead>
<tr> <tr>
<th>Experiment</th> <th>Experiment</th>
<th>Variation</th> <th>Variation</th>
<th>Source</th> <th>Actions</th>
<th>Toggle</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ experiments.map( { experiments.map( ( { name, variation }, index ) => {
( { name, variation, source }, index ) => { return (
return ( <tr key={ index }>
<tr key={ index }> <td className="experiment-name">{ name }</td>
<td className="experiment-name"> <td align="center">{ variation }</td>
{ name } <td className="actions" align="center">
</td> <Button
<td align="center">{ variation }</td> onClick={ () => {
<td align="center">{ source }</td> toggleExperiment( name, variation );
<td align="center"> } }
<Button isPrimary
onClick={ () => { >
toggleExperiment( Toggle
name, </Button>
variation, <Button
source onClick={ () => {
); deleteExperiment( name );
} } } }
isPrimary className="btn btn-danger"
> >
Toggle Delete
</Button> </Button>
</td> </td>
</tr> </tr>
); );
} } ) }
) }
</tbody> </tbody>
</table> </table>
</div> </div>
@ -98,10 +100,11 @@ export default compose(
}; };
} ), } ),
withDispatch( ( dispatch ) => { withDispatch( ( dispatch ) => {
const { toggleExperiment } = dispatch( STORE_KEY ); const { toggleExperiment, deleteExperiment } = dispatch( STORE_KEY );
return { return {
toggleExperiment, toggleExperiment,
deleteExperiment,
}; };
} ) } )
)( Experiments ); )( Experiments );

View File

@ -92,6 +92,37 @@
} }
} }
#wc-admin-test-helper-experiments {
.actions {
button {
margin-right: 5px;
}
}
.manual-input {
margin-bottom: 20px;
float: right;
.description {
text-align: right;
margin-bottom: 5px;
}
button {
height: 34px;
position: relative;
top: -1px;
}
input {
height: 34px;
width: 250px;
}
select {
height: 34px;
position: relative;
top: -2px;
margin-right: 2px;
}
}
}
#wc-admin-test-helper-rest-api-filters { #wc-admin-test-helper-rest-api-filters {
.btn-new { .btn-new {
float: right; float: right;