diff --git a/package.json b/package.json index f08b4aad5c8..2369e439d80 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.scss b/src/index.scss index e55b4600ba0..dfba22273c9 100644 --- a/src/index.scss +++ b/src/index.scss @@ -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; } \ No newline at end of file diff --git a/src/options/OptionEditor.js b/src/options/OptionEditor.js new file mode 100644 index 00000000000..5784b179fd3 --- /dev/null +++ b/src/options/OptionEditor.js @@ -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 ( + <> + + +
+ + ); +}; + +OptionEditor.propTypes = { + option: PropTypes.object.isRequired, + onSave: PropTypes.func.isRequired, +}; + +export default OptionEditor; diff --git a/src/options/data/action-types.js b/src/options/data/action-types.js index e0763d223c4..1dddc589a34 100644 --- a/src/options/data/action-types.js +++ b/src/options/data/action-types.js @@ -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', }; diff --git a/src/options/data/actions.js b/src/options/data/actions.js index dff16e2205d..1f0751f6efb 100644 --- a/src/options/data/actions.js +++ b/src/options/data/actions.js @@ -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(); + } +} diff --git a/src/options/data/reducer.js b/src/options/data/reducer.js index 80a09e2e898..cc74ab794f8 100644 --- a/src/options/data/reducer.js +++ b/src/options/data/reducer.js @@ -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, diff --git a/src/options/data/resolvers.js b/src/options/data/resolvers.js index 5058069e543..6c1ec7ce2ad 100644 --- a/src/options/data/resolvers.js +++ b/src/options/data/resolvers.js @@ -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 ); + } +} diff --git a/src/options/data/selectors.js b/src/options/data/selectors.js index 0bc9bd720a4..c2b78240e34 100644 --- a/src/options/data/selectors.js +++ b/src/options/data/selectors.js @@ -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; +} diff --git a/src/options/index.js b/src/options/index.js index 86fc47ee9e6..e7e0c2e0853 100644 --- a/src/options/index.js +++ b/src/options/index.js @@ -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 ( - + Loading... ); }; - const renderTableData = function () { + const renderTableData = () => { if ( options.length === 0 ) { return ( - + No Options Found @@ -58,8 +82,10 @@ function Options( { { optionId } { optionName } - { autoload } - + + { autoload } + + + + + ); } ); }; - const searchOption = function ( event ) { + const searchOption = ( event ) => { event.preventDefault(); const keyword = event.target.search.value; @@ -84,61 +118,125 @@ function Options( { }; return ( -
-
-
-
+ { notice.message } + + ) } +
+
+ + + +
+
+
+ + + + + + + + + + + + { isLoading ? renderLoading() : renderTableData() } + +
+ I.D + + Name + + Autoload + + Delete + + Edit +
+
+ ); } 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 );