Merge pull request #33329 from woocommerce/add/admin-tester

Add WooCommerce Admin Test Helper
This commit is contained in:
Paul Sealock 2022-06-15 07:31:21 +12:00 committed by GitHub
commit dc2977cea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 6935 additions and 119 deletions

View File

@ -13,5 +13,4 @@ phpcs.xml
# build files # build files
woocommerce-beta-tester.zip woocommerce-beta-tester.zip
node_modules/ node_modules/
build/
bin/ bin/

View File

@ -1 +1,5 @@
*.min.js *.min.js
build
build-module
node_modules
vendor

View File

@ -1,25 +1,13 @@
{ {
"root": true, "root": true,
"extends": [ "plugin:@woocommerce/eslint-plugin/recommended" ],
"env": { "env": {
"browser": true, "browser": true,
"node": true "node": true
}, },
"globals": {
"wp": true,
"es6": true
},
"rules": { "rules": {
"camelcase": 0, "camelcase": 0,
"indent": 0, "react/react-in-jsx-scope": "off",
"max-len": [ "no-alert": "off"
2,
{
"code": 140
}
],
"no-console": 1
},
"parserOptions": {
"ecmaVersion": 6
} }
} }

View File

@ -20,3 +20,6 @@ node_modules/
# Built assets # Built assets
build/ build/
woocommerce-beta-tester.zip woocommerce-beta-tester.zip
build
build-module
build-style

View File

@ -0,0 +1,129 @@
# WooCommerce Admin Test Helper
A plugin that makes it easier to test the WooCommerce Admin plugin.
## Development
To get started, run the following commands:
```text
npm install
npm start
```
See [wp-scripts](https://github.com/WordPress/gutenberg/tree/master/packages/scripts) for more usage information.
## Extending
There are two client-side filters available if you want to extend the test
helper with your own plugin's test setup code.
This example adds a new tab:
```
import { addFilter } from '@wordpress/hooks';
const SuperSekret = () => (
<>
<h2>Super sekret</h2>
<p>This section contains super sekret tools.</p>
<NewTool/>
</>
);
addFilter(
'woocommerce_admin_test_helper_tabs',
'wath',
( tabs ) => [
...tabs,
{
name: 'super-sekret',
title: 'Super sekret',
content: <SuperSekret/>,
}
]
);
```
This example adds a new tool to the existing Options tab:
```
import { addFilter } from '@wordpress/hooks';
const NewTool = () => (
<>
<strong>New tool</strong>
<p>Description</p>
<button>Execute</button>
</>
);
addFilter(
'woocommerce_admin_test_helper_tab_options',
'wath',
( entries ) => [
...entries,
<NewTool/>
]
);
```
Register a REST API endpoint to perform server-side actions in the usual way:
```
add_action( 'rest_api_init', function() {
register_rest_route(
'your-plugin/v1',
'/area/action',
array(
'methods' => 'POST',
'callback' => 'your_plugin_area_action',
'permission_callback' => function( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit ) ) {
return new \WP_Error(
'woocommerce_rest_cannot_edit',
__( 'Sorry, you cannot perform this action', 'your-plugin' )
);
}
return true;
}
)
);
} );
function your_plugin_area_action() {
return [];
}
```
This would be used on the client like this:
```
import apiFetch from '@wordpress/api-fetch';
...
const response = await apiFetch( {
path: '/your-plugin/v1/area/action',
method: 'POST',
} );
```
### Deploying
Prerequisites:
- [Hub](https://github.com/github/hub)
- Write access to this repository
You can create a test ZIP of the plugin using this command:
```
npm run build
```
This creates `woocommerce-admin-test-helper.zip` in the project root.
We release the plugin using GitHub Releases. There is a script to automate this:
0. Make sure the version is updated in `woocommerce-admin-test-helper.php` and `package.json`
1. Commit and push to `trunk`
2. Run `npm run release`
3. Make sure you provide the correct version number when prompted
4. That's it!

View File

@ -0,0 +1,67 @@
<?php
use Automattic\WooCommerce\Admin\Notes\Note;
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/add-note/v1',
'admin_notes_add_note'
);
function admin_notes_add_note( $request ) {
$note = new Note();
$mock_note_data = get_mock_note_data();
$type = $request->get_param( 'type' );
$layout = $request->get_param( 'layout' );
$note->set_name( $request->get_param( 'name' ) );
$note->set_title( $request->get_param( 'title' ) );
$note->set_content( $mock_note_data[ 'content' ] );
$note->set_image( $mock_note_data[ $type ][ $layout ] );
$note->set_layout( $layout );
$note->set_type( $type );
possibly_add_action( $note );
if ( 'email' === $type ) {
add_email_note_params( $note );
}
$note->save();
return true;
}
function add_email_note_params( $note ) {
$additional_data = array(
'role' => 'administrator',
);
$note->set_content_data( (object) $additional_data );
}
function possibly_add_action( $note ) {
if ( $note->get_type() === 'info' ) {
return;
}
$action_name = sprintf(
'test-action-%s',
$note->get_name()
);
$note->add_action( $action_name, 'Test action', wc_admin_url() );
}
function get_mock_note_data() {
$plugin_url = site_url() . '/wp-content/plugins/woocommerce-admin-test-helper/';
return array(
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
'info' => array(
'banner' => $plugin_url . 'images/admin-notes/banner.jpg',
'thumbnail' => $plugin_url . 'images/admin-notes/thumbnail.jpg',
'plain' => ''
),
'email' => array(
'plain' => $plugin_url . 'images/admin-notes/woocommerce-logo-vector.png'
),
'update' => array(
'plain' => ''
)
);
}

View File

@ -0,0 +1,18 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/delete-all-notes/v1',
'admin_notes_delete_all_notes'
);
function admin_notes_delete_all_notes() {
global $wpdb;
$deleted_note_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_notes" );
$deleted_action_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions" );
return array(
'deleted_note_count' => $deleted_note_count,
'deleted_action_count' => $deleted_action_count,
);
}

View File

@ -0,0 +1,41 @@
<?php
function register_woocommerce_admin_test_helper_rest_route( $route, $callback, $additional_options = array() ) {
add_action( 'rest_api_init', function() use ( $route, $callback, $additional_options ) {
$default_options = array(
'methods' => 'POST',
'callback' => $callback,
'permission_callback' => function( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error(
'woocommerce_rest_cannot_edit',
__( 'Sorry, you cannot perform this action', 'woocommerce-admin-test-helper' )
);
}
return true;
},
);
$default_options = array_merge( $default_options, $additional_options );
register_rest_route(
'wc-admin-test-helper',
$route,
$default_options
);
} );
}
require( 'admin-notes/delete-all-notes.php' );
require( 'admin-notes/add-note.php' );
require( 'tools/trigger-wca-install.php' );
require( 'tools/trigger-cron-job.php' );
require( 'tools/run-wc-admin-daily.php' );
require( 'options/rest-api.php' );
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' );
require( 'rest-api-filters/rest-api-filters.php' );
require( 'rest-api-filters/hook.php' );

View File

@ -0,0 +1,67 @@
<?php
use Automattic\WooCommerce\Admin\Notes\Note;
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/add-note/v1',
'admin_notes_add_note'
);
function admin_notes_add_note( $request ) {
$note = new Note();
$mock_note_data = get_mock_note_data();
$type = $request->get_param( 'type' );
$layout = $request->get_param( 'layout' );
$note->set_name( $request->get_param( 'name' ) );
$note->set_title( $request->get_param( 'title' ) );
$note->set_content( $mock_note_data[ 'content' ] );
$note->set_image( $mock_note_data[ $type ][ $layout ] );
$note->set_layout( $layout );
$note->set_type( $type );
possibly_add_action( $note );
if ( 'email' === $type ) {
add_email_note_params( $note );
}
$note->save();
return true;
}
function add_email_note_params( $note ) {
$additional_data = array(
'role' => 'administrator',
);
$note->set_content_data( (object) $additional_data );
}
function possibly_add_action( $note ) {
if ( $note->get_type() === 'info' ) {
return;
}
$action_name = sprintf(
'test-action-%s',
$note->get_name()
);
$note->add_action( $action_name, 'Test action', wc_admin_url() );
}
function get_mock_note_data() {
$plugin_url = site_url() . '/wp-content/plugins/woocommerce-admin-test-helper/';
return array(
'content' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.',
'info' => array(
'banner' => $plugin_url . 'images/admin-notes/banner.jpg',
'thumbnail' => $plugin_url . 'images/admin-notes/thumbnail.jpg',
'plain' => ''
),
'email' => array(
'plain' => $plugin_url . 'images/admin-notes/woocommerce-logo-vector.png'
),
'update' => array(
'plain' => ''
)
);
}

View File

@ -0,0 +1,18 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/admin-notes/delete-all-notes/v1',
'admin_notes_delete_all_notes'
);
function admin_notes_delete_all_notes() {
global $wpdb;
$deleted_note_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_notes" );
$deleted_action_count = $wpdb->query( "DELETE FROM {$wpdb->prefix}wc_admin_note_actions" );
return array(
'deleted_note_count' => $deleted_note_count,
'deleted_action_count' => $deleted_action_count,
);
}

View File

@ -0,0 +1,41 @@
<?php
function register_woocommerce_admin_test_helper_rest_route( $route, $callback, $additional_options = array() ) {
add_action( 'rest_api_init', function() use ( $route, $callback, $additional_options ) {
$default_options = array(
'methods' => 'POST',
'callback' => $callback,
'permission_callback' => function( $request ) {
if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) {
return new \WP_Error(
'woocommerce_rest_cannot_edit',
__( 'Sorry, you cannot perform this action', 'woocommerce-admin-test-helper' )
);
}
return true;
},
);
$default_options = array_merge( $default_options, $additional_options );
register_rest_route(
'wc-admin-test-helper',
$route,
$default_options
);
} );
}
require( 'admin-notes/delete-all-notes.php' );
require( 'admin-notes/add-note.php' );
require( 'tools/trigger-wca-install.php' );
require( 'tools/trigger-cron-job.php' );
require( 'tools/run-wc-admin-daily.php' );
require( 'options/rest-api.php' );
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' );
require( 'rest-api-filters/rest-api-filters.php' );
require( 'rest-api-filters/hook.php' );

View File

@ -0,0 +1,59 @@
<?php
use Automattic\WooCommerce\Admin\Features\Features;
const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values';
register_woocommerce_admin_test_helper_rest_route(
'/features/(?P<feature_name>[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();
}

View File

@ -0,0 +1,79 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/options',
'wca_test_helper_get_options',
array(
'methods' => 'GET',
'args' => array(
'page' => array(
'description' => 'Current page of the collection.',
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
),
'per_page' => array(
'description' => 'Maximum number of items to be returned in result set.',
'type' => 'integer',
'default' => 10,
'sanitize_callback' => 'absint',
),
'search' => array(
'description' => 'Limit results to those matching a string.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/options/(?P<option_names>(.*)+)',
'wca_test_helper_delete_option',
array(
'methods' => 'DELETE',
'args' => array(
'option_names' => array(
'type' => 'string',
),
),
)
);
function wca_test_helper_delete_option( $request ) {
global $wpdb;
$option_names = explode( ',', $request->get_param( 'option_names' ) );
$option_names = array_map( function( $option_name ) {
return "'" . $option_name . "'";
}, $option_names );
$option_names = implode( ',', $option_names );
$query = "delete from {$wpdb->prefix}options where option_name in ({$option_names})";
$wpdb->query( $query );
return new WP_REST_RESPONSE( null, 204 );
}
function wca_test_helper_get_options( $request ) {
global $wpdb;
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
$search = $request->get_param( 'search' );
$query = "
select option_id, option_name, option_value, autoload
from {$wpdb->prefix}options
";
if ( $search ) {
$query .= "where option_name like '%{$search}%'";
}
$query .= ' order by option_id desc limit 30';
$options = $wpdb->get_results( $query );
return new WP_REST_Response( $options, 200 );
}

View File

@ -0,0 +1,52 @@
<?php
$filters = get_option(WCA_Test_Helper_Rest_Api_Filters::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, [] );
function array_dot_set( &$array, $key, $value ) {
if ( is_null( $key ) ) {
return $array = $value;
}
$keys = explode('.', $key);
while ( count($keys) > 1 ) {
$key = array_shift($keys);
if (! isset($array[$key]) || ! is_array($array[$key]) ) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[ array_shift($keys) ] = $value;
return $array;
}
add_filter(
'rest_request_after_callbacks',
function ( $response, array $handler, \WP_REST_Request $request ) use ( $filters ) {
if (! $response instanceof \WP_REST_Response ) {
return $response;
}
$route = $request->get_route();
$filters = array_filter(
$filters, function ( $filter ) use ( $request, $route ) {
if ($filter['enabled'] && $filter['endpoint'] == $route ) {
return true;
}
return false;
}
);
$data = $response->get_data();
foreach ( $filters as $filter ) {
array_dot_set($data, $filter['dot_notation'], $filter['replacement']);
}
$response->set_data($data);
return $response;
},
10,
3
);

View File

@ -0,0 +1,109 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'create' ],
array(
'methods' => 'POST',
'args' => array(
'endpoint' => array(
'description' => 'Rest API endpoint.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'dot_notation' => array(
'description' => 'Dot notation of the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'replacement' => array(
'description' => 'Replacement value for the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'delete' ],
array(
'methods' => 'DELETE',
'args' => array(
'index' => array(
'description' => 'Rest API endpoint.',
'type' => 'integer',
'required' => true,
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters/(?P<index>\d+)/toggle',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'toggle' ],
array(
'methods' => 'POST',
)
);
class WCA_Test_Helper_Rest_Api_Filters {
const WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION = 'wc-admin-test-helper-rest-api-filters';
public static function create( $request ) {
$endpoint = $request->get_param('endpoint');
$dot_notation = $request->get_param('dot_notation');
$replacement = $request->get_param('replacement');
self::update(
function ( $filters ) use (
$endpoint,
$dot_notation,
$replacement
) {
$filters[] = array(
'endpoint' => $endpoint,
'dot_notation' => $dot_notation,
'replacement' => filter_var( $replacement, FILTER_VALIDATE_BOOLEAN ),
'enabled' => true,
);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function update( callable $callback ) {
$filters = get_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, array());
$filters = $callback( $filters );
return update_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, $filters);
}
public static function delete( $request ) {
self::update(
function ( $filters ) use ( $request ) {
array_splice($filters, $request->get_param('index'), 1);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function toggle( $request ) {
self::update(
function ( $filters ) use ( $request ) {
$index = $request->get_param('index');
$filters[$index]['enabled'] = !$filters[$index]['enabled'];
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
}

View File

@ -0,0 +1,16 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/delete-all-products/v1',
'tools_delete_all_products'
);
function tools_delete_all_products() {
$query = new \WC_Product_Query();
$products = $query->get_products();
foreach ( $products as $product ) {
$product->delete( true );
}
return true;
}

View File

@ -0,0 +1,46 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/toggle-emails/v1',
'toggle_emails'
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-email-status/v1',
'get_email_status',
array(
'methods' => 'GET',
)
);
function toggle_emails() {
$emails_disabled = 'yes';
if ( $emails_disabled === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
$emails_disabled = 'no';
remove_filter('woocommerce_email_get_option', 'disable_wc_emails' );
}
update_option('wc_admin_test_helper_email_disabled', $emails_disabled );
return new WP_REST_Response( $emails_disabled, 200 );
}
function get_email_status() {
$emails_disabled = get_option( 'wc_admin_test_helper_email_disabled', 'no' );
return new WP_REST_Response( $emails_disabled, 200 );
}
if ( 'yes' === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
add_filter('woocommerce_email_get_option', 'disable_wc_emails' );
add_action( 'woocommerce_email', 'unhook_other_wc_emails' );
}
function disable_wc_emails( $key ) {
if ( $key === 'enabled' ) {
return false;
}
}
function unhook_other_wc_emails( $email ) {
remove_action( 'woocommerce_low_stock_notification', array( $email, 'low_stock' ) );
remove_action( 'woocommerce_no_stock_notification', array( $email, 'no_stock' ) );
remove_action( 'woocommerce_product_on_backorder_notification', array( $email, 'backorder' ) );
remove_action( 'woocommerce_new_customer_note_notification', array( $email->emails['WC_Email_Customer_Note'], 'trigger' ) );
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/run-wc-admin-daily/v1',
'tools_run_wc_admin_daily'
);
function tools_run_wc_admin_daily() {
do_action('wc_admin_daily');
return true;
}

View File

@ -0,0 +1,98 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-cron-list/v1',
'tools_get_cron_list',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-cron/v1',
'trigger_selected_cron',
array(
'methods' => 'POST',
'args' => array(
'hook' => array(
'description' => 'Name of the cron that will be triggered.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'signature' => array(
'description' => 'Signature of the cron to trigger.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
function tools_get_cron_list() {
$crons = _get_cron_array();
$events = array();
if ( empty( $crons ) ) {
return array();
}
foreach ( $crons as $cron ) {
foreach ( $cron as $hook => $data ) {
foreach ( $data as $signature => $element ) {
$events[ $hook ] = (object) array(
'hook' => $hook,
'signature' => $signature,
);
}
}
}
return new WP_REST_Response( $events, 200 );
}
function trigger_selected_cron( $request ) {
$hook = $request->get_param( 'hook' );
$signature = $request->get_param( 'signature' );
if ( ! isset( $hook ) || ! isset( $signature ) ) {
return;
}
$crons = _get_cron_array();
foreach ( $crons as $cron ) {
if ( isset( $cron[ $hook ][ $signature ] ) ) {
$args = $cron[ $hook ][ $signature ]['args'];
delete_transient( 'doing_cron' );
$scheduled = schedule_event( $hook, $args );
if ( false === $scheduled ) {
return $scheduled;
}
add_filter( 'cron_request', function( array $cron_request ) {
$cron_request['url'] = add_query_arg( 'run-cron', 1, $cron_request['url'] );
return $cron_request;
} );
spawn_cron();
sleep( 1 );
return true;
}
}
return false;
}
function schedule_event( $hook, $args = array() ) {
$event = (object) array(
'hook' => $hook,
'timestamp' => 1,
'schedule' => false,
'args' => $args,
);
$crons = (array) _get_cron_array();
$key = md5( serialize( $event->args ) );
$crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
'schedule' => $event->schedule,
'args' => $event->args,
);
uksort( $crons, 'strnatcasecmp' );
return _set_cron_array( $crons );
}

View File

@ -0,0 +1,47 @@
<?php
use Automattic\WooCommerce\Admin\API\Reports\Cache;
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-update-versions/v1',
'tools_get_wc_admin_versions',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-update-callbacks/v1',
'trigger_selected_update_callbacks',
array(
'methods' => 'POST',
'args' => array(
'version' => array(
'description' => 'Name of the update version',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
)
),
)
);
function tools_get_wc_admin_versions() {
$db_updates = \WC_Install::get_db_update_callbacks();
return new WP_REST_Response( array_keys( $db_updates ), 200 );
}
function trigger_selected_update_callbacks( $request ) {
$version = $request->get_param( 'version' );
if ( ! isset( $version ) ) {
return;
}
$db_updates = \WC_Install::get_db_update_callbacks();
$update_callbacks = $db_updates[ $version ];
foreach ( $update_callbacks as $update_callback ) {
call_user_func( $update_callback );
}
return false;
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-wca-install/v1',
'tools_trigger_wca_install'
);
function tools_trigger_wca_install() {
\WC_Install::install();
return true;
}

View File

@ -0,0 +1,53 @@
<?php
/**
* A class for logging tracked events.
*/
class TracksDebugLog {
/**
* Logger class to use.
*
* @var WC_Logger_Interface|null
*/
private $logger;
/**
* Logger source.
*
* @var string logger source.
*/
private $source = 'tracks';
/**
* Initialize hooks.
*/
public function __construct() {
add_filter( 'woocommerce_tracks_event_properties', array( $this, 'log_event' ), 10, 2 );
$logger = wc_get_logger();
$this->logger = $logger;
$this->logger = $logger;
}
/**
* Log the event.
*
* @param array $properties Event properties.
* @param string $event_name Event name.
*/
public function log_event( $properties, $event_name ) {
$this->logger->debug(
$event_name,
array( 'source' => $this->source )
);
foreach ( $properties as $key => $property ) {
$this->logger->debug(
" - {$key}: {$property}",
array( 'source' => $this->source )
);
}
return $properties;
}
}
new TracksDebugLog();

View File

@ -0,0 +1,59 @@
<?php
use Automattic\WooCommerce\Admin\Features\Features;
const OPTION_NAME_PREFIX = 'wc_admin_helper_feature_values';
register_woocommerce_admin_test_helper_rest_route(
'/features/(?P<feature_name>[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();
}

View File

@ -0,0 +1,79 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/options',
'wca_test_helper_get_options',
array(
'methods' => 'GET',
'args' => array(
'page' => array(
'description' => 'Current page of the collection.',
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
),
'per_page' => array(
'description' => 'Maximum number of items to be returned in result set.',
'type' => 'integer',
'default' => 10,
'sanitize_callback' => 'absint',
),
'search' => array(
'description' => 'Limit results to those matching a string.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/options/(?P<option_names>(.*)+)',
'wca_test_helper_delete_option',
array(
'methods' => 'DELETE',
'args' => array(
'option_names' => array(
'type' => 'string',
),
),
)
);
function wca_test_helper_delete_option( $request ) {
global $wpdb;
$option_names = explode( ',', $request->get_param( 'option_names' ) );
$option_names = array_map( function( $option_name ) {
return "'" . $option_name . "'";
}, $option_names );
$option_names = implode( ',', $option_names );
$query = "delete from {$wpdb->prefix}options where option_name in ({$option_names})";
$wpdb->query( $query );
return new WP_REST_RESPONSE( null, 204 );
}
function wca_test_helper_get_options( $request ) {
global $wpdb;
$per_page = $request->get_param( 'per_page' );
$page = $request->get_param( 'page' );
$search = $request->get_param( 'search' );
$query = "
select option_id, option_name, option_value, autoload
from {$wpdb->prefix}options
";
if ( $search ) {
$query .= "where option_name like '%{$search}%'";
}
$query .= ' order by option_id desc limit 30';
$options = $wpdb->get_results( $query );
return new WP_REST_Response( $options, 200 );
}

View File

@ -0,0 +1,52 @@
<?php
$filters = get_option(WCA_Test_Helper_Rest_Api_Filters::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, [] );
function array_dot_set( &$array, $key, $value ) {
if ( is_null( $key ) ) {
return $array = $value;
}
$keys = explode('.', $key);
while ( count($keys) > 1 ) {
$key = array_shift($keys);
if (! isset($array[$key]) || ! is_array($array[$key]) ) {
$array[$key] = [];
}
$array = &$array[$key];
}
$array[ array_shift($keys) ] = $value;
return $array;
}
add_filter(
'rest_request_after_callbacks',
function ( $response, array $handler, \WP_REST_Request $request ) use ( $filters ) {
if (! $response instanceof \WP_REST_Response ) {
return $response;
}
$route = $request->get_route();
$filters = array_filter(
$filters, function ( $filter ) use ( $request, $route ) {
if ($filter['enabled'] && $filter['endpoint'] == $route ) {
return true;
}
return false;
}
);
$data = $response->get_data();
foreach ( $filters as $filter ) {
array_dot_set($data, $filter['dot_notation'], $filter['replacement']);
}
$response->set_data($data);
return $response;
},
10,
3
);

View File

@ -0,0 +1,109 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'create' ],
array(
'methods' => 'POST',
'args' => array(
'endpoint' => array(
'description' => 'Rest API endpoint.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'dot_notation' => array(
'description' => 'Dot notation of the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
'replacement' => array(
'description' => 'Replacement value for the target field.',
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'delete' ],
array(
'methods' => 'DELETE',
'args' => array(
'index' => array(
'description' => 'Rest API endpoint.',
'type' => 'integer',
'required' => true,
),
),
)
);
register_woocommerce_admin_test_helper_rest_route(
'/rest-api-filters/(?P<index>\d+)/toggle',
[ WCA_Test_Helper_Rest_Api_Filters::class, 'toggle' ],
array(
'methods' => 'POST',
)
);
class WCA_Test_Helper_Rest_Api_Filters {
const WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION = 'wc-admin-test-helper-rest-api-filters';
public static function create( $request ) {
$endpoint = $request->get_param('endpoint');
$dot_notation = $request->get_param('dot_notation');
$replacement = $request->get_param('replacement');
self::update(
function ( $filters ) use (
$endpoint,
$dot_notation,
$replacement
) {
$filters[] = array(
'endpoint' => $endpoint,
'dot_notation' => $dot_notation,
'replacement' => filter_var( $replacement, FILTER_VALIDATE_BOOLEAN ),
'enabled' => true,
);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function update( callable $callback ) {
$filters = get_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, array());
$filters = $callback( $filters );
return update_option(self::WC_ADMIN_TEST_HELPER_REST_API_FILTER_OPTION, $filters);
}
public static function delete( $request ) {
self::update(
function ( $filters ) use ( $request ) {
array_splice($filters, $request->get_param('index'), 1);
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
public static function toggle( $request ) {
self::update(
function ( $filters ) use ( $request ) {
$index = $request->get_param('index');
$filters[$index]['enabled'] = !$filters[$index]['enabled'];
return $filters;
}
);
return new WP_REST_RESPONSE(null, 204);
}
}

View File

@ -0,0 +1,16 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/delete-all-products/v1',
'tools_delete_all_products'
);
function tools_delete_all_products() {
$query = new \WC_Product_Query();
$products = $query->get_products();
foreach ( $products as $product ) {
$product->delete( true );
}
return true;
}

View File

@ -0,0 +1,46 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/toggle-emails/v1',
'toggle_emails'
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-email-status/v1',
'get_email_status',
array(
'methods' => 'GET',
)
);
function toggle_emails() {
$emails_disabled = 'yes';
if ( $emails_disabled === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
$emails_disabled = 'no';
remove_filter('woocommerce_email_get_option', 'disable_wc_emails' );
}
update_option('wc_admin_test_helper_email_disabled', $emails_disabled );
return new WP_REST_Response( $emails_disabled, 200 );
}
function get_email_status() {
$emails_disabled = get_option( 'wc_admin_test_helper_email_disabled', 'no' );
return new WP_REST_Response( $emails_disabled, 200 );
}
if ( 'yes' === get_option( 'wc_admin_test_helper_email_disabled', 'no' ) ) {
add_filter('woocommerce_email_get_option', 'disable_wc_emails' );
add_action( 'woocommerce_email', 'unhook_other_wc_emails' );
}
function disable_wc_emails( $key ) {
if ( $key === 'enabled' ) {
return false;
}
}
function unhook_other_wc_emails( $email ) {
remove_action( 'woocommerce_low_stock_notification', array( $email, 'low_stock' ) );
remove_action( 'woocommerce_no_stock_notification', array( $email, 'no_stock' ) );
remove_action( 'woocommerce_product_on_backorder_notification', array( $email, 'backorder' ) );
remove_action( 'woocommerce_new_customer_note_notification', array( $email->emails['WC_Email_Customer_Note'], 'trigger' ) );
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/run-wc-admin-daily/v1',
'tools_run_wc_admin_daily'
);
function tools_run_wc_admin_daily() {
do_action('wc_admin_daily');
return true;
}

View File

@ -0,0 +1,98 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-cron-list/v1',
'tools_get_cron_list',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-cron/v1',
'trigger_selected_cron',
array(
'methods' => 'POST',
'args' => array(
'hook' => array(
'description' => 'Name of the cron that will be triggered.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
'signature' => array(
'description' => 'Signature of the cron to trigger.',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
)
);
function tools_get_cron_list() {
$crons = _get_cron_array();
$events = array();
if ( empty( $crons ) ) {
return array();
}
foreach ( $crons as $cron ) {
foreach ( $cron as $hook => $data ) {
foreach ( $data as $signature => $element ) {
$events[ $hook ] = (object) array(
'hook' => $hook,
'signature' => $signature,
);
}
}
}
return new WP_REST_Response( $events, 200 );
}
function trigger_selected_cron( $request ) {
$hook = $request->get_param( 'hook' );
$signature = $request->get_param( 'signature' );
if ( ! isset( $hook ) || ! isset( $signature ) ) {
return;
}
$crons = _get_cron_array();
foreach ( $crons as $cron ) {
if ( isset( $cron[ $hook ][ $signature ] ) ) {
$args = $cron[ $hook ][ $signature ]['args'];
delete_transient( 'doing_cron' );
$scheduled = schedule_event( $hook, $args );
if ( false === $scheduled ) {
return $scheduled;
}
add_filter( 'cron_request', function( array $cron_request ) {
$cron_request['url'] = add_query_arg( 'run-cron', 1, $cron_request['url'] );
return $cron_request;
} );
spawn_cron();
sleep( 1 );
return true;
}
}
return false;
}
function schedule_event( $hook, $args = array() ) {
$event = (object) array(
'hook' => $hook,
'timestamp' => 1,
'schedule' => false,
'args' => $args,
);
$crons = (array) _get_cron_array();
$key = md5( serialize( $event->args ) );
$crons[ $event->timestamp ][ $event->hook ][ $key ] = array(
'schedule' => $event->schedule,
'args' => $event->args,
);
uksort( $crons, 'strnatcasecmp' );
return _set_cron_array( $crons );
}

View File

@ -0,0 +1,47 @@
<?php
use Automattic\WooCommerce\Admin\API\Reports\Cache;
register_woocommerce_admin_test_helper_rest_route(
'/tools/get-update-versions/v1',
'tools_get_wc_admin_versions',
array(
'methods' => 'GET',
)
);
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-selected-update-callbacks/v1',
'trigger_selected_update_callbacks',
array(
'methods' => 'POST',
'args' => array(
'version' => array(
'description' => 'Name of the update version',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
)
),
)
);
function tools_get_wc_admin_versions() {
$db_updates = \WC_Install::get_db_update_callbacks();
return new WP_REST_Response( array_keys( $db_updates ), 200 );
}
function trigger_selected_update_callbacks( $request ) {
$version = $request->get_param( 'version' );
if ( ! isset( $version ) ) {
return;
}
$db_updates = \WC_Install::get_db_update_callbacks();
$update_callbacks = $db_updates[ $version ];
foreach ( $update_callbacks as $update_callback ) {
call_user_func( $update_callback );
}
return false;
}

View File

@ -0,0 +1,11 @@
<?php
register_woocommerce_admin_test_helper_rest_route(
'/tools/trigger-wca-install/v1',
'tools_trigger_wca_install'
);
function tools_trigger_wca_install() {
\WC_Install::install();
return true;
}

View File

@ -0,0 +1,53 @@
<?php
/**
* A class for logging tracked events.
*/
class TracksDebugLog {
/**
* Logger class to use.
*
* @var WC_Logger_Interface|null
*/
private $logger;
/**
* Logger source.
*
* @var string logger source.
*/
private $source = 'tracks';
/**
* Initialize hooks.
*/
public function __construct() {
add_filter( 'woocommerce_tracks_event_properties', array( $this, 'log_event' ), 10, 2 );
$logger = wc_get_logger();
$this->logger = $logger;
$this->logger = $logger;
}
/**
* Log the event.
*
* @param array $properties Event properties.
* @param string $event_name Event name.
*/
public function log_event( $properties, $event_name ) {
$this->logger->debug(
$event_name,
array( 'source' => $this->source )
);
foreach ( $properties as $key => $property ) {
$this->logger->debug(
" - {$key}: {$property}",
array( 'source' => $this->source )
);
}
return $properties;
}
}
new TracksDebugLog();

View File

@ -1,44 +1,51 @@
/** /**
* Handles version information modal. * Handles version information modal.
* *
* @package WooCommerceBetaTester\JS * @package
*/ */
jQuery(function( $ ) { // eslint-disable-next-line no-undef
jQuery( function ( $ ) {
/** /**
* Version information * Version information
*/ */
var wc_beta_tester_version_information = { const wc_beta_tester_version_information = {
/** /**
* Initialize Version Information click * Initialize Version Information click
*/ */
init: function() { init() {
$( '#wp-admin-bar-show-version-info' ) $( '#wp-admin-bar-show-version-info' ).on(
.on( 'click', this.showModal ); 'click',
this.showModal
);
}, },
/** /**
* Handler for showing/hiding version information modal * Handler for showing/hiding version information modal
*
* @param {Event} event
*/ */
showModal: function( event ) { showModal( event ) {
event.preventDefault(); event.preventDefault();
// Prevent multiple modals. // Prevent multiple modals.
if ( 0 < $( '.wc-backbone-modal-beta-tester-version-info' ).length ) { if (
$( '.wc-backbone-modal-beta-tester-version-info' ).length > 0
) {
return; return;
} }
$( this ).WCBackboneModal({ $( this ).WCBackboneModal( {
template: 'wc-beta-tester-version-info', template: 'wc-beta-tester-version-info',
variable: { variable: {
// eslint-disable-next-line no-undef
version: wc_beta_tester_version_info_params.version, version: wc_beta_tester_version_info_params.version,
// eslint-disable-next-line no-undef
description: wc_beta_tester_version_info_params.description, description: wc_beta_tester_version_info_params.description,
}, },
}); } );
} },
}; };
wc_beta_tester_version_information.init(); wc_beta_tester_version_information.init();
}); } );

View File

@ -1 +1 @@
jQuery(function(i){({init:function(){i("#wp-admin-bar-show-version-info").on("click",this.showModal)},showModal:function(e){e.preventDefault(),0<i(".wc-backbone-modal-beta-tester-version-info").length||i(this).WCBackboneModal({template:"wc-beta-tester-version-info",variable:{version:wc_beta_tester_version_info_params.version,description:wc_beta_tester_version_info_params.description}})}}).init()}); jQuery(function(i){const e={init(){i("#wp-admin-bar-show-version-info").on("click",this.showModal)},showModal(e){e.preventDefault(),0<i(".wc-backbone-modal-beta-tester-version-info").length||i(this).WCBackboneModal({template:"wc-beta-tester-version-info",variable:{version:wc_beta_tester_version_info_params.version,description:wc_beta_tester_version_info_params.description}})}};e.init()});

View File

@ -1,57 +1,69 @@
/** /**
* Handles the version picker form. * Handles the version picker form.
* *
* @package WooCommerceBetaTester\JS * @package
*/ */
jQuery(function( $ ) { // eslint-disable-next-line no-undef
jQuery( function ( $ ) {
/** /**
* Version picker * Version picker
*/ */
var wc_beta_tester_version_picker = { const wc_beta_tester_version_picker = {
/** /**
* Initialize Version Information click * Initialize Version Information click
*/ */
init: function() { init() {
instance = this; const instance = this;
instance.new_version = undefined; instance.new_version = undefined;
$( '#wcbt-modal-version-switch-confirm' ) $( '#wcbt-modal-version-switch-confirm' ).on(
.on( 'click', this.showConfirmVersionSwitchModal ); 'click',
$( 'input[type=radio][name=wcbt_switch_to_version]' ).change( function() { this.showConfirmVersionSwitchModal
);
$( 'input[type=radio][name=wcbt_switch_to_version]' )
.change( function () {
if ( $( this ).is( ':checked' ) ) { if ( $( this ).is( ':checked' ) ) {
instance.new_version = $( this ).val(); instance.new_version = $( this ).val();
} }
} ).trigger( 'change' ); } )
.trigger( 'change' );
}, },
/** /**
* Handler for showing/hiding version switch modal * Handler for showing/hiding version switch modal
*
* @param {Event} event
*/ */
showConfirmVersionSwitchModal: function( event ) { showConfirmVersionSwitchModal( event ) {
event.preventDefault(); event.preventDefault();
const instance = this;
if ( ! instance.new_version ) { if ( ! instance.new_version ) {
// eslint-disable-next-line no-undef
alert( wc_beta_tester_version_picker_params.i18n_pick_version ); alert( wc_beta_tester_version_picker_params.i18n_pick_version );
} else { } else {
$( this ).WCBackboneModal({ $( this ).WCBackboneModal( {
template: 'wcbt-version-switch-confirm', template: 'wcbt-version-switch-confirm',
variable: { variable: {
new_version: instance.new_version, new_version: instance.new_version,
}, },
}); } );
$( '#wcbt-submit-version-switch' ) $( '#wcbt-submit-version-switch' ).on(
.on( 'click', instance.submitSwitchVersionForm ); 'click',
instance.submitSwitchVersionForm
);
} }
}, },
/** /**
* Submit form to switch version of WooCommerce. * Submit form to switch version of WooCommerce.
*
* @param {Event} event
*/ */
submitSwitchVersionForm: function( event ) { submitSwitchVersionForm( event ) {
event.preventDefault(); event.preventDefault();
$( 'form[name=wcbt-select-version]' ).get( 0 ).submit(); $( 'form[name=wcbt-select-version]' ).get( 0 ).submit();
@ -59,5 +71,4 @@ jQuery(function( $ ) {
}; };
wc_beta_tester_version_picker.init(); wc_beta_tester_version_picker.init();
} );
});

View File

@ -1 +1 @@
jQuery(function(n){({init:function(){instance=this,instance.new_version=void 0,n("#wcbt-modal-version-switch-confirm").on("click",this.showConfirmVersionSwitchModal),n("input[type=radio][name=wcbt_switch_to_version]").change(function(){n(this).is(":checked")&&(instance.new_version=n(this).val())}).trigger("change")},showConfirmVersionSwitchModal:function(i){i.preventDefault(),instance.new_version?(n(this).WCBackboneModal({template:"wcbt-version-switch-confirm",variable:{new_version:instance.new_version}}),n("#wcbt-submit-version-switch").on("click",instance.submitSwitchVersionForm)):alert(wc_beta_tester_version_picker_params.i18n_pick_version)},submitSwitchVersionForm:function(i){i.preventDefault(),n("form[name=wcbt-select-version]").get(0).submit()}}).init()}); jQuery(function(e){const i={init(){const i=this;i.new_version=void 0,e("#wcbt-modal-version-switch-confirm").on("click",this.showConfirmVersionSwitchModal),e("input[type=radio][name=wcbt_switch_to_version]").change(function(){e(this).is(":checked")&&(i.new_version=e(this).val())}).trigger("change")},showConfirmVersionSwitchModal(i){i.preventDefault();i=this;i.new_version?(e(this).WCBackboneModal({template:"wcbt-version-switch-confirm",variable:{new_version:i.new_version}}),e("#wcbt-submit-version-switch").on("click",i.submitSwitchVersionForm)):alert(wc_beta_tester_version_picker_params.i18n_pick_version)},submitSwitchVersionForm(i){i.preventDefault(),e("form[name=wcbt-select-version]").get(0).submit()}};i.init()});

View File

@ -12,7 +12,7 @@ mkdir -p "$DEST_PATH"
echo "Installing PHP and JS dependencies..." echo "Installing PHP and JS dependencies..."
pnpm install pnpm install
echo "Running JS Build..." echo "Running JS Build..."
pnpm run uglify pnpm run build
echo "Syncing files..." echo "Syncing files..."
rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add WooCommerce Admin Helper Tester functionality to Beta Tester

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -13,9 +13,23 @@
"build_step": "pnpm run build:zip" "build_step": "pnpm run build:zip"
}, },
"devDependencies": { "devDependencies": {
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
"@wordpress/scripts": "^19.2.4",
"eslint": "5.16.0", "eslint": "5.16.0",
"uglify-js": "^3.5.3" "uglify-js": "^3.5.3"
}, },
"dependencies": {
"@types/prop-types": "^15.7.4",
"@woocommerce/data": "workspace:*",
"@wordpress/api-fetch": "^3.21.5",
"@wordpress/components": "^12.0.7",
"@wordpress/compose": "^3.24.4",
"@wordpress/data": "^4.26.7",
"@wordpress/data-controls": "^1.20.7",
"@wordpress/element": "^2.19.1",
"@wordpress/hooks": "^2.11.1"
},
"assets": { "assets": {
"js": { "js": {
"min": "assets/js/*.min.js", "min": "assets/js/*.min.js",
@ -25,12 +39,25 @@
"scripts": { "scripts": {
"postinstall": "composer install", "postinstall": "composer install",
"changelog": "composer exec -- changelogger", "changelog": "composer exec -- changelogger",
"build": "pnpm run uglify", "build": "pnpm run build:admin && pnpm run uglify",
"build:admin": "wp-scripts build",
"build:zip": "./bin/build-zip.sh", "build:zip": "./bin/build-zip.sh",
"build:dev": "pnpm run lint:js && pnpm run uglify", "build:dev": "pnpm run lint:js && pnpm run build",
"uglify": "rm -f $npm_package_assets_js_min && for f in $npm_package_assets_js_js; do file=${f%.js}; node_modules/.bin/uglifyjs $f -c -m > $file.min.js; done", "uglify": "rm -f $npm_package_assets_js_min && for f in $npm_package_assets_js_js; do file=${f%.js}; node_modules/.bin/uglifyjs $f -c -m > $file.min.js; done",
"lint": "eslint assets/js --ext=js", "check-engines": "wp-scripts check-engines",
"lint:fix": "eslint assets/js --ext=js --fix" "check-licenses": "wp-scripts check-licenses",
"format:js": "wp-scripts format-js",
"lint:css": "wp-scripts lint-style",
"lint:css:fix": "wp-scripts lint-style --fix",
"lint:js": "wp-scripts lint-js",
"lint:js:fix": "wp-scripts lint-js --fix",
"lint:md:docs": "wp-scripts lint-md-docs",
"lint:md:js": "wp-scripts lint-md-js",
"lint:pkg-json": "wp-scripts lint-pkg-json",
"packages-update": "wp-scripts packages-update",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
}, },
"engines": { "engines": {
"node": ">=10.15.0", "node": ">=10.15.0",
@ -45,8 +72,11 @@
"php -d display_errors=1 -l", "php -d display_errors=1 -l",
"composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit" "composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit"
], ],
"!(*min).js": [ "*.(t|j)s?(x)": [
"pnpm lint:fix" "npm run lint:js:fix"
],
"*.scss": [
"npm run lint:css:fix"
] ]
} }
} }

View File

@ -0,0 +1,26 @@
<?php
add_action( 'admin_menu', function() {
add_management_page(
'WooCommerce Admin Test Helper',
'WCA Test Helper',
'install_plugins',
'woocommerce-admin-test-helper',
function() {
?><div id="woocommerce-admin-test-helper-app-root"></div><?php
}
);
} );
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;
} );

View File

@ -0,0 +1,138 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { Button, SelectControl } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
export const AddNote = () => {
const [ isAdding, setIsAdding ] = useState( false );
const [ hasAdded, setHasAdded ] = useState( false );
const [ errorMessage, setErrorMessage ] = useState( false );
const [ noteType, setNoteType ] = useState( 'info' );
const [ noteLayout, setNoteLayout ] = useState( 'plain' );
async function triggerAddNote() {
setIsAdding( true );
setHasAdded( false );
setErrorMessage( false );
const name = prompt( 'Enter the note name' );
if ( ! name ) {
setIsAdding( false );
return;
}
const title = prompt( 'Enter the note title' );
if ( ! title ) {
setIsAdding( false );
return;
}
try {
await apiFetch( {
path: '/wc-admin-test-helper/admin-notes/add-note/v1',
method: 'POST',
data: {
name,
type: noteType,
layout: noteLayout,
title,
},
} );
setHasAdded( true );
} catch ( ex ) {
setErrorMessage( ex.message );
}
setIsAdding( false );
}
function onTypeChange( val ) {
setNoteType( val );
if ( val !== 'info' ) {
setNoteLayout( 'plain' );
}
}
function onLayoutChange( val ) {
setNoteLayout( val );
}
function getAddNoteDescription() {
switch ( noteType ) {
case 'email':
return (
<>
This will add a new <strong>email</strong> note. Enable
email insights{ ' ' }
<a href="/wp-admin/admin.php?page=wc-settings&tab=email">
here
</a>{ ' ' }
and run the cron to send the note by email.
</>
);
default:
return (
<>
This will add a new note. Currently only the note name
and title will be used to create the note.
</>
);
}
}
return (
<>
<p>
<strong>Add a note</strong>
</p>
<div>
{ getAddNoteDescription() }
<br />
<div className="woocommerce-admin-test-helper__add-notes">
<Button
onClick={ triggerAddNote }
disabled={ isAdding }
isPrimary
>
Add admin note
</Button>
<SelectControl
label="Type"
onChange={ onTypeChange }
labelPosition="side"
options={ [
{ label: 'Info', value: 'info' },
{ label: 'Update', value: 'update' },
{ label: 'Email', value: 'email' },
] }
value={ noteType }
/>
<SelectControl
label="Layout"
onChange={ onLayoutChange }
labelPosition="side"
options={ [
{ label: 'Plain', value: 'plain' },
{ label: 'Banner', value: 'banner' },
{ label: 'Thumbnail', value: 'thumbnail' },
] }
disabled={ noteType !== 'info' }
value={ noteLayout }
/>
</div>
<br />
<span className="woocommerce-admin-test-helper__action-status">
{ isAdding && 'Adding, please wait' }
{ hasAdded && 'Note added' }
{ errorMessage && (
<>
<strong>Error:</strong> { errorMessage }
</>
) }
</span>
</div>
</>
);
};

View File

@ -0,0 +1,16 @@
/**
* Internal dependencies
*/
import { DeleteAllNotes } from './delete-all-notes';
import { AddNote } from './add-note';
export const AdminNotes = () => {
return (
<>
<h2>Admin notes</h2>
<p>This section contains tools for managing admin notes.</p>
<AddNote />
<DeleteAllNotes />
</>
);
};

View File

@ -0,0 +1,75 @@
/**
* External dependencies
*/
import { useState } from '@wordpress/element';
import { Button } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
export const DeleteAllNotes = () => {
const [ isDeleting, setIsDeleting ] = useState( false );
const [ deleteStatus, setDeleteStatus ] = useState( false );
const [ errorMessage, setErrorMessage ] = useState( false );
async function triggerDeleteAllNotes() {
setIsDeleting( true );
setErrorMessage( false );
setDeleteStatus( false );
try {
const response = await apiFetch( {
path: '/wc-admin-test-helper/admin-notes/delete-all-notes/v1',
method: 'POST',
} );
setDeleteStatus( response );
} catch ( ex ) {
setErrorMessage( ex.message );
}
setIsDeleting( false );
}
return (
<>
<p>
<strong>Delete all admin notes</strong>
</p>
<p>
This will delete all notes from the{ ' ' }
<code>wp_wc_admin_notes</code>
table, and actions from the{ ' ' }
<code>wp_wc_admin_note_actions</code>
table.
<br />
<Button
onClick={ triggerDeleteAllNotes }
disabled={ isDeleting }
isPrimary
>
Delete all notes
</Button>
<br />
<span className="woocommerce-admin-test-helper__action-status">
{ isDeleting && 'Deleting, please wait.' }
{ deleteStatus && (
<>
Deleted{ ' ' }
<strong>{ deleteStatus.deleted_note_count }</strong>{ ' ' }
admin notes and{ ' ' }
<strong>
{ deleteStatus.deleted_action_count }
</strong>{ ' ' }
actions.
</>
) }
{ errorMessage && (
<>
<strong>Error: </strong>
{ errorMessage }
</>
) }
</span>
</p>
</>
);
};

View File

@ -0,0 +1 @@
export { AdminNotes } from './admin-notes.js';

View File

@ -0,0 +1,72 @@
/**
* External dependencies
*/
import { TabPanel } from '@wordpress/components';
import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
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';
import { default as RestAPIFilters } from '../rest-api-filters';
const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
{
name: 'options',
title: 'Options',
content: <Options />,
},
{
name: 'admin-notes',
title: 'Admin notes',
content: <AdminNotes />,
},
{
name: 'tools',
title: 'Tools',
content: <Tools />,
},
{
name: 'experiments',
title: 'Experiments',
content: <Experiments />,
},
{
name: 'features',
title: 'Features',
content: <Features />,
},
{
name: 'rest-api-filters',
title: 'REST API FIlters',
content: <RestAPIFilters />,
},
] );
export function App() {
return (
<div className="wrap">
<h1>WooCommerce Admin Test Helper</h1>
<TabPanel
className="woocommerce-admin-test-helper__main-tab-panel"
activeClass="active-tab"
tabs={ tabs }
initialTabName={ tabs[ 0 ].name }
>
{ ( tab ) => (
<>
{ tab.content }
{ applyFilters(
`woocommerce_admin_test_helper_tab_${ tab.name }`,
[]
) }
</>
) }
</TabPanel>
</div>
);
}

View File

@ -0,0 +1 @@
export { App } from './app';

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

@ -0,0 +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

@ -0,0 +1,105 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import {
EXPERIMENT_NAME_PREFIX,
TRANSIENT_NAME_PREFIX,
TRANSIENT_TIMEOUT_NAME_PREFIX,
} from './constants';
function toggleFrontendExperiment( experimentName, newVariation ) {
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;
window.localStorage.setItem(
EXPERIMENT_NAME_PREFIX + experimentName,
JSON.stringify( storageItem )
);
}
function* toggleBackendExperiment( experimentName, newVariation ) {
try {
const payload = {};
payload[ TRANSIENT_NAME_PREFIX + experimentName ] = newVariation;
payload[ TRANSIENT_TIMEOUT_NAME_PREFIX + experimentName ] =
Math.round( Date.now() / 1000 ) + 3600;
yield apiFetch( {
method: 'POST',
path: '/wc-admin/options',
headers: { 'content-type': 'application/json' },
body: JSON.stringify( payload ),
} );
} catch ( error ) {
throw new Error();
}
}
export function* toggleExperiment( experimentName, currentVariation ) {
const newVariation =
currentVariation === 'control' ? 'treatment' : 'control';
toggleFrontendExperiment( experimentName, newVariation );
yield toggleBackendExperiment( experimentName, newVariation );
return {
type: TYPES.TOGGLE_EXPERIMENT,
experimentName,
newVariation,
};
}
export function setExperiments( experiments ) {
return {
type: TYPES.SET_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

@ -0,0 +1,6 @@
export const STORE_KEY = 'wc-admin-helper/experiments';
export const EXPERIMENT_NAME_PREFIX = 'explat-experiment--';
export const TRANSIENT_NAME_PREFIX = '_transient_abtest_variation_';
export const TRANSIENT_TIMEOUT_NAME_PREFIX =
'_transient_timeout_abtest_variation';
export const API_NAMESPACE = '/wc-admin-test-helper';

View File

@ -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,
} );

View File

@ -0,0 +1,72 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
experiments: [],
};
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
? action.newVariation
: experiment.variation,
} ) ),
};
case TYPES.SET_EXPERIMENTS:
return {
...state,
experiments: action.experiments,
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { setExperiments } from './actions';
import {
EXPERIMENT_NAME_PREFIX,
TRANSIENT_NAME_PREFIX,
API_NAMESPACE,
} from './constants';
function getExperimentsFromFrontend() {
const storageItems = Object.entries( { ...window.localStorage } ).filter(
( item ) => {
return item[ 0 ].indexOf( EXPERIMENT_NAME_PREFIX ) === 0;
}
);
return storageItems.map( ( storageItem ) => {
const [ key, value ] = storageItem;
const objectValue = JSON.parse( value );
return {
name: key.replace( EXPERIMENT_NAME_PREFIX, '' ),
variation: objectValue.variationName || 'control',
};
} );
}
export function* getExperiments() {
try {
const response = yield apiFetch( {
path: `${ API_NAMESPACE }/options?search=_transient_abtest_variation_`,
} );
const experimentsFromBackend = response.map( ( experiment ) => {
return {
name: experiment.option_name.replace(
TRANSIENT_NAME_PREFIX,
''
),
variation:
experiment.option_value === 'control'
? 'control'
: 'treatment',
};
} );
// 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

@ -0,0 +1,3 @@
export function getExperiments( state ) {
return state.experiments;
}

View File

@ -0,0 +1,110 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Button } from '@wordpress/components';
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
/**
* Internal dependencies
*/
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>
{ isTrackingEnabled === 'no' && (
<p className="tracking-disabled">
The following list might not be complete without tracking
enabled. <br />
Please visit&nbsp;
<a
target="_blank"
href={
wcSettings.adminUrl +
'/admin.php?page=wc-settings&tab=advanced&section=woocommerce_com'
}
rel="noreferrer"
>
WooCommerce &#8594; Settings &#8594; Advanced &#8594;
Woocommerce.com
</a>
&nbsp;and check{ ' ' }
<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>Actions</th>
</tr>
</thead>
<tbody>
{ 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>
);
}
export default compose(
withSelect( ( select ) => {
const { getExperiments } = select( STORE_KEY );
const { getOption, isResolving } = select( OPTIONS_STORE_NAME );
return {
experiments: getExperiments(),
isTrackingEnabled: getOption( 'woocommerce_allow_tracking' ),
isResolving: isResolving( 'getOption', [ 'getExperiments' ] ),
};
} ),
withDispatch( ( dispatch ) => {
const { toggleExperiment, deleteExperiment } = dispatch( STORE_KEY );
return {
toggleExperiment,
deleteExperiment,
};
} )
)( Experiments );

View File

@ -0,0 +1,7 @@
const TYPES = {
TOGGLE_FEATURE: 'TOGGLE_FEATURE',
SET_FEATURES: 'SET_FEATURES',
SET_MODIFIED_FEATURES: 'SET_MODIFIED_FEATURES',
};
export default TYPES;

View File

@ -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,
};
}

View File

@ -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';

View File

@ -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,
} );

View File

@ -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;

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
export function getFeatures( state ) {
return state.features;
}
export function getModifiedFeatures( state ) {
return state.modifiedFeatures;
}

View File

@ -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 (
<div id="wc-admin-test-helper-features">
<h2>
Features
<Button
disabled={ modifiedFeatures.length === 0 }
onClick={ () => resetModifiedFeatures() }
isSecondary
style={ { marginLeft: '24px' } }
>
Reset to defaults
</Button>
</h2>
<table className="features wp-list-table striped table-view-list widefat">
<thead>
<tr>
<th>Feature Name</th>
<th>Enabled?</th>
<th>Toggle</th>
</tr>
</thead>
<tbody>
{ Object.keys( features ).map( ( feature_name ) => {
return (
<tr key={ feature_name }>
<td className="feature-name">
{ feature_name }
</td>
<td>{ features[ feature_name ].toString() }</td>
<td>
<Button
onClick={ () => {
toggleFeature( feature_name );
} }
isPrimary
>
Toggle
</Button>
</td>
</tr>
);
} ) }
</tbody>
</table>
</div>
);
}
export default Features;

View File

@ -0,0 +1,18 @@
/**
* External dependencies
*/
import { render } from '@wordpress/element';
/**
* Internal dependencies
*/
import { App } from './app';
import './index.scss';
const appRoot = document.getElementById(
'woocommerce-admin-test-helper-app-root'
);
if ( appRoot ) {
render( <App />, appRoot );
}

View File

@ -0,0 +1,155 @@
#woocommerce-admin-test-helper-app-root {
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.btn-primary {
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
}
.woocommerce-admin-test-helper__main-tab-panel {
.active-tab {
box-shadow: inset 0 1.5px #007cba;
box-shadow: inset 0 1.5px var( --wp-admin-theme-color );
}
}
.woocommerce-admin-test-helper__action-status {
color: #007cba;
color: var( --wp-admin-theme-color );
font-family: monospace;
}
.woocommerce-admin-test-helper__add-notes {
width: 410px;
display: flex;
justify-content: space-between;
.components-base-control__field {
margin-bottom: 0;
padding-top: 3px;
}
}
#wc-admin-test-helper-options {
div.search-box {
float: right;
margin-bottom: 10px;
}
.align-center {
text-align: center;
}
.components-notice {
margin: 0px 0px 10px 0px;
}
}
.wca-test-helper-option-editor {
width: 100%;
height: 300px;
}
.wca-test-helper-edit-btn-save {
float: right;
}
#wc-admin-test-helper-tools,
#wc-admin-test-helper-experiments {
table.tools,
table.experiments {
thead th {
text-align: center;
}
tbody td {
vertical-align: middle;
&.command {
white-space: nowrap;
}
.trigger-cron-job {
width: 40%;
padding-top: 4px;
.components-base-control__field {
margin-bottom: 0;
}
}
}
}
.components-notice {
margin: 0px 0px 10px 0px;
}
.tracking-disabled {
border: 1px solid #cc99c2;
border-left: 4px solid #cc99c2;
line-height: 1.5em;
background: #fff;
padding: 10px;
}
}
#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;
}
}
form.rest-api-filter-new-form {
.grid {
display: grid;
grid-template-columns: max-content max-content;
grid-gap: 5px;
input[type='text'] {
width: 350px;
}
label {
text-align: right;
}
label:after {
content: ':';
}
}
.btn-new {
color: #fff;
background-color: #007bff;
border-color: #007bff;
float: right;
margin-top: 10px;
}
}

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { useEffect, useState } from '@wordpress/element';
import PropTypes from 'prop-types';
import { Button } from '@wordpress/components';
const OptionEditor = ( props ) => {
const [ value, setValue ] = useState( props.option.content );
useEffect( () => {
setValue( props.option.content );
}, [ props.option ] );
const handleChange = ( event ) => {
setValue( event.target.value );
};
const handleSave = () => {
props.onSave( props.option.name, value );
};
return (
<>
<textarea
className="wca-test-helper-option-editor"
value={ value }
onChange={ handleChange }
></textarea>
<Button
className="wca-test-helper-edit-btn-save"
isPrimary
onClick={ handleSave }
disabled={ props.option.isSaving === true }
>
{ props.option.isSaving ? 'Saving...' : 'Save' }
</Button>
<div className="clear"></div>
</>
);
};
OptionEditor.propTypes = {
option: PropTypes.object.isRequired,
onSave: PropTypes.func.isRequired,
};
export default OptionEditor;

View File

@ -0,0 +1,9 @@
const TYPES = {
SET_OPTIONS: 'SET_OPTIONS',
SET_OPTION_FOR_EDITING: 'SET_OPTION_FOR_EDITING',
SET_IS_LOADING: 'SET_IS_LOADING',
SET_NOTICE: 'SET_NOTICE',
DELETE_OPTION: 'DELETE_OPTION',
};
export default TYPES;

View File

@ -0,0 +1,81 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { API_NAMESPACE } from './constants';
/**
* Initialize the state
*
* @param {Array} options
*/
export function setOptions( options ) {
return {
type: TYPES.SET_OPTIONS,
options,
};
}
export function setLoadingState( isLoading ) {
return {
type: TYPES.SET_IS_LOADING,
isLoading,
};
}
export function setOptionForEditing( editingOption ) {
return {
type: TYPES.SET_OPTION_FOR_EDITING,
editingOption,
};
}
export function setNotice( notice ) {
return {
type: TYPES.SET_NOTICE,
notice,
};
}
export function* deleteOption( optionName ) {
try {
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionName }`,
} );
yield {
type: TYPES.DELETE_OPTION,
optionName,
};
} catch {
throw new Error();
}
}
export function* saveOption( optionName, newOptionValue ) {
try {
const payload = {};
payload[ optionName ] = JSON.parse( newOptionValue );
yield apiFetch( {
method: 'POST',
path: '/wc-admin/options',
headers: { 'content-type': 'application/json' },
body: JSON.stringify( payload ),
} );
yield setNotice( {
status: 'success',
message: optionName + ' has been saved.',
} );
} catch {
yield setNotice( {
status: 'error',
message: 'Unable to save ' + optionName,
} );
throw new Error();
}
}

View File

@ -0,0 +1,2 @@
export const STORE_KEY = 'wc-admin-helper/options';
export const API_NAMESPACE = '/wc-admin-test-helper';

View File

@ -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,
} );

View File

@ -0,0 +1,60 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
options: [],
isLoading: true,
editingOption: {
name: null,
content: '{}',
},
notice: {
status: 'success',
message: '',
},
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.SET_OPTION_FOR_EDITING:
return {
...state,
editingOption: {
...state.editingOption,
...action.editingOption,
},
};
case TYPES.SET_IS_LOADING:
return {
...state,
isLoading: action.isLoading,
};
case TYPES.SET_OPTIONS:
return {
...state,
options: action.options,
isLoading: false,
};
case TYPES.SET_NOTICE:
return {
...state,
notice: {
...state.notice,
...action.notice,
},
};
case TYPES.DELETE_OPTION:
return {
...state,
options: state.options.filter(
( item ) => item.option_name !== action.optionName
),
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,61 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { API_NAMESPACE } from './constants';
import { setLoadingState, setOptions, setOptionForEditing } from './actions';
export function* getOptions( search ) {
let path = `${ API_NAMESPACE }/options?`;
if ( search ) {
path += `search=${ search }`;
}
yield setLoadingState( true );
try {
const response = yield apiFetch( {
path,
} );
yield setOptions( response );
} catch ( error ) {
throw new Error();
}
}
export function* getOptionForEditing( optionName ) {
const loadingOption = {
name: 'Loading...',
content: '',
saved: false,
};
if ( optionName === undefined ) {
return setOptionForEditing( loadingOption );
}
yield setOptionForEditing( loadingOption );
const path = '/wc-admin/options?options=' + optionName;
try {
const response = yield apiFetch( {
path,
} );
let content = response[ optionName ];
if ( typeof content === 'object' ) {
content = JSON.stringify( response[ optionName ], null, 2 );
}
yield setOptionForEditing( {
name: optionName,
content,
} );
} catch ( error ) {
throw new Error( error );
}
}

View File

@ -0,0 +1,15 @@
export function getOptions( state ) {
return state.options;
}
export function isLoading( state ) {
return state.isLoading;
}
export function getOptionForEditing( state ) {
return state.editingOption;
}
export function getNotice( state ) {
return state.notice;
}

View File

@ -0,0 +1,257 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Modal, Notice } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import { default as OptionEditor } from './OptionEditor';
import './data';
function shorten( input ) {
if ( input.length > 20 ) {
return input.substring( 0, 20 ) + '...';
}
return input;
}
function Options( {
options,
getOptions,
deleteOption,
isLoading,
invalidateResolution,
getOptionForEditing,
editingOption,
saveOption,
notice,
setNotice,
} ) {
const [ isEditModalOpen, setEditModalOpen ] = useState( false );
const deleteOptionByName = ( optionName ) => {
// eslint-disable-next-line no-alert
if ( confirm( 'Are you sure you want to delete this option?' ) ) {
deleteOption( optionName );
}
};
const openEditModal = ( optionName ) => {
invalidateResolution( STORE_KEY, 'getOptionForEditing', [
optionName,
] );
getOptionForEditing( optionName );
setEditModalOpen( true );
};
const handleSaveOption = ( optionName, newValue ) => {
saveOption( optionName, newValue );
setEditModalOpen( false );
};
const renderLoading = () => {
return (
<tr>
<td colSpan="6" align="center">
Loading...
</td>
</tr>
);
};
const renderTableData = () => {
if ( options.length === 0 ) {
return (
<tr>
<td colSpan="6" align="center">
No Options Found
</td>
</tr>
);
}
return options.map( ( option ) => {
// eslint-disable-next-line camelcase
const { option_id, option_name, option_value, autoload } = option;
// eslint-disable-next-line camelcase
const optionId = option_id;
// eslint-disable-next-line camelcase
const optionName = option_name;
// eslint-disable-next-line camelcase
const optionValue = shorten( option_value );
return (
<tr key={ optionId }>
<td key={ 0 }>{ optionId }</td>
<td key={ 1 }>{ optionName }</td>
<td key={ 'optionValue' }>{ optionValue }</td>
<td className="align-center" key={ 2 }>
{ autoload }
</td>
<td className="align-center" key={ 3 }>
<button
className="button btn-danger"
onClick={ () => deleteOptionByName( optionName ) }
>
Delete
</button>
</td>
<td className="align-center" key={ 4 }>
<button
className="button btn-primary"
onClick={ () => openEditModal( optionName ) }
>
Edit
</button>
</td>
</tr>
);
} );
};
const searchOption = ( event ) => {
event.preventDefault();
const keyword = event.target.search.value;
// Invalidate resolution of the same selector + arg
// so that entering the same keyword always works
invalidateResolution( STORE_KEY, 'getOptions', [ keyword ] );
getOptions( keyword );
};
return (
<>
{ isEditModalOpen && (
<Modal
title={ editingOption.name }
onRequestClose={ () => {
setEditModalOpen( false );
} }
>
<OptionEditor
option={ editingOption }
onSave={ handleSaveOption }
></OptionEditor>
</Modal>
) }
<div id="wc-admin-test-helper-options">
{ notice.message.length > 0 && (
<Notice
status={ notice.status }
onRemove={ () => {
setNotice( { message: '' } );
} }
>
{ notice.message }
</Notice>
) }
<form onSubmit={ searchOption }>
<div className="search-box">
<label
className="screen-reader-text"
htmlFor="post-search-input"
>
Search options:
</label>
<input type="search" name="search" />
<input
type="submit"
id="search-submit"
className="button"
value="Search options"
/>
</div>
</form>
<div className="clear"></div>
<table className="wp-list-table striped table-view-list widefat">
<thead>
<tr>
<td
className="manage-column column-thumb"
key={ 0 }
>
I.D
</td>
<td
className="manage-column column-thumb"
key={ 1 }
>
Name
</td>
<td
className="manage-column column-thumb"
key={ 'optionValue' }
>
Value
</td>
<td
className="manage-column column-thumb align-center"
key={ 2 }
>
Autoload
</td>
<td
className="manage-column column-thumb align-center"
key={ 3 }
>
Delete
</td>
<td
className="manage-column column-thumb align-center"
key={ 4 }
>
Edit
</td>
</tr>
</thead>
<tbody>
{ isLoading ? renderLoading() : renderTableData() }
</tbody>
</table>
</div>
</>
);
}
export default compose(
withSelect( ( select ) => {
const {
getOptions,
getOptionForEditing,
getNotice,
isLoading,
} = select( STORE_KEY );
const options = getOptions();
const editingOption = getOptionForEditing();
const notice = getNotice();
return {
options,
getOptions,
isLoading: isLoading(),
editingOption,
getOptionForEditing,
notice,
};
} ),
withDispatch( ( dispatch ) => {
const { deleteOption, saveOption, setNotice } = dispatch( STORE_KEY );
const { invalidateResolution } = dispatch( 'core/data' );
return {
deleteOption,
invalidateResolution,
saveOption,
setNotice,
};
} )
)( Options );

View File

@ -0,0 +1,9 @@
const TYPES = {
SET_FILTERS: 'SET_FILTERS',
SET_IS_LOADING: 'SET_IS_LOADING',
DELETE_FILTER: 'DELETE_FILTER',
SAVE_FILTER: 'SAVE_FILTER',
TOGGLE_FILTER: 'TOGGLE_FILTER',
};
export default TYPES;

View File

@ -0,0 +1,92 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { API_NAMESPACE } from './constants';
/**
* Initialize the state
*
* @param {Array} filters
*/
export function setFilters( filters ) {
return {
type: TYPES.SET_FILTERS,
filters,
};
}
export function setLoadingState( isLoading ) {
return {
type: TYPES.SET_IS_LOADING,
isLoading,
};
}
export function* toggleFilter( index ) {
try {
yield apiFetch( {
method: 'POST',
path: `${ API_NAMESPACE }/rest-api-filters/${ index }/toggle`,
headers: { 'content-type': 'application/json' },
} );
yield {
type: TYPES.TOGGLE_FILTER,
index,
};
} catch {
throw new Error();
}
}
export function* deleteFilter( index ) {
try {
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/rest-api-filters/`,
headers: { 'content-type': 'application/json' },
body: JSON.stringify( {
index,
} ),
} );
yield {
type: TYPES.DELETE_FILTER,
index,
};
} catch {
throw new Error();
}
}
export function* saveFilter( endpoint, dotNotation, replacement ) {
try {
yield apiFetch( {
method: 'POST',
path: API_NAMESPACE + '/rest-api-filters',
headers: { 'content-type': 'application/json' },
body: JSON.stringify( {
endpoint,
dot_notation: dotNotation,
replacement,
} ),
} );
yield {
type: TYPES.SAVE_FILTER,
filter: {
endpoint,
dot_notation: dotNotation,
replacement,
enabled: true,
},
};
} catch {
throw new Error();
}
}

View File

@ -0,0 +1,5 @@
export const STORE_KEY = 'wc-admin-helper/rest-api-filters';
export const API_NAMESPACE = '/wc-admin-test-helper';
// Option name where we're going to save the filters.
export const FILTERS_OPTION_NAME = 'wc-admin-test-helper-rest-api-filters';

View File

@ -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,
} );

View File

@ -0,0 +1,55 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
filters: [],
isLoading: true,
notice: {
status: 'success',
message: '',
},
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.TOGGLE_FILTER:
return {
...state,
filters: state.filters.map( ( filter, index ) => {
if ( index === action.index ) {
filter.enabled = ! filter.enabled;
}
return filter;
} ),
};
case TYPES.SET_IS_LOADING:
return {
...state,
isLoading: action.isLoading,
};
case TYPES.SET_FILTERS:
return {
...state,
filters: action.filters,
isLoading: false,
};
case TYPES.DELETE_FILTER:
return {
...state,
filters: state.filters.filter(
( item, index ) => index !== action.index
),
};
case TYPES.SAVE_FILTER:
return {
...state,
filters: [ ...state.filters, action.filter ],
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,29 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { FILTERS_OPTION_NAME } from './constants';
import { setLoadingState, setFilters } from './actions';
export function* getFilters() {
const path = '/wc-admin/options?options=' + FILTERS_OPTION_NAME;
yield setLoadingState( true );
try {
const response = yield apiFetch( {
path,
} );
if ( response[ FILTERS_OPTION_NAME ] === false ) {
yield setFilters( [] );
} else {
yield setFilters( response[ FILTERS_OPTION_NAME ] );
}
} catch ( error ) {
throw new Error();
}
}

View File

@ -0,0 +1,7 @@
export function getFilters( state ) {
return state.filters;
}
export function isLoading( state ) {
return state.isLoading;
}

View File

@ -0,0 +1,182 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
import { Modal } from '@wordpress/components';
import { useState } from '@wordpress/element';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function RestAPIFilters( {
filters,
deleteFilter,
isLoading,
saveFilter,
toggleFilter,
} ) {
const [ isNewModalOpen, setNewModalOpen ] = useState( false );
const submitAddForm = ( e ) => {
e.preventDefault();
saveFilter(
e.target.endpoint.value,
e.target.dotNotation.value,
e.target.replacement.value
);
setNewModalOpen( false );
};
const renderLoading = () => {
return (
<tr>
<td colSpan="6" align="center">
Loading...
</td>
</tr>
);
};
const renderTableData = () => {
if ( filters.length === 0 ) {
return (
<tr>
<td colSpan="7" align="center">
No Filters Found
</td>
</tr>
);
}
return filters.map( ( filter, index ) => {
// eslint-disable-next-line camelcase
const {
endpoint,
dot_notation: dotNotation,
replacement,
enabled,
} = filter;
return (
<tr key={ index }>
<td>{ index + 1 }</td>
<td>{ endpoint }</td>
<td key={ 'optionValue' }>{ dotNotation }</td>
<td className="align-center">{ replacement + '' }</td>
<td className="align-center">{ enabled + '' }</td>
<td className="align-center">
<button
className="button btn-primary"
onClick={ () => toggleFilter( index ) }
>
Toggle
</button>
</td>
<td className="align-center">
<button
className="button btn-danger"
onClick={ () => deleteFilter( index ) }
>
Delete
</button>
</td>
</tr>
);
} );
};
return (
<>
{ isNewModalOpen && (
<Modal
title={ 'New Filter' }
onRequestClose={ () => {
setNewModalOpen( false );
} }
>
<form
className="rest-api-filter-new-form"
onSubmit={ submitAddForm }
>
<div className="grid">
<label htmlFor="endpoint">Endpoint</label>
<input type="text" name="endpoint" />
<label htmlFor="jsonPath">Dot Notation</label>
<input type="text" name="dotNotation" />
<label htmlFor="replacement">Replacement </label>
<input type="text" name="replacement" />
</div>
<input
type="submit"
value="Create New Filter"
className="button btn-new"
/>
</form>
</Modal>
) }
<div id="wc-admin-test-helper-rest-api-filters">
<input
type="button"
className="button btn-primary btn-new"
value="New Filter"
onClick={ () => setNewModalOpen( true ) }
/>
<br />
<br />
<table className="wp-list-table striped table-view-list widefat">
<thead>
<tr>
<td className="manage-column column-thumb">I.D</td>
<td className="manage-column column-thumb">
Endpoint
</td>
<td className="manage-column column-thumb">
Dot Notation
</td>
<td className="manage-column column-thumb align-center">
Replacement
</td>
<td className="manage-column column-thumb align-center">
Enabled
</td>
<td className="manage-column column-thumb align-center">
Toggle
</td>
<td className="manage-column column-thumb align-center"></td>
</tr>
</thead>
<tbody>
{ isLoading ? renderLoading() : renderTableData() }
</tbody>
</table>
</div>
</>
);
}
export default compose(
withSelect( ( select ) => {
const { getFilters, isLoading } = select( STORE_KEY );
const filters = getFilters();
return {
filters,
isLoading: isLoading(),
};
} ),
withDispatch( ( dispatch ) => {
const { saveFilter, deleteFilter, toggleFilter } = dispatch(
STORE_KEY
);
return {
saveFilter,
deleteFilter,
toggleFilter,
};
} )
)( RestAPIFilters );

View File

@ -0,0 +1,17 @@
# Tools
## Adding a New Command
1. Open `commands.js` and add a new object with command, description, and action keys. Action value must be a valid function name.
2. Open `data/actions.js` and add a function. The function name must be the value of `Action` from the first step.
Sample function:
```
export function* helloWorld() {
yield runCommand( 'Hello World', function* () {
console.log('Hello World');
} );
}
```
3. Run `npm start` to compile and test.

View File

@ -0,0 +1,33 @@
/**
* External dependencies
*/
import { useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { STORE_KEY } from '../data/constants';
export const DisableEmail = () => {
const { isEmailDisabled } = useSelect( ( select ) => {
const { getIsEmailDisabled } = select( STORE_KEY );
return {
isEmailDisabled: getIsEmailDisabled(),
};
} );
const getEmailStatus = () => {
switch ( isEmailDisabled ) {
case 'yes':
return 'WooCommerce emails are turned off 🔴';
case 'no':
return 'WooCommerce emails are turned on 🟢';
case 'error':
return 'Error 🙁';
default:
return 'Loading ...';
}
};
return <div className="disable-wc-email">{ getEmailStatus() }</div>;
};

View File

@ -0,0 +1,67 @@
/**
* Internal dependencies
*/
import { TriggerCronJob, TRIGGER_CRON_ACTION_NAME } from './trigger-cron';
import { DisableEmail } from './disable-email';
import {
TriggerUpdateCallbacks,
TRIGGER_UPDATE_CALLBACKS_ACTION_NAME,
} from './trigger-update-callbacks';
export default [
{
command: 'Trigger WCA Install',
description: `This will trigger a WooCommerce Admin install, which usually
happens when a new version (or new install) of WooCommerce
Admin is installed. Triggering the install manually can
run tasks such as removing obsolete admin notes.`,
action: 'triggerWcaInstall',
},
{
command: 'Reset Onboarding Wizard',
description: 'Resets Onboarding Wizard progress.',
action: 'resetOnboardingWizard',
},
{
command: 'Reset Jetpack Connection',
description: 'Resets Jepack Connection options.',
action: 'resetJetpackConnection',
},
{
command: 'Enable wc-admin Tracking',
description:
'Enable Tracking Debug mode. You should change your console level to verbose.',
action: 'enableTrackingDebug',
},
{
command: 'Update WC installation timestamp',
description:
'Updates woocommerce_admin_install_timestamp to a certain date',
action: 'updateStoreAge',
},
{
command: 'Run wc_admin_daily job',
description: 'Run wc_admin_daily job',
action: 'runWcAdminDailyJob',
},
{
command: 'Delete all products',
description: 'Delete all products',
action: 'deleteAllProducts',
},
{
command: 'Run a cron job',
description: <TriggerCronJob />,
action: TRIGGER_CRON_ACTION_NAME,
},
{
command: 'Disable WC emails',
description: <DisableEmail />,
action: 'runDisableEmail',
},
{
command: 'Run version update callbacks',
description: <TriggerUpdateCallbacks />,
action: TRIGGER_UPDATE_CALLBACKS_ACTION_NAME,
},
];

View File

@ -0,0 +1,48 @@
/**
* External dependencies
*/
import { SelectControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { STORE_KEY } from '../data/constants';
export const TRIGGER_CRON_ACTION_NAME = 'runSelectedCronJob';
export const TriggerCronJob = () => {
const { cronList } = useSelect( ( select ) => {
const { getCronJobs } = select( STORE_KEY );
return {
cronList: getCronJobs(),
};
} );
const { updateCommandParams } = useDispatch( STORE_KEY );
function onCronChange( selectedValue ) {
const { hook, signature } = cronList[ selectedValue ];
updateCommandParams( TRIGGER_CRON_ACTION_NAME, { hook, signature } );
}
function getOptions() {
return Object.keys( cronList ).map( ( name ) => {
return { label: name, value: name };
} );
}
return (
<div className="trigger-cron-job">
{ ! cronList ? (
<p>Loading ...</p>
) : (
<SelectControl
label="Select cron job to run"
onChange={ onCronChange }
labelPosition="side"
options={ getOptions() }
/>
) }
</div>
);
};

View File

@ -0,0 +1,51 @@
/**
* External dependencies
*/
import { SelectControl } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
/**
* Internal dependencies
*/
import { STORE_KEY } from '../data/constants';
export const TRIGGER_UPDATE_CALLBACKS_ACTION_NAME =
'runSelectedUpdateCallbacks';
export const TriggerUpdateCallbacks = () => {
const { dbUpdateVersions } = useSelect( ( select ) => {
const { getDBUpdateVersions } = select( STORE_KEY );
return {
dbUpdateVersions: getDBUpdateVersions(),
};
} );
const { updateCommandParams } = useDispatch( STORE_KEY );
function onCronChange( version ) {
updateCommandParams( TRIGGER_UPDATE_CALLBACKS_ACTION_NAME, {
version,
} );
}
function getOptions() {
return dbUpdateVersions.map( ( version ) => {
return { label: version, value: version };
} );
}
return (
<div className="trigger-cron-job">
{ ! dbUpdateVersions ? (
<p>Loading ...</p>
) : (
<SelectControl
label="Select a version to run"
onChange={ onCronChange }
labelPosition="side"
options={ getOptions().reverse() }
/>
) }
</div>
);
};

View File

@ -0,0 +1,13 @@
const TYPES = {
ADD_CURRENTLY_RUNNING: 'ADD_CURRENTLY_RUNNING',
REMOVE_CURRENTLY_RUNNING: 'REMOVE_CURRENTLY_RUNNING',
ADD_MESSAGE: 'ADD_MESSAGE',
UPDATE_MESSAGE: 'UPDATE_MESSAGE',
REMOVE_MESSAGE: 'REMOVE_MESSAGE',
ADD_COMMAND_PARAMS: 'ADD_COMMAND_PARAMS',
SET_CRON_JOBS: 'SET_CRON_JOBS',
IS_EMAIL_DISABLED: 'IS_EMAIL_DISABLED',
SET_DB_UPDATE_VERSIONS: 'SET_DB_UPDATE_VERSIONS',
};
export default TYPES;

View File

@ -0,0 +1,212 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { API_NAMESPACE } from './constants';
export function addCurrentlyRunning( command ) {
return {
type: TYPES.ADD_CURRENTLY_RUNNING,
command,
};
}
export function removeCurrentlyRunning( command ) {
return {
type: TYPES.REMOVE_CURRENTLY_RUNNING,
command,
};
}
export function addMessage( source, message ) {
return {
type: TYPES.ADD_MESSAGE,
source,
message,
};
}
export function updateMessage( source, message, status ) {
return {
type: TYPES.ADD_MESSAGE,
source,
message,
status,
};
}
export function removeMessage( source ) {
return {
type: TYPES.REMOVE_MESSAGE,
source,
};
}
export function updateCommandParams( source, params ) {
return {
type: TYPES.ADD_COMMAND_PARAMS,
source,
params,
};
}
export function setCronJobs( cronJobs ) {
return {
type: TYPES.SET_CRON_JOBS,
cronJobs,
};
}
export function setDBUpdateVersions( versions ) {
return {
type: TYPES.SET_DB_UPDATE_VERSIONS,
versions,
};
}
export function setIsEmailDisabled( isEmailDisabled ) {
return {
type: TYPES.IS_EMAIL_DISABLED,
isEmailDisabled,
};
}
function* runCommand( commandName, func ) {
try {
yield addCurrentlyRunning( commandName );
yield addMessage( commandName, 'Executing...' );
yield func();
yield removeCurrentlyRunning( commandName );
yield updateMessage( commandName, 'Successful!' );
} catch ( e ) {
yield updateMessage( commandName, e.message, 'error' );
yield removeCurrentlyRunning( commandName );
}
}
export function* triggerWcaInstall() {
yield runCommand( 'Trigger WCA Install', function* () {
yield apiFetch( {
path: API_NAMESPACE + '/tools/trigger-wca-install/v1',
method: 'POST',
} );
} );
}
export function* resetOnboardingWizard() {
yield runCommand( 'Reset Onboarding Wizard', function* () {
const optionsToDelete = [
'woocommerce_task_list_tracked_completed_tasks',
'woocommerce_onboarding_profile',
'_transient_wc_onboarding_themes',
];
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionsToDelete.join( ',' ) }`,
} );
} );
}
export function* resetJetpackConnection() {
yield runCommand( 'Reset Jetpack Connection', function* () {
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/jetpack_options`,
} );
} );
}
export function* enableTrackingDebug() {
yield runCommand( 'Enable WC Admin Tracking Debug Mode', function* () {
window.localStorage.setItem( 'debug', 'wc-admin:*' );
} );
}
export function* updateStoreAge() {
yield runCommand( 'Update Installation timestamp', function* () {
const today = new Date();
const dd = String( today.getDate() ).padStart( 2, '0' );
const mm = String( today.getMonth() + 1 ).padStart( 2, '0' ); //January is 0!
const yyyy = today.getFullYear();
// eslint-disable-next-line no-alert
const numberOfDays = window.prompt(
'Please enter a date in yyyy/mm/dd format',
yyyy + '/' + mm + '/' + dd
);
if ( numberOfDays !== null ) {
const dates = numberOfDays.split( '/' );
const newTimestamp = Math.round(
new Date( dates[ 0 ], dates[ 1 ] - 1, dates[ 2 ] ).getTime() /
1000
);
const payload = {
woocommerce_admin_install_timestamp: JSON.parse( newTimestamp ),
};
yield apiFetch( {
method: 'POST',
path: '/wc-admin/options',
headers: { 'content-type': 'application/json' },
body: JSON.stringify( payload ),
} );
}
} );
}
export function* runWcAdminDailyJob() {
yield runCommand( 'Run wc_admin_daily job', function* () {
yield apiFetch( {
path: API_NAMESPACE + '/tools/run-wc-admin-daily/v1',
method: 'POST',
} );
} );
}
export function* deleteAllProducts() {
if ( ! confirm( 'Are you sure you want to delete all of the products?' ) ) {
return;
}
yield runCommand( 'Delete all products', function* () {
yield apiFetch( {
path: `${ API_NAMESPACE }/tools/delete-all-products/v1`,
method: 'POST',
} );
} );
}
export function* runSelectedCronJob( params ) {
yield runCommand( 'Run selected cron job', function* () {
yield apiFetch( {
path: API_NAMESPACE + '/tools/run-wc-admin-daily/v1',
method: 'POST',
data: params,
} );
} );
}
export function* runSelectedUpdateCallbacks( params ) {
yield runCommand( 'Run version update callbacks', function* () {
yield apiFetch( {
path: API_NAMESPACE + '/tools/trigger-selected-update-callbacks/v1',
method: 'POST',
data: params,
} );
} );
}
export function* runDisableEmail() {
yield runCommand( 'Disable/Enable WooCommerce emails', function* () {
const response = yield apiFetch( {
path: `${ API_NAMESPACE }/tools/toggle-emails/v1`,
method: 'POST',
} );
yield setIsEmailDisabled( response );
} );
}

View File

@ -0,0 +1,2 @@
export const STORE_KEY = 'wc-admin-helper/tools';
export const API_NAMESPACE = '/wc-admin-test-helper';

View File

@ -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,
} );

View File

@ -0,0 +1,88 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
currentlyRunning: {},
errorMessages: [],
cronJobs: false,
isEmailDisabled: '',
messages: {},
params: [],
status: '',
dbUpdateVersions: [],
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.ADD_MESSAGE:
if ( ! action.status ) {
action.status = 'info';
}
return {
...state,
messages: {
...state.messages,
[ action.source ]: {
message: action.message,
status: action.status,
},
},
};
case TYPES.REMOVE_MESSAGE:
const messages = { ...state.messages };
delete messages[ action.source ];
return {
...state,
messages,
};
case TYPES.SET_STATUS:
return {
...state,
status: action.status,
};
case TYPES.ADD_CURRENTLY_RUNNING:
return {
...state,
currentlyRunning: {
...state,
[ action.command ]: true,
},
};
case TYPES.REMOVE_CURRENTLY_RUNNING:
return {
...state,
currentlyRunning: {
...state,
[ action.command ]: false,
},
};
case TYPES.SET_CRON_JOBS:
return {
...state,
cronJobs: action.cronJobs,
};
case TYPES.IS_EMAIL_DISABLED:
return {
...state,
isEmailDisabled: action.isEmailDisabled,
};
case TYPES.ADD_COMMAND_PARAMS:
return {
...state,
params: {
[ action.source ]: action.params,
},
};
case TYPES.SET_DB_UPDATE_VERSIONS:
return {
...state,
dbUpdateVersions: action.versions,
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,57 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { API_NAMESPACE } from './constants';
import {
setCronJobs,
setDBUpdateVersions,
setIsEmailDisabled,
} from './actions';
export function* getCronJobs() {
const path = `${ API_NAMESPACE }/tools/get-cron-list/v1`;
try {
const response = yield apiFetch( {
path,
method: 'GET',
} );
yield setCronJobs( response );
} catch ( error ) {
throw new Error( error );
}
}
export function* getDBUpdateVersions() {
const path = `${ API_NAMESPACE }/tools/get-update-versions/v1`;
try {
const response = yield apiFetch( {
path,
method: 'GET',
} );
yield setDBUpdateVersions( response );
} catch ( error ) {
throw new Error( error );
}
}
export function* getIsEmailDisabled() {
const path = `${ API_NAMESPACE }/tools/get-email-status/v1`;
try {
const response = yield apiFetch( {
path,
method: 'GET',
} );
yield setIsEmailDisabled( response );
} catch ( error ) {
yield setIsEmailDisabled( 'error' );
throw new Error( error );
}
}

Some files were not shown because too many files have changed in this diff Show More