Merge pull request #37 from woocommerce/add/rest-api-filter

Add REST API Filters to modify API responses
This commit is contained in:
Moon 2022-04-18 21:59:57 -07:00 committed by GitHub
commit 27fea8b434
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 678 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,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

@ -12,6 +12,7 @@ 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', [
{
@ -39,6 +40,11 @@ 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() {

View File

@ -1,3 +1,17 @@
#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;
@ -27,12 +41,6 @@
margin-bottom: 10px;
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
.align-center {
text-align: center;
}
@ -51,8 +59,10 @@
float: right;
}
#wc-admin-test-helper-tools, #wc-admin-test-helper-experiments {
table.tools, table.experiments {
#wc-admin-test-helper-tools,
#wc-admin-test-helper-experiments {
table.tools,
table.experiments {
thead th {
text-align: center;
}
@ -74,3 +84,34 @@
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,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" 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">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 );