diff --git a/src/blocks/helpers/use-settings.js b/src/blocks/helpers/use-settings.js index 5fec66137..85e89959e 100644 --- a/src/blocks/helpers/use-settings.js +++ b/src/blocks/helpers/use-settings.js @@ -71,8 +71,9 @@ const useSettings = () => { * @param {string?} success Success message for Notice. * @param {string?} noticeId Notice ID. * @param {function?} onSuccess Callback function to be executed on success. + * @param {function?} onError Callback function to be executed on error. */ - const updateOption = ( option, value, success = __( 'Settings saved.', 'otter-blocks' ), noticeId = undefined, onSuccess = () => {}) => { + const updateOption = ( option, value, success = __( 'Settings saved.', 'otter-blocks' ), noticeId = undefined, onSuccess = () => {}, onError = () => {}) => { setStatus( 'saving' ); const save = new api.models.Settings({ [option]: value }).save(); @@ -105,6 +106,7 @@ const useSettings = () => { } ); } + getSettings(); onSuccess?.( response ); }); @@ -114,13 +116,15 @@ const useSettings = () => { createNotice( 'error', - response.responseJSON.message ? response.responseJSON.message : __( 'An unknown error occurred.', 'otter-blocks' ), + response?.responseJSON?.message ?? __( 'An unknown error occurred.', 'otter-blocks' ), { isDismissible: true, type: 'snackbar', id: noticeId } ); + + onError?.( response ); }); }; diff --git a/src/dashboard/components/pages/Dashboard.js b/src/dashboard/components/pages/Dashboard.js index 8ca2d765d..1ea27e860 100644 --- a/src/dashboard/components/pages/Dashboard.js +++ b/src/dashboard/components/pages/Dashboard.js @@ -1,3 +1,8 @@ +/** + * External dependencies. + */ +import { isString } from 'lodash'; + /** * WordPress dependencies. */ @@ -18,6 +23,7 @@ import { dispatch } from '@wordpress/data'; import { Fragment, useEffect, + useReducer, useState } from '@wordpress/element'; @@ -25,23 +31,123 @@ import { * Internal dependencies. */ import ButtonControl from '../ButtonControl.js'; +import useSettings from '../../../blocks/helpers/use-settings'; + +const optionMapping = { + enableCustomCss: 'themeisle_blocks_settings_css_module', + enableBlocksAnimation: 'themeisle_blocks_settings_blocks_animation', + enableBlockConditions: 'themeisle_blocks_settings_block_conditions', + enableSectionDefaultBlock: 'themeisle_blocks_settings_default_block', + enableOptimizeAnimationsCss: 'themeisle_blocks_settings_optimize_animations_css', + enableRichSchema: 'themeisle_blocks_settings_disable_review_schema', + enableReviewScale: 'themeisle_blocks_settings_review_scale', + enableHighlightDynamic: 'themeisle_blocks_settings_highlight_dynamic', + enableAnonymousDataTracking: 'otter_blocks_logger_flag' +}; + +const initialState = { + values: { + enableCustomCss: false, + enableBlocksAnimation: false, + enableBlockConditions: false, + enableSectionDefaultBlock: false, + enableOptimizeAnimationsCss: false, + enableRichSchema: false, + enableReviewScale: false, + enableHighlightDynamic: false, + enableAnonymousDataTracking: 'no' + }, + status: { + enableCustomCss: 'init', + enableBlocksAnimation: 'init', + enableBlockConditions: 'init', + enableSectionDefaultBlock: 'init', + enableOptimizeAnimationsCss: 'init', + enableRichSchema: 'init', + enableReviewScale: 'init', + enableHighlightDynamic: 'init', + enableAnonymousDataTracking: 'init' + }, + dirty: { + enableCustomCss: false, + enableBlocksAnimation: false, + enableBlockConditions: false, + enableSectionDefaultBlock: false, + enableOptimizeAnimationsCss: false, + enableRichSchema: false, + enableReviewScale: false, + enableHighlightDynamic: false, + enableAnonymousDataTracking: false + }, + old: {} +}; + +/** + * Reducer. + * @param {Object} state The current state. + * @param {Object} action The action to be performed. + * @returns {*} + */ +const reducer = ( state, action ) => { + switch ( action.type ) { + case 'init': + state.values[ action.name ] = action.value; + state.status[ action.name ] = 'saved'; + return { ...state }; + + case 'update': + state.old[ action.name ] = isString( state.values[ action.name ]) ? state.values[ action.name ] : Boolean( state.values[ action.name ]); + state.values[ action.name ] = action.value; + state.dirty[ action.name ] = true; + return { ...state }; + + case 'status_bulk': + action.names.forEach( name => { + state.status[ name ] = action.value; + state.dirty[ name ] = false; + }); + return { ...state }; -const Dashboard = ({ - status, - getOption, - updateOption -}) => { + case 'saved': + state.status[ action.name ] = 'saved'; + state.values[ action.name ] = action.value; + state.old[ action.name ] = undefined; + return { ...state }; + + case 'rollback': + if ( undefined !== state.old[ action.name ]) { + state.values[action.name] = state.old[action.name]; + } + state.old[ action.name ] = undefined; + state.dirty[ action.name ] = false; + state.status[ action.name ] = 'saved'; + return { ...state }; + + default: + return state; + } +}; + +const Dashboard = () => { useEffect( () => { if ( ! Boolean( window.otterObj.stylesExist ) ) { setRegeneratedDisabled( true ); } }, []); + const [ getOption, updateOption, status ] = useSettings(); + const { createNotice } = dispatch( 'core/notices' ); const [ isRegeneratedDisabled, setRegeneratedDisabled ] = useState( false ); const [ isOpen, setOpen ] = useState( false ); + const [ state, applyAction ] = useReducer( reducer, initialState ); + + /** + * Regenerate styles. + * @returns {Promise} + */ const regenerateStyles = async() => { const data = await apiFetch({ path: 'otter/v1/regenerate', method: 'DELETE' }); @@ -58,6 +164,50 @@ const Dashboard = ({ setOpen( false ); }; + /** + * Initialize the state with values from the WordPress options. + */ + useEffect( () => { + if ( 'loaded' !== status ) { + return; + } + + Object.entries( state.status ) + .filter( ([ key, value ]) => 'init' === value ) + .forEach( ([ name, _ ]) => { + applyAction({ type: 'init', name, value: getOption( optionMapping[ name ]) }); + }); + }, [ state, status, getOption ]); + + /** + * Update the WordPress options. + */ + useEffect( () => { + const dirtyOptionNames = Object.entries( state.dirty ).filter( ([ key, value ]) => value ).map( ([ key, value ]) => key ); + + if ( dirtyOptionNames.length ) { + if ( 'error' !== status ) { + applyAction({ type: 'status_bulk', value: 'saving', names: dirtyOptionNames }); + } + + for ( const name of dirtyOptionNames ) { + updateOption( + optionMapping[ name ], + state.values[ name ], + __( 'Settings saved.', 'otter-blocks' ), + 'o-settings-saved-notice', + ( response ) => { + applyAction({ type: 'saved', name, value: response[ optionMapping[ name ] ] }); + }, + () => { + applyAction({ type: 'rollback', name }); + } + ); + } + } + + }, [ state, status ]); + return ( updateOption( 'themeisle_blocks_settings_css_module', ! Boolean( getOption( 'themeisle_blocks_settings_css_module' ) ) ) } + checked={ state.values.enableCustomCss } + disabled={ 'saving' === state.status.enableCustomCss } + onChange={ ( value ) => { + applyAction({ type: 'update', name: 'enableCustomCss', value }); + } } /> @@ -77,9 +229,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_blocks_animation', ! Boolean( getOption( 'themeisle_blocks_settings_blocks_animation' ) ) ) } + checked={ state.values.enableBlocksAnimation } + disabled={ 'saving' === state.status.enableBlocksAnimation } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableBlocksAnimation', value }) } /> @@ -87,9 +239,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_block_conditions', ! Boolean( getOption( 'themeisle_blocks_settings_block_conditions' ) ) ) } + checked={ state.values.enableBlockConditions } + disabled={ 'saving' === state.status.enableBlockConditions } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableBlockConditions', value }) } /> @@ -101,9 +253,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_default_block', ! Boolean( getOption( 'themeisle_blocks_settings_default_block' ) ) ) } + checked={ state.values.enableSectionDefaultBlock } + disabled={ 'saving' === state.status.enableSectionDefaultBlock } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableSectionDefaultBlock', value }) } /> @@ -111,9 +263,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_optimize_animations_css', ! Boolean( getOption( 'themeisle_blocks_settings_optimize_animations_css' ) ) ) } + checked={ state.values.enableOptimizeAnimationsCss } + disabled={ 'saving' === state.status.enableOptimizeAnimationsCss } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableOptimizeAnimationsCss', value }) } /> @@ -121,9 +273,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_disable_review_schema', ! Boolean( getOption( 'themeisle_blocks_settings_disable_review_schema' ) ) ) } + checked={ state.values.enableRichSchema } + disabled={ 'saving' === state.status.enableRichSchema } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableRichSchema', value }) } /> @@ -131,9 +283,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_review_scale', ! Boolean( getOption( 'themeisle_blocks_settings_review_scale' ) ) ) } + checked={ state.values.enableReviewScale } + disabled={ 'saving' === state.status.enableReviewScale } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableReviewScale', value }) } /> @@ -141,9 +293,9 @@ const Dashboard = ({ updateOption( 'themeisle_blocks_settings_highlight_dynamic', ! Boolean( getOption( 'themeisle_blocks_settings_highlight_dynamic' ) ) ) } + checked={ state.values.enableHighlightDynamic } + disabled={ 'saving' === state.status.enableHighlightDynamic } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableHighlightDynamic', value }) } /> @@ -151,9 +303,9 @@ const Dashboard = ({ updateOption( 'otter_blocks_logger_flag', ( 'yes' === getOption( 'otter_blocks_logger_flag' ) ? 'no' : 'yes' ) ) } + checked={ 'yes' === state.values.enableAnonymousDataTracking } + disabled={ 'saving' === state.status.enableAnonymousDataTracking } + onChange={ ( value ) => applyAction({ type: 'update', name: 'enableAnonymousDataTracking', value: value ? 'yes' : 'no' }) } />