Allow option editing

This commit is contained in:
Moon 2021-03-27 10:03:18 -07:00
parent f0098cdbcb
commit 028d4401f2
9 changed files with 322 additions and 54 deletions

View File

@ -22,7 +22,8 @@
"devDependencies": {
"@woocommerce/dependency-extraction-webpack-plugin": "1.4.0",
"@woocommerce/eslint-plugin": "1.1.0",
"@wordpress/scripts": "^13.0.3"
"@wordpress/scripts": "^13.0.3",
"prop-types": "^15.7.2"
},
"dependencies": {
"@woocommerce/data": "^1.1.1",

View File

@ -23,4 +23,21 @@
background-color: #dc3545;
border-color: #dc3545;
}
.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;
}

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

@ -1,6 +1,8 @@
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_BY_ID: 'DELETE_OPTION_BY_ID',
};

View File

@ -28,6 +28,20 @@ export function setLoadingState( 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* deleteOptionById( optionId ) {
try {
yield apiFetch( {
@ -42,3 +56,26 @@ export function* deleteOptionById( optionId ) {
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

@ -6,10 +6,26 @@ 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,
@ -21,6 +37,14 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
options: action.options,
isLoading: false,
};
case TYPES.SET_NOTICE:
return {
...state,
notice: {
...state.notice,
...action.notice,
},
};
case TYPES.DELETE_OPTION_BY_ID:
return {
...state,

View File

@ -7,7 +7,7 @@ import { apiFetch } from '@wordpress/data-controls';
* Internal dependencies
*/
import { API_NAMESPACE } from './constants';
import { setLoadingState, setOptions } from './actions';
import { setLoadingState, setOptions, setOptionForEditing } from './actions';
export function* getOptions( search ) {
let path = `${ API_NAMESPACE }/options?`;
@ -26,3 +26,36 @@ export function* getOptions( search ) {
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

@ -5,3 +5,11 @@ export function getOptions( state ) {
export function isLoading( state ) {
return state.isLoading;
}
export function getOptionForEditing( state ) {
return state.editingOption;
}
export function getNotice( state ) {
return state.notice;
}

View File

@ -3,11 +3,14 @@
*/
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 Options( {
@ -16,29 +19,50 @@ function Options( {
deleteOptionById,
isLoading,
invalidateResolution,
getOptionForEditing,
editingOption,
saveOption,
notice,
setNotice,
} ) {
const deleteOption = function ( optionId ) {
const [ isEditModalOpen, setEditModalOpen ] = useState( false );
const deleteOption = ( optionId ) => {
// eslint-disable-next-line no-alert
if ( confirm( 'Are you sure you want to delete?' ) ) {
deleteOptionById( optionId );
}
};
const renderLoading = function () {
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="4" align="center">
<td colSpan="5" align="center">
Loading...
</td>
</tr>
);
};
const renderTableData = function () {
const renderTableData = () => {
if ( options.length === 0 ) {
return (
<tr>
<td colSpan="4" align="center">
<td colSpan="5" align="center">
No Options Found
</td>
</tr>
@ -58,8 +82,10 @@ function Options( {
<tr key={ optionId }>
<td key={ 0 }>{ optionId }</td>
<td key={ 1 }>{ optionName }</td>
<td key={ 2 }>{ autoload }</td>
<td key={ 3 }>
<td className="align-center" key={ 2 }>
{ autoload }
</td>
<td className="align-center" key={ 3 }>
<button
className="button btn-danger"
onClick={ () => deleteOption( optionId ) }
@ -67,12 +93,20 @@ function Options( {
Delete
</button>
</td>
<td className="align-center" key={ 4 }>
<button
className="button btn-primary"
onClick={ () => openEditModal( optionName ) }
>
Edit
</button>
</td>
</tr>
);
} );
};
const searchOption = function ( event ) {
const searchOption = ( event ) => {
event.preventDefault();
const keyword = event.target.search.value;
@ -84,61 +118,125 @@ function Options( {
};
return (
<div id="wc-admin-test-helper-options">
<form onSubmit={ searchOption }>
<div className="search-box">
<label
className="screen-reader-text"
htmlFor="post-search-input"
<>
{ 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: '' } );
} }
>
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>
{ isLoading ? renderLoading() : renderTableData() }
</tbody>
</table>
</div>
{ notice.message }
</Notice>
) }
<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 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, isLoading } = select( STORE_KEY );
const {
getOptions,
getOptionForEditing,
getNotice,
isLoading,
} = select( STORE_KEY );
const options = getOptions();
const editingOption = getOptionForEditing();
const notice = getNotice();
return { options, getOptions, isLoading: isLoading() };
return {
options,
getOptions,
isLoading: isLoading(),
editingOption,
getOptionForEditing,
notice,
};
} ),
withDispatch( ( dispatch ) => {
const { deleteOptionById } = dispatch( STORE_KEY );
const { deleteOptionById, saveOption, setNotice } = dispatch(
STORE_KEY
);
const { invalidateResolution } = dispatch( 'core/data' );
return { deleteOptionById, invalidateResolution };
return {
deleteOptionById,
invalidateResolution,
saveOption,
setNotice,
};
} )
)( Options );