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 = {
TOGGLE_EXPERIMENT: 'TOGGLE_EXPERIMENT',
SET_EXPERIMENTS: 'SET_EXPERIMENTS',
ADD_EXPERIMENT: 'ADD_EXPERIMENT',
DELETE_EXPERIMENT: 'DELETE_EXPERIMENT',
};
export default TYPES;

View File

@ -14,10 +14,18 @@ import {
} from './constants';
function toggleFrontendExperiment( experimentName, newVariation ) {
const storageItem = JSON.parse(
let storageItem = JSON.parse(
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.ttl = 3600;
@ -45,21 +53,17 @@ function* toggleBackendExperiment( experimentName, newVariation ) {
}
}
export function* toggleExperiment( experimentName, currentVariation, source ) {
export function* toggleExperiment( experimentName, currentVariation ) {
const newVariation =
currentVariation === 'control' ? 'treatment' : 'control';
if ( source === 'frontend' ) {
toggleFrontendExperiment( experimentName, newVariation );
} else {
yield toggleBackendExperiment( experimentName, newVariation );
}
toggleFrontendExperiment( experimentName, newVariation );
yield toggleBackendExperiment( experimentName, newVariation );
return {
type: TYPES.TOGGLE_EXPERIMENT,
experimentName,
newVariation,
source,
};
}
@ -69,3 +73,33 @@ export function setExperiments( 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 ) => {
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:
return {
...state,
experiments: state.experiments.map( ( experiment ) => ( {
...experiment,
variation:
experiment.name === action.experimentName &&
experiment.source === action.source
experiment.name === action.experimentName
? action.newVariation
: experiment.variation,
} ) ),

View File

@ -26,7 +26,6 @@ function getExperimentsFromFrontend() {
return {
name: key.replace( EXPERIMENT_NAME_PREFIX, '' ),
variation: objectValue.variationName || 'control',
source: 'frontend',
};
} );
}
@ -47,12 +46,22 @@ export function* getExperiments() {
experiment.option_value === 'control'
? 'control'
: '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 ) {
throw new Error();
}

View File

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