diff --git a/api/api.php b/api/api.php index 9a3a02173bf..cab00d3441b 100644 --- a/api/api.php +++ b/api/api.php @@ -36,3 +36,4 @@ require( 'tools/delete-all-products.php'); require( 'tools/disable-wc-email.php' ); require( 'tools/trigger-update-callbacks.php' ); require( 'tracks/tracks-debug-log.php' ); +require( 'features/features.php' ); diff --git a/api/features/features.php b/api/features/features.php new file mode 100644 index 00000000000..f19db0cb94f --- /dev/null +++ b/api/features/features.php @@ -0,0 +1,59 @@ +[a-z0-9_\-]+)/toggle', + 'toggle_feature', + array( + 'methods' => 'POST', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/features', + 'get_features', + array( + 'methods' => 'GET', + ) +); + +register_woocommerce_admin_test_helper_rest_route( + '/features/reset', + 'reset_features', + array( + 'methods' => 'POST', + ) +); + +function toggle_feature( $request ) { + $features = get_features(); + $custom_feature_values = get_option( OPTION_NAME_PREFIX, array() ); + $feature_name = $request->get_param( 'feature_name' ); + + if ( ! isset( $features[$feature_name ]) ) { + return new WP_REST_Response( $features, 204 ); + } + + if ( isset( $custom_feature_values[$feature_name] ) ) { + unset( $custom_feature_values[$feature_name] ); + } else { + $custom_feature_values[$feature_name] = ! $features[ $feature_name ]; + } + + update_option(OPTION_NAME_PREFIX, $custom_feature_values ); + return new WP_REST_Response( get_features(), 200 ); +} + +function reset_features() { + delete_option( OPTION_NAME_PREFIX ); + return new WP_REST_Response( get_features(), 200 ); +} + +function get_features() { + if ( function_exists( 'wc_admin_get_feature_config' ) ) { + return apply_filters( 'woocommerce_admin_get_feature_config', wc_admin_get_feature_config() ); + } + return array(); +} diff --git a/plugin.php b/plugin.php index 04ba8574c2f..0d01e8f77d8 100644 --- a/plugin.php +++ b/plugin.php @@ -14,3 +14,13 @@ add_action( 'admin_menu', function() { add_action( 'wp_loaded', function() { require( 'api/api.php' ); } ); + +add_filter( 'woocommerce_admin_get_feature_config', function( $feature_config ) { + $custom_feature_values = get_option( 'wc_admin_helper_feature_values', array() ); + foreach ( $custom_feature_values as $feature => $value ) { + if ( isset( $feature_config[$feature] ) ) { + $feature_config[$feature] = $value; + } + } + return $feature_config; +} ); \ No newline at end of file diff --git a/src/app/app.js b/src/app/app.js index 69d70987a8d..acc8790b431 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -11,6 +11,7 @@ import { AdminNotes } from '../admin-notes'; import { default as Tools } from '../tools'; import { default as Options } from '../options'; import { default as Experiments } from '../experiments'; +import { default as Features } from '../features'; const tabs = applyFilters('woocommerce_admin_test_helper_tabs', [ { @@ -33,6 +34,11 @@ const tabs = applyFilters('woocommerce_admin_test_helper_tabs', [ title: 'Experiments', content: , }, + { + name: 'features', + title: 'Features', + content: , + }, ]); export function App() { diff --git a/src/experiments/data/actions.js b/src/experiments/data/actions.js index 18e74e00b06..0db766f5760 100644 --- a/src/experiments/data/actions.js +++ b/src/experiments/data/actions.js @@ -9,42 +9,42 @@ import { apiFetch } from '@wordpress/data-controls'; import TYPES from './action-types'; import { EXPERIMENT_NAME_PREFIX, TRANSIENT_NAME_PREFIX } from './constants'; -function toggleFrontendExperiment( experimentName, newVariation ) { +function toggleFrontendExperiment(experimentName, newVariation) { const storageItem = JSON.parse( - window.localStorage.getItem( EXPERIMENT_NAME_PREFIX + experimentName ) + window.localStorage.getItem(EXPERIMENT_NAME_PREFIX + experimentName) ); storageItem.variationName = newVariation; window.localStorage.setItem( EXPERIMENT_NAME_PREFIX + experimentName, - JSON.stringify( storageItem ) + JSON.stringify(storageItem) ); } -function* toggleBackendExperiment( experimentName, newVariation ) { +function* toggleBackendExperiment(experimentName, newVariation) { try { const payload = {}; - payload[ TRANSIENT_NAME_PREFIX + experimentName ] = newVariation; - yield apiFetch( { + payload[TRANSIENT_NAME_PREFIX + experimentName] = newVariation; + yield apiFetch({ method: 'POST', path: '/wc-admin/options', headers: { 'content-type': 'application/json' }, - body: JSON.stringify( payload ), - } ); - } catch ( error ) { + body: JSON.stringify(payload), + }); + } catch (error) { throw new Error(); } } -export function* toggleExperiment( experimentName, currentVariation, source ) { +export function* toggleExperiment(experimentName, currentVariation, source) { const newVariation = currentVariation === 'control' ? 'treatment' : 'control'; - if ( source === 'frontend' ) { - toggleFrontendExperiment( experimentName, newVariation ); + if (source === 'frontend') { + toggleFrontendExperiment(experimentName, newVariation); } else { - yield toggleBackendExperiment( experimentName, newVariation ); + yield toggleBackendExperiment(experimentName, newVariation); } return { @@ -55,7 +55,7 @@ export function* toggleExperiment( experimentName, currentVariation, source ) { }; } -export function setExperiments( experiments ) { +export function setExperiments(experiments) { return { type: TYPES.SET_EXPERIMENTS, experiments, diff --git a/src/features/data/action-types.js b/src/features/data/action-types.js new file mode 100644 index 00000000000..1e6a4d8a3bf --- /dev/null +++ b/src/features/data/action-types.js @@ -0,0 +1,7 @@ +const TYPES = { + TOGGLE_FEATURE: 'TOGGLE_FEATURE', + SET_FEATURES: 'SET_FEATURES', + SET_MODIFIED_FEATURES: 'SET_MODIFIED_FEATURES', +}; + +export default TYPES; diff --git a/src/features/data/actions.js b/src/features/data/actions.js new file mode 100644 index 00000000000..25bc100d791 --- /dev/null +++ b/src/features/data/actions.js @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; +import { controls } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import TYPES from './action-types'; +import { API_NAMESPACE, STORE_KEY } from './constants'; + +export function* resetModifiedFeatures() { + try { + const response = yield apiFetch( { + path: `${ API_NAMESPACE }/features/reset`, + method: 'POST', + } ); + + yield setModifiedFeatures( [] ); + yield setFeatures( response ); + } catch ( error ) { + throw new Error(); + } +} + +export function* toggleFeature( featureName ) { + try { + const response = yield apiFetch( { + method: 'POST', + path: API_NAMESPACE + '/features/' + featureName + '/toggle', + headers: { 'content-type': 'application/json' }, + } ); + yield setFeatures( response ); + yield controls.dispatch( + STORE_KEY, + 'invalidateResolutionForStoreSelector', + 'getModifiedFeatures' + ); + } catch ( error ) { + throw new Error(); + } +} + +export function setFeatures( features ) { + return { + type: TYPES.SET_FEATURES, + features, + }; +} + +export function setModifiedFeatures( modifiedFeatures ) { + return { + type: TYPES.SET_MODIFIED_FEATURES, + modifiedFeatures, + }; +} diff --git a/src/features/data/constants.js b/src/features/data/constants.js new file mode 100644 index 00000000000..c9da14951e5 --- /dev/null +++ b/src/features/data/constants.js @@ -0,0 +1,3 @@ +export const STORE_KEY = 'wc-admin-helper/features'; +export const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values'; +export const API_NAMESPACE = '/wc-admin-test-helper'; diff --git a/src/features/data/index.js b/src/features/data/index.js new file mode 100644 index 00000000000..e476479ea88 --- /dev/null +++ b/src/features/data/index.js @@ -0,0 +1,22 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; +import { controls } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import * as actions from './actions'; +import * as resolvers from './resolvers'; +import * as selectors from './selectors'; +import reducer from './reducer'; +import { STORE_KEY } from './constants'; + +export default registerStore( STORE_KEY, { + actions, + selectors, + resolvers, + controls, + reducer, +} ); diff --git a/src/features/data/reducer.js b/src/features/data/reducer.js new file mode 100644 index 00000000000..61999300c24 --- /dev/null +++ b/src/features/data/reducer.js @@ -0,0 +1,28 @@ +/** + * Internal dependencies + */ +import TYPES from './action-types'; + +const DEFAULT_STATE = { + features: {}, + modifiedFeatures: [], +}; + +const reducer = ( state = DEFAULT_STATE, action ) => { + switch ( action.type ) { + case TYPES.SET_MODIFIED_FEATURES: + return { + ...state, + modifiedFeatures: action.modifiedFeatures, + }; + case TYPES.SET_FEATURES: + return { + ...state, + features: action.features, + }; + default: + return state; + } +}; + +export default reducer; diff --git a/src/features/data/resolvers.js b/src/features/data/resolvers.js new file mode 100644 index 00000000000..29d7ccfbf19 --- /dev/null +++ b/src/features/data/resolvers.js @@ -0,0 +1,38 @@ +/** + * External dependencies + */ +import { apiFetch } from '@wordpress/data-controls'; + +/** + * Internal dependencies + */ +import { setFeatures, setModifiedFeatures } from './actions'; +import { API_NAMESPACE, OPTION_NAME_PREFIX } from './constants'; + +export function* getModifiedFeatures() { + try { + const response = yield apiFetch( { + path: `wc-admin/options?options=` + OPTION_NAME_PREFIX, + } ); + + yield setModifiedFeatures( + response && response[ OPTION_NAME_PREFIX ] + ? Object.keys( response[ OPTION_NAME_PREFIX ] ) + : [] + ); + } catch ( error ) { + throw new Error(); + } +} + +export function* getFeatures() { + try { + const response = yield apiFetch( { + path: `${ API_NAMESPACE }/features`, + } ); + + yield setFeatures( response ); + } catch ( error ) { + throw new Error(); + } +} diff --git a/src/features/data/selectors.js b/src/features/data/selectors.js new file mode 100644 index 00000000000..6fa3938dafb --- /dev/null +++ b/src/features/data/selectors.js @@ -0,0 +1,7 @@ +export function getFeatures( state ) { + return state.features; +} + +export function getModifiedFeatures( state ) { + return state.modifiedFeatures; +} diff --git a/src/features/index.js b/src/features/index.js new file mode 100644 index 00000000000..f89defecc1c --- /dev/null +++ b/src/features/index.js @@ -0,0 +1,72 @@ +/** + * External dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { Button } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { STORE_KEY } from './data/constants'; +import './data'; + +function Features() { + const { features = {}, modifiedFeatures = [] } = useSelect( ( select ) => { + const { getFeatures, getModifiedFeatures } = select( STORE_KEY ); + return { + features: getFeatures(), + modifiedFeatures: getModifiedFeatures(), + }; + } ); + + const { toggleFeature, resetModifiedFeatures } = useDispatch( STORE_KEY ); + + return ( +
+

+ Features + +

+ + + + + + + + + + { Object.keys( features ).map( ( feature_name ) => { + return ( + + + + + + ); + } ) } + +
Feature NameEnabled?Toggle
+ { feature_name } + { features[ feature_name ].toString() } + +
+
+ ); +} + +export default Features;