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
woocommerce-beta-tester.zip
node_modules/
build/
bin/

View File

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

View File

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

View File

@ -20,3 +20,6 @@ node_modules/
# Built assets
build/
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.
*
* @package WooCommerceBetaTester\JS
* @package
*/
jQuery(function( $ ) {
// eslint-disable-next-line no-undef
jQuery( function ( $ ) {
/**
* Version information
*/
var wc_beta_tester_version_information = {
const wc_beta_tester_version_information = {
/**
* Initialize Version Information click
*/
init: function() {
$( '#wp-admin-bar-show-version-info' )
.on( 'click', this.showModal );
init() {
$( '#wp-admin-bar-show-version-info' ).on(
'click',
this.showModal
);
},
/**
* Handler for showing/hiding version information modal
*
* @param {Event} event
*/
showModal: function( event ) {
showModal( event ) {
event.preventDefault();
// Prevent multiple modals.
if ( 0 < $( '.wc-backbone-modal-beta-tester-version-info' ).length ) {
if (
$( '.wc-backbone-modal-beta-tester-version-info' ).length > 0
) {
return;
}
$( this ).WCBackboneModal({
$( this ).WCBackboneModal( {
template: 'wc-beta-tester-version-info',
variable: {
// eslint-disable-next-line no-undef
version: wc_beta_tester_version_info_params.version,
// eslint-disable-next-line no-undef
description: wc_beta_tester_version_info_params.description,
},
});
}
} );
},
};
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.
*
* @package WooCommerceBetaTester\JS
* @package
*/
jQuery(function( $ ) {
// eslint-disable-next-line no-undef
jQuery( function ( $ ) {
/**
* Version picker
*/
var wc_beta_tester_version_picker = {
const wc_beta_tester_version_picker = {
/**
* Initialize Version Information click
*/
init: function() {
instance = this;
init() {
const instance = this;
instance.new_version = undefined;
$( '#wcbt-modal-version-switch-confirm' )
.on( 'click', this.showConfirmVersionSwitchModal );
$( 'input[type=radio][name=wcbt_switch_to_version]' ).change( function() {
if ( $( this ).is( ':checked' ) ) {
instance.new_version = $( this ).val();
}
} ).trigger( 'change' );
$( '#wcbt-modal-version-switch-confirm' ).on(
'click',
this.showConfirmVersionSwitchModal
);
$( 'input[type=radio][name=wcbt_switch_to_version]' )
.change( function () {
if ( $( this ).is( ':checked' ) ) {
instance.new_version = $( this ).val();
}
} )
.trigger( 'change' );
},
/**
* Handler for showing/hiding version switch modal
*
* @param {Event} event
*/
showConfirmVersionSwitchModal: function( event ) {
showConfirmVersionSwitchModal( event ) {
event.preventDefault();
const instance = this;
if ( ! instance.new_version ) {
// eslint-disable-next-line no-undef
alert( wc_beta_tester_version_picker_params.i18n_pick_version );
} else {
$( this ).WCBackboneModal({
$( this ).WCBackboneModal( {
template: 'wcbt-version-switch-confirm',
variable: {
new_version: instance.new_version,
},
});
} );
$( '#wcbt-submit-version-switch' )
.on( 'click', instance.submitSwitchVersionForm );
$( '#wcbt-submit-version-switch' ).on(
'click',
instance.submitSwitchVersionForm
);
}
},
/**
* Submit form to switch version of WooCommerce.
*
* @param {Event} event
*/
submitSwitchVersionForm: function( event ) {
submitSwitchVersionForm( event ) {
event.preventDefault();
$( 'form[name=wcbt-select-version]' ).get( 0 ).submit();
@ -59,5 +71,4 @@ jQuery(function( $ ) {
};
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..."
pnpm install
echo "Running JS Build..."
pnpm run uglify
pnpm run build
echo "Syncing files..."
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"
},
"devDependencies": {
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
"@woocommerce/eslint-plugin": "workspace:*",
"@wordpress/scripts": "^19.2.4",
"eslint": "5.16.0",
"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": {
"js": {
"min": "assets/js/*.min.js",
@ -25,12 +39,25 @@
"scripts": {
"postinstall": "composer install",
"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: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",
"lint": "eslint assets/js --ext=js",
"lint:fix": "eslint assets/js --ext=js --fix"
"check-engines": "wp-scripts check-engines",
"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": {
"node": ">=10.15.0",
@ -45,8 +72,11 @@
"php -d display_errors=1 -l",
"composer --working-dir=./plugins/woocommerce-beta-tester run-script phpcs-pre-commit"
],
"!(*min).js": [
"pnpm lint:fix"
"*.(t|j)s?(x)": [
"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