Add REST API Filters that allows us to modify API responses without modifying code.

This commit is contained in:
moon 2022-04-13 13:42:37 -07:00
parent 24dffe3aac
commit 887cba71cf
13 changed files with 700 additions and 66 deletions

View File

@ -37,3 +37,5 @@ 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,50 @@
<?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' );
if ( $replacement === 'false' ) {
$replacement = false;
} else if ( $replacement === 'true' ) {
$replacement = true;
}
self::update( function( $filters ) use (
$endpoint,
$dot_notation,
$replacement
) {
$filters[] = array(
'endpoint' => $endpoint,
'dot_notation' => $dot_notation,
'replacement' => $replacement,
'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

@ -12,8 +12,9 @@ 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', [
const tabs = applyFilters( 'woocommerce_admin_test_helper_tabs', [
{
name: 'options',
title: 'Options',
@ -39,7 +40,12 @@ const tabs = applyFilters('woocommerce_admin_test_helper_tabs', [
title: 'Features',
content: <Features />,
},
]);
{
name: 'rest-api-filters',
title: 'REST API FIlters',
content: <RestAPIFilters />,
},
] );
export function App() {
return (
@ -48,18 +54,18 @@ export function App() {
<TabPanel
className="woocommerce-admin-test-helper__main-tab-panel"
activeClass="active-tab"
tabs={tabs}
initialTabName={tabs[0].name}
tabs={ tabs }
initialTabName={ tabs[ 0 ].name }
>
{(tab) => (
{ ( tab ) => (
<>
{tab.content}
{applyFilters(
`woocommerce_admin_test_helper_tab_${tab.name}`,
{ tab.content }
{ applyFilters(
`woocommerce_admin_test_helper_tab_${ tab.name }`,
[]
)}
) }
</>
)}
) }
</TabPanel>
</div>
);

View File

@ -1,76 +1,117 @@
#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);
}
.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;
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;
}
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;
}
div.search-box {
float: right;
margin-bottom: 10px;
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.align-center {
text-align: center;
}
.align-center {
text-align: center;
}
.components-notice {
margin: 0px 0px 10px 0px;
}
.components-notice {
margin: 0px 0px 10px 0px;
}
}
.wca-test-helper-option-editor {
width: 100%;
height: 300px;
width: 100%;
height: 300px;
}
.wca-test-helper-edit-btn-save {
float: right;
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;
}
#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;
}
}
#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,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,93 @@
/**
* 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} filter
* @param 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,206 @@
/**
* 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, replacement, enabled } = filter;
// eslint-disable-next-line camelcase
const dotNotation = dot_notation;
return (
<tr key={ index }>
<td key={ 0 }>{ index + 1 }</td>
<td key={ 1 }>{ endpoint }</td>
<td key={ 'optionValue' }>{ dotNotation }</td>
<td className="align-center" key={ 2 }>
{ replacement + '' }
</td>
<td className="align-center" key={ 3 }>
{ enabled + '' }
</td>
<td className="align-center" key={ 4 }>
<button
className="button btn-primary"
onClick={ () => toggleFilter( index ) }
>
Toggle
</button>
</td>
<td className="align-center" key={ 5 }>
<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" autoFocus />
<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"
key={ 0 }
>
I.D
</td>
<td
className="manage-column column-thumb"
key={ 1 }
>
Endpoint
</td>
<td
className="manage-column column-thumb"
key={ 'optionValue' }
>
Dot Notation
</td>
<td
className="manage-column column-thumb align-center"
key={ 2 }
>
Replacement
</td>
<td
className="manage-column column-thumb align-center"
key={ 3 }
>
Enabled
</td>
<td
className="manage-column column-thumb align-center"
key={ 3 }
>
Toggle
</td>
<td
className="manage-column column-thumb align-center"
key={ 4 }
></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 );