Add Options component to CRUD wp_options

This commit is contained in:
Moon 2021-03-02 22:05:45 -08:00
parent e2f20bd546
commit 9c928fc86e
15 changed files with 479 additions and 94 deletions

View File

@ -24,3 +24,4 @@ function register_woocommerce_admin_test_helper_rest_route( $route, $callback )
require( 'admin-notes/delete-all-notes.php' );
require( 'admin-notes/add-note.php' );
require( 'tools/trigger-wca-install.php' );
require( 'options/rest-api.php' );

106
api/options/rest-api.php Normal file
View File

@ -0,0 +1,106 @@
<?php
class Options_Rest_Api extends WP_REST_Controller {
protected $namespace = 'wc-admin-test-helper/v1';
public function register_routes() {
register_rest_route(
$this->namespace,
'/options',
array(
'methods' => 'GET',
'callback' => array( $this, 'get_items' ),
'args' => array(
'per_page' => $this->get_collection_params()['per_page'],
),
'permission_callback' => function( $request ) {
return true;
},
)
);
register_rest_route(
$this->namespace,
'/options/(?P<option_id>\d+)',
array(
'methods' => 'DELETE',
'callback' => array( $this, 'delete_item' ),
'args' => array(
'option_id' => array(
'type' => 'integer',
'sanitize_callback' => 'absint',
),
),
'permission_callback' => function( $request ) {
return true;
},
)
);
}
public function delete_item( $request ) {
global $wpdb;
$option_id = $request->get_param( 'option_id' );
$query = $wpdb->prepare( "delete from {$wpdb->prefix}options where option_id = %d", $option_id );
$wpdb->query( $query );
return new WP_REST_RESPONSE( null, 204 );
}
public function get_items( $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, 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 );
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
return 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',
),
);
}
}
add_action(
'rest_api_init',
function() {
( new Options_Rest_Api() )->register_routes();
}
);

183
package-lock.json generated
View File

@ -2973,6 +2973,17 @@
"use-memo-one": "^1.1.1"
}
},
"@wordpress/data-controls": {
"version": "1.20.7",
"resolved": "https://registry.npmjs.org/@wordpress/data-controls/-/data-controls-1.20.7.tgz",
"integrity": "sha512-81/Cbk7PXAKOf0ButPqSDUdUntu5ltQVhvdfEBbFOwUxPYHiRNlP05mFuGHSqR2yJPcZToW7g1qFj2UyEK66SA==",
"requires": {
"@babel/runtime": "^7.12.5",
"@wordpress/api-fetch": "^3.21.5",
"@wordpress/data": "^4.26.7",
"@wordpress/deprecated": "^2.11.1"
}
},
"@wordpress/date": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@wordpress/date/-/date-3.13.1.tgz",
@ -3417,15 +3428,6 @@
"strip-bom": "^2.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
@ -3436,30 +3438,6 @@
"yallist": "^2.1.2"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@ -3489,59 +3467,6 @@
"pinkie-promise": "^2.0.0"
}
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"requires": {
"find-up": "^4.0.0"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
}
}
},
"prettier": {
"version": "npm:wp-prettier@2.2.1-beta-1",
"resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.2.1-beta-1.tgz",
"integrity": "sha512-+JHkqs9LC/JPp51yy1hzs3lQ7qeuWCwOcSzpQNeeY/G7oSpnF61vxt7hRh87zNRTr6ob2ndy0W8rVzhgrcA+Gw==",
"dev": true
},
"puppeteer": {
"version": "npm:puppeteer-core@5.5.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-5.5.0.tgz",
"integrity": "sha512-tlA+1n+ziW/Db03hVV+bAecDKse8ihFRXYiEypBe9IlLRvOCzYFG6qrCMBYK34HO/Q/Ecjc+tvkHRAfLVH+NgQ==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"devtools-protocol": "0.0.818844",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.6.1",
"pkg-dir": "^4.2.0",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@ -13820,6 +13745,12 @@
"integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=",
"dev": true
},
"prettier": {
"version": "npm:wp-prettier@2.2.1-beta-1",
"resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.2.1-beta-1.tgz",
"integrity": "sha512-+JHkqs9LC/JPp51yy1hzs3lQ7qeuWCwOcSzpQNeeY/G7oSpnF61vxt7hRh87zNRTr6ob2ndy0W8rVzhgrcA+Gw==",
"dev": true
},
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
@ -14011,6 +13942,86 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
},
"puppeteer": {
"version": "npm:puppeteer-core@5.5.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-5.5.0.tgz",
"integrity": "sha512-tlA+1n+ziW/Db03hVV+bAecDKse8ihFRXYiEypBe9IlLRvOCzYFG6qrCMBYK34HO/Q/Ecjc+tvkHRAfLVH+NgQ==",
"dev": true,
"requires": {
"debug": "^4.1.0",
"devtools-protocol": "0.0.818844",
"extract-zip": "^2.0.0",
"https-proxy-agent": "^4.0.0",
"node-fetch": "^2.6.1",
"pkg-dir": "^4.2.0",
"progress": "^2.0.1",
"proxy-from-env": "^1.0.0",
"rimraf": "^3.0.2",
"tar-fs": "^2.0.0",
"unbzip2-stream": "^1.3.3",
"ws": "^7.2.3"
},
"dependencies": {
"find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"dev": true,
"requires": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
}
},
"locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"dev": true,
"requires": {
"p-locate": "^4.1.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"dev": true,
"requires": {
"p-limit": "^2.2.0"
}
},
"p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
"integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
"dev": true,
"requires": {
"find-up": "^4.0.0"
}
}
}
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",

View File

@ -28,7 +28,9 @@
"@woocommerce/data": "^1.1.1",
"@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"
}

View File

@ -1,17 +1,15 @@
/**
* External dependencies.
* External dependencies
*/
import { TabPanel } from '@wordpress/components';
import { applyFilters, addFilter } from '@wordpress/hooks';
import { applyFilters } from '@wordpress/hooks';
/**
* Internal dependencies
*/
// TODO replace this with the actual controls
// import { Options } from '../Options';
const Options = () => <h2>Options</h2>;
import { AdminNotes } from '../admin-notes';
import { Tools } from '../tools';
import { default as Options } from '../options';
const tabs = applyFilters(
'woocommerce_admin_test_helper_tabs',

View File

@ -10,3 +10,17 @@
color: var(--wp-admin-theme-color);
font-family: monospace;
}
#wc-admin-test-helper-options {
div.search-box {
float: right;
margin-bottom:10px;
}
.btn-danger {
color: #fff;
background-color: #dc3545;
border-color: #dc3545;
}
}

View File

@ -0,0 +1,6 @@
const TYPES = {
SET_OPTIONS: 'SET_OPTIONS',
DELETE_OPTION_BY_ID: 'DELETE_OPTION_BY_ID',
};
export default TYPES;

View File

@ -0,0 +1,37 @@
/**
* 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* deleteOptionById( optionId ) {
try {
yield apiFetch( {
method: 'DELETE',
path: `${ API_NAMESPACE }/options/${ optionId }`,
} );
yield {
type: TYPES.DELETE_OPTION_BY_ID,
optionId,
};
} catch {
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/v1';

22
src/options/data/index.js Normal file
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,30 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
const DEFAULT_STATE = {
options: [],
};
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case TYPES.SET_OPTIONS:
return {
...state,
options: action.options,
};
case TYPES.DELETE_OPTION_BY_ID:
return {
...state,
options: state.options.filter(
( item ) => item.option_id !== action.optionId
),
};
default:
return state;
}
};
export default reducer;

View File

@ -0,0 +1,27 @@
/**
* External dependencies
*/
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { API_NAMESPACE } from './constants';
import { setOptions } from './actions';
export function* getOptions( search ) {
let path = `${ API_NAMESPACE }/options?`;
if ( search ) {
path += `search=${ search }`;
}
console.log( path );
try {
const response = yield apiFetch( {
path,
} );
yield setOptions( response );
} catch ( error ) {
throw new Error();
}
}

View File

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

122
src/options/index.js Normal file
View File

@ -0,0 +1,122 @@
/**
* External dependencies
*/
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';
/**
* Internal dependencies
*/
import { STORE_KEY } from './data/constants';
import './data';
function Options( {
options,
getOptions,
deleteOptionById,
invalidateResolutionForStoreSelector,
} ) {
const deleteOption = function ( optionId ) {
// eslint-disable-next-line no-alert
if ( confirm( 'Are you sure you want to delete?' ) ) {
deleteOptionById( optionId );
}
};
const renderTableData = function () {
return options.map( ( option ) => {
// eslint-disable-next-line camelcase
const { option_id, option_name, autoload } = option;
// eslint-disable-next-line camelcase
const optionId = option_id;
// eslint-disable-next-line camelcase
const optionName = option_name;
return (
<tr key={ optionId }>
<td key={ 0 }>{ optionId }</td>
<td key={ 1 }>{ optionName }</td>
<td key={ 2 }>{ autoload }</td>
<td key={ 3 }>
<button
className="button btn-danger"
onClick={ () => deleteOption( optionId ) }
>
Delete
</button>
</td>
</tr>
);
} );
};
const searchOption = function ( event ) {
event.preventDefault();
let keyword = event.target.search.value;
if ( keyword === '' ) {
keyword = undefined;
}
getOptions( keyword );
// force invlidation of the cached selector resolvers
invalidateResolutionForStoreSelector( 'getOptions' );
};
return (
<div id="wc-admin-test-helper-options">
<form onSubmit={ searchOption }>
<div className="search-box">
<label
className="screen-reader-text"
htmlFor="post-search-input"
>
Search products:
</label>
<input type="search" name="search" />
<input
type="submit"
id="search-submit"
className="button"
value="Search Option"
/>
</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={ 2 }>
Autoload
</td>
<td className="manage-column column-thumb" key={ 3 }>
Delete
</td>
</tr>
</thead>
<tbody>{ renderTableData() }</tbody>
</table>
</div>
);
}
export default compose(
withSelect( ( select ) => {
const { getOptions } = select( STORE_KEY );
const options = getOptions();
return { options, getOptions };
} ),
withDispatch( ( dispatch ) => {
const {
deleteOptionById,
invalidateResolutionForStoreSelector,
} = dispatch( STORE_KEY );
return { deleteOptionById, invalidateResolutionForStoreSelector };
} )
)( Options );

View File

@ -50,7 +50,11 @@ function add_extension_register_script() {
),
$css_file_version
);
wp_enqueue_style( 'woocommerce-admin-test-helper' );
wp_enqueue_style('woocommerce-styles', '/wp-content/plugins/woocommerce-admin/dist/components/style.css');
}
add_action( 'admin_enqueue_scripts', 'add_extension_register_script' );