* Show success screen based on conditions instead of step

* Use promise chain to handle updating tax settings

* Fix up error handling for failed setting updates

* Skip store location step if complete address exists

* Fix up pending/requesting state logic

* Allow opt in to TOS on tax task

* Don't complete task from completeStep

* Add caption styling to TOS text
This commit is contained in:
Joshua T Flowers 2020-07-01 15:19:15 +03:00 committed by GitHub
parent 1d5e7db33d
commit 8e5291e231
8 changed files with 238 additions and 265 deletions

View File

@ -696,11 +696,7 @@ BusinessDetails.contextType = CurrencyContext;
export default compose(
withSelect( ( select ) => {
const {
getSettings,
getSettingsError,
isGetSettingsRequesting,
} = select( SETTINGS_STORE_NAME );
const { getSettings, getSettingsError } = select( SETTINGS_STORE_NAME );
const { getProfileItems, getOnboardingError } = select(
ONBOARDING_STORE_NAME
);
@ -716,7 +712,6 @@ export default compose(
isError: Boolean( getOnboardingError( 'updateProfileItems' ) ),
profileItems: getProfileItems(),
isSettingsError: Boolean( getSettingsError( 'general' ) ),
isSettingsRequesting: isGetSettingsRequesting( 'general' ),
settings,
isInstallingActivating:
isPluginsRequesting( 'installPlugins' ) ||

View File

@ -237,11 +237,7 @@ StoreDetails.contextType = CurrencyContext;
export default compose(
withSelect( ( select ) => {
const {
getSettings,
getSettingsError,
isGetSettingsRequesting,
} = select( SETTINGS_STORE_NAME );
const { getSettings, getSettingsError } = select( SETTINGS_STORE_NAME );
const { getOnboardingError, getProfileItems } = select(
ONBOARDING_STORE_NAME
);
@ -253,12 +249,10 @@ export default compose(
const { general: settings = {} } = getSettings( 'general' );
const isSettingsError = Boolean( getSettingsError( 'general' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'general' );
return {
isProfileItemsError,
isSettingsError,
isSettingsRequesting,
profileItems,
settings,
};

View File

@ -584,3 +584,8 @@
border-top-right-radius: 2px;
}
}
.woocommerce-task__caption {
color: $dark-gray-300;
margin-top: $gap;
}

View File

@ -302,13 +302,13 @@ class Shipping extends Component {
render() {
const { isPending, step } = this.state;
const { isSettingsRequesting } = this.props;
const { isUpdateSettingsRequesting } = this.props;
return (
<div className="woocommerce-task-shipping">
<Card className="is-narrow">
<Stepper
isPending={ isPending || isSettingsRequesting }
isPending={ isPending || isUpdateSettingsRequesting }
isVertical
currentStep={ step }
steps={ this.getSteps() }
@ -321,19 +321,14 @@ class Shipping extends Component {
export default compose(
withSelect( ( select ) => {
const {
getSettings,
getSettingsError,
isGetSettingsRequesting,
} = select( SETTINGS_STORE_NAME );
const { getSettings, isUpdateSettingsRequesting } = select(
SETTINGS_STORE_NAME
);
const { getActivePlugins, isJetpackConnected } = select(
PLUGINS_STORE_NAME
);
const { general: settings = {} } = getSettings( 'general' );
const isSettingsError = Boolean( getSettingsError( 'general' ) );
const isSettingsRequesting = isGetSettingsRequesting( 'general' );
const countryCode = getCountryCode(
settings.woocommerce_default_country
);
@ -348,8 +343,7 @@ export default compose(
return {
countryCode,
countryName,
isSettingsError,
isSettingsRequesting,
isUpdateSettingsRequesting: isUpdateSettingsRequesting( 'general' ),
settings,
activePlugins,
isJetpackConnected: isJetpackConnected(),

View File

@ -2,7 +2,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Button, __experimentalText as Text } from '@wordpress/components';
import { Component, Fragment } from '@wordpress/element';
import { compose } from '@wordpress/compose';
import { difference, filter } from 'lodash';
@ -29,6 +29,7 @@ import {
* Internal dependencies
*/
import Connect from 'dashboard/components/connect';
import { createNoticesFromResponse } from 'lib/notices';
import { getCountryCode } from 'dashboard/utils';
import StoreLocation from './steps/location';
import { recordEvent, queueRecordEvent } from 'lib/tracks';
@ -36,24 +37,18 @@ import { recordEvent, queueRecordEvent } from 'lib/tracks';
class Tax extends Component {
constructor( props ) {
super( props );
const { hasCompleteAddress, pluginsToActivate } = props;
this.initialState = {
isPending: false,
stepIndex: 0,
automatedTaxEnabled: true,
// Cache the value of pluginsToActivate so that we can show/hide tasks based on it, but not have them update mid task.
pluginsToActivate: props.pluginsToActivate,
stepIndex: hasCompleteAddress ? 1 : 0,
// Cache the value of pluginsToActivate so that we can
// show/hide tasks based on it, but not have them update mid task.
cachedPluginsToActivate: pluginsToActivate,
};
this.state = this.initialState;
this.completeStep = this.completeStep.bind( this );
this.configureTaxRates = this.configureTaxRates.bind( this );
this.updateAutomatedTax = this.updateAutomatedTax.bind( this );
this.setIsPending = this.setIsPending.bind( this );
this.shouldShowSuccessScreen = this.shouldShowSuccessScreen.bind(
this
);
}
componentDidMount() {
@ -65,87 +60,22 @@ class Tax extends Component {
}
shouldShowSuccessScreen() {
const { stepIndex } = this.state;
const {
isJetpackConnected,
hasCompleteAddress,
pluginsToActivate,
generalSettings,
} = this.props;
const {
woocommerce_store_address: storeAddress,
woocommerce_default_country: defaultCountry,
woocommerce_store_postcode: storePostCode,
} = generalSettings;
const isCompleteAddress = Boolean(
storeAddress && defaultCountry && storePostCode
);
return (
stepIndex !== null &&
isCompleteAddress &&
hasCompleteAddress &&
! pluginsToActivate.length &&
isJetpackConnected &&
this.isTaxJarSupported()
);
}
componentDidUpdate( prevProps ) {
const { generalSettings, isJetpackConnected, taxSettings } = this.props;
const {
woocommerce_calc_taxes: calcTaxes,
woocommerce_store_address: storeAddress,
woocommerce_default_country: defaultCountry,
woocommerce_store_postcode: storePostCode,
} = generalSettings;
const { stepIndex } = this.state;
const currentStep = this.getSteps()[ stepIndex ];
const currentStepKey = currentStep && currentStep.key;
if ( stepIndex !== null && this.shouldShowSuccessScreen() ) {
/* eslint-disable react/no-did-update-set-state */
this.setState( { stepIndex: null } );
/* eslint-enable react/no-did-update-set-state */
return;
}
if (
taxSettings.wc_connect_taxes_enabled &&
taxSettings.wc_connect_taxes_enabled !==
prevProps.taxSettings.wc_connect_taxes_enabled
) {
/* eslint-disable react/no-did-update-set-state */
this.setState( {
automatedTaxEnabled:
taxSettings.wc_connect_taxes_enabled === 'yes'
? true
: false,
} );
/* eslint-enable react/no-did-update-set-state */
}
if ( currentStepKey === 'connect' && isJetpackConnected ) {
this.completeStep();
}
const isCompleteAddress = Boolean(
storeAddress && defaultCountry && storePostCode
);
if ( currentStepKey === 'store_location' && isCompleteAddress ) {
this.completeStep();
}
const {
woocommerce_calc_taxes: prevCalcTaxes,
} = prevProps.generalSettings;
if ( prevCalcTaxes === 'no' && calcTaxes === 'yes' ) {
window.location = getAdminLink(
'admin.php?page=wc-settings&tab=tax&section=standard'
);
}
}
isTaxJarSupported() {
const { countryCode, tosAccepted } = this.props;
const { countryCode } = this.props;
const {
automatedTaxSupportedCountries = [],
taxJarActivated,
@ -153,7 +83,6 @@ class Tax extends Component {
return (
! taxJarActivated && // WCS integration doesn't work with the official TaxJar plugin.
tosAccepted &&
automatedTaxSupportedCountries.includes( countryCode )
);
}
@ -165,12 +94,10 @@ class Tax extends Component {
if ( nextStep ) {
this.setState( { stepIndex: stepIndex + 1 } );
} else {
getHistory().push( getNewPath( {}, '/', {} ) );
}
}
async configureTaxRates() {
async manuallyConfigureTaxRates() {
const {
generalSettings,
updateAndPersistSettingsForGroup,
@ -178,82 +105,87 @@ class Tax extends Component {
if ( generalSettings.woocommerce_calc_taxes !== 'yes' ) {
this.setState( { isPending: true } );
await updateAndPersistSettingsForGroup( 'general', {
updateAndPersistSettingsForGroup( 'general', {
general: {
...generalSettings,
woocommerce_calc_taxes: 'yes',
},
} );
} )
.then( () => this.redirectToTaxSettings() )
.catch( ( error ) => createNoticesFromResponse( error ) );
} else {
this.redirectToTaxSettings();
}
window.location = getAdminLink(
'admin.php?page=wc-settings&tab=tax&section=standard&wc_onboarding_active_task=tax'
);
}
async updateAutomatedTax() {
updateAutomatedTax( isEnabling ) {
const {
createNotice,
updateAndPersistSettingsForGroup,
generalSettings,
taxSettings,
} = this.props;
const { automatedTaxEnabled } = this.state;
await updateAndPersistSettingsForGroup( 'tax', {
tax: {
...taxSettings,
wc_connect_taxes_enabled: automatedTaxEnabled ? 'yes' : 'no',
},
} );
await updateAndPersistSettingsForGroup( 'general', {
general: {
...generalSettings,
woocommerce_calc_taxes: 'yes',
},
} );
if (
! this.props.isTaxSettingsError &&
! this.props.isGeneralSettingsError
) {
// @todo This is a workaround to force the task to mark as complete.
// This should probably be updated to use wc-api so we can fetch tax rates.
setSetting( 'onboarding', {
...getSetting( 'onboarding', {} ),
isTaxComplete: true,
Promise.all( [
updateAndPersistSettingsForGroup( 'tax', {
tax: {
...taxSettings,
wc_connect_taxes_enabled: isEnabling ? 'yes' : 'no',
},
} ),
updateAndPersistSettingsForGroup( 'general', {
general: {
...generalSettings,
woocommerce_calc_taxes: 'yes',
},
} ),
] )
.then( () => {
// @todo This is a workaround to force the task to mark as complete.
// This should probably be updated to use wc-api so we can fetch tax rates.
setSetting( 'onboarding', {
...getSetting( 'onboarding', {} ),
isTaxComplete: true,
} );
if ( isEnabling ) {
createNotice(
'success',
__(
"You're awesome! One less item on your to-do list ✅",
'woocommerce-admin'
)
);
getHistory().push( getNewPath( {}, '/', {} ) );
} else {
this.redirectToTaxSettings();
}
} )
.catch( () => {
createNotice(
'error',
__(
'There was a problem updating your tax settings.',
'woocommerce-admin'
)
);
} );
createNotice(
'success',
__(
"You're awesome! One less item on your to-do list ✅",
'woocommerce-admin'
)
);
if ( automatedTaxEnabled ) {
getHistory().push( getNewPath( {}, '/', {} ) );
} else {
this.configureTaxRates();
}
} else {
createNotice(
'error',
__(
'There was a problem updating your tax settings.',
'woocommerce-admin'
)
);
}
}
setIsPending( value ) {
this.setState( { isPending: value } );
redirectToTaxSettings() {
window.location = getAdminLink(
'admin.php?page=wc-settings&tab=tax&section=standard&wc_onboarding_active_task=tax'
);
}
getSteps() {
const { generalSettings, isJetpackConnected } = this.props;
const { isPending, pluginsToActivate } = this.state;
const {
generalSettings,
isJetpackConnected,
isPending,
tosAccepted,
updateOptions,
} = this.props;
const { cachedPluginsToActivate } = this.state;
const steps = [
{
@ -273,12 +205,7 @@ class Tax extends Component {
recordEvent( 'tasklist_tax_set_location', {
country,
} );
if ( this.shouldShowSuccessScreen() ) {
this.setState( { stepIndex: null } );
// Only complete step if another update hasn't already shown succes screen.
} else if ( this.state.stepIndex !== null ) {
this.completeStep();
}
this.completeStep();
} }
isSettingsRequesting={ false }
settings={ generalSettings }
@ -297,31 +224,63 @@ class Tax extends Component {
'woocommerce-admin'
),
content: (
<Plugins
onComplete={ () => {
recordEvent( 'tasklist_tax_install_extensions', {
install_extensions: true,
} );
this.completeStep();
} }
onSkip={ () => {
queueRecordEvent(
'tasklist_tax_install_extensions',
{
install_extensions: false,
}
);
window.location.href = getAdminLink(
'admin.php?page=wc-settings&tab=tax&section=standard'
);
} }
skipText={ __(
'Set up tax rates manually',
'woocommerce-admin'
<Fragment>
<Plugins
onComplete={ () => {
recordEvent(
'tasklist_tax_install_extensions',
{
install_extensions: true,
}
);
updateOptions( {
woocommerce_setup_jetpack_opted_in: true,
} );
this.completeStep();
} }
onSkip={ () => {
queueRecordEvent(
'tasklist_tax_install_extensions',
{
install_extensions: false,
}
);
this.redirectToTaxSettings();
} }
skipText={ __(
'Set up tax rates manually',
'woocommerce-admin'
) }
/>
{ ! tosAccepted && (
<Text
variant="caption"
className="woocommerce-task__caption"
>
{ interpolateComponents( {
mixedString: __(
'By installing Jetpack and WooCommerce Services you agree to the {{link}}Terms of Service{{/link}}.',
'woocommerce-admin'
),
components: {
link: (
<Link
href={
'https://wordpress.com/tos/'
}
target="_blank"
type="external"
/>
),
},
} ) }
</Text>
) }
/>
</Fragment>
),
visible: pluginsToActivate.length && this.isTaxJarSupported(),
visible:
( cachedPluginsToActivate.length || ! tosAccepted ) &&
this.isTaxJarSupported(),
},
{
key: 'connect',
@ -333,7 +292,6 @@ class Tax extends Component {
content: (
<Connect
{ ...this.props }
setIsPending={ this.setIsPending }
onConnect={ () => {
recordEvent( 'tasklist_tax_connect_store', {
connect: true,
@ -343,9 +301,7 @@ class Tax extends Component {
queueRecordEvent( 'tasklist_tax_connect_store', {
connect: false,
} );
window.location.href = getAdminLink(
'admin.php?page=wc-settings&tab=tax&section=standard'
);
this.manuallyConfigureTaxRates();
} }
skipText={ __(
'Set up tax rates manually',
@ -365,11 +321,12 @@ class Tax extends Component {
content: (
<Fragment>
<Button
disabled={ isPending }
isPrimary
isBusy={ isPending }
onClick={ () => {
recordEvent( 'tasklist_tax_config_rates' );
this.configureTaxRates();
this.manuallyConfigureTaxRates();
} }
>
{ __( 'Configure', 'woocommerce-admin' ) }
@ -405,21 +362,14 @@ class Tax extends Component {
}
render() {
const { isPending, stepIndex } = this.state;
const { isTaxSettingsRequesting, taxSettings } = this.props;
const { stepIndex } = this.state;
const { isPending, isResolving } = this.props;
const step = this.getSteps()[ stepIndex ];
return (
<div className="woocommerce-task-tax">
<Card className="is-narrow">
{ step ? (
<Stepper
isPending={ isPending || isTaxSettingsRequesting }
isVertical={ true }
currentStep={ step.key }
steps={ this.getSteps() }
/>
) : (
{ this.shouldShowSuccessScreen() ? (
<div className="woocommerce-task-tax__success">
<span
className="woocommerce-task-tax__success-icon"
@ -444,11 +394,9 @@ class Tax extends Component {
} ) }
</p>
<Button
disabled={ isPending }
isPrimary
isBusy={
Object.keys( taxSettings ).length &&
isTaxSettingsRequesting
}
isBusy={ isPending }
onClick={ () => {
recordEvent(
'tasklist_tax_setup_automated_proceed',
@ -456,15 +404,14 @@ class Tax extends Component {
setup_automatically: true,
}
);
this.setState(
{ automatedTaxEnabled: true },
this.updateAutomatedTax
);
this.updateAutomatedTax( true );
} }
>
{ __( 'Yes please', 'woocommerce-admin' ) }
</Button>
<Button
disabled={ isPending }
isBusy={ isPending }
onClick={ () => {
recordEvent(
'tasklist_tax_setup_automated_proceed',
@ -472,10 +419,7 @@ class Tax extends Component {
setup_automatically: false,
}
);
this.setState(
{ automatedTaxEnabled: false },
this.updateAutomatedTax
);
this.updateAutomatedTax( false );
} }
>
{ __(
@ -484,6 +428,13 @@ class Tax extends Component {
) }
</Button>
</div>
) : (
<Stepper
isPending={ isPending || isResolving }
isVertical={ true }
currentStep={ step.key }
steps={ this.getSteps() }
/>
) }
</Card>
</div>
@ -493,26 +444,30 @@ class Tax extends Component {
export default compose(
withSelect( ( select ) => {
const {
getSettings,
getSettingsError,
isGetSettingsRequesting,
} = select( SETTINGS_STORE_NAME );
const { getOption } = select( OPTIONS_STORE_NAME );
const { getActivePlugins, isJetpackConnected } = select(
PLUGINS_STORE_NAME
const { getSettings, isUpdateSettingsRequesting } = select(
SETTINGS_STORE_NAME
);
const { getOption } = select( OPTIONS_STORE_NAME );
const {
getActivePlugins,
isJetpackConnected,
isPluginsRequesting,
} = select( PLUGINS_STORE_NAME );
const { general: generalSettings = {} } = getSettings( 'general' );
const isGeneralSettingsError = Boolean( getSettingsError( 'general' ) );
const countryCode = getCountryCode(
generalSettings.woocommerce_default_country
);
const {
woocommerce_store_address: storeAddress,
woocommerce_default_country: defaultCountry,
woocommerce_store_postcode: storePostCode,
} = generalSettings;
const hasCompleteAddress = Boolean(
storeAddress && defaultCountry && storePostCode
);
const { tax: taxSettings = {} } = getSettings( 'tax' );
const isTaxSettingsError = Boolean( getSettingsError( 'tax' ) );
const isTaxSettingsRequesting = isGetSettingsRequesting( 'tax' );
const activePlugins = getActivePlugins();
const pluginsToActivate = difference(
[ 'jetpack', 'woocommerce-services' ],
@ -523,20 +478,26 @@ export default compose(
connectOptions.tos_accepted ||
getOption( 'woocommerce_setup_jetpack_opted_in' );
const isPending =
isUpdateSettingsRequesting( 'tax' ) ||
isUpdateSettingsRequesting( 'general' );
const isResolving = isPluginsRequesting( 'getJetpackConnectUrl' );
return {
isGeneralSettingsError,
generalSettings,
countryCode,
taxSettings,
isTaxSettingsError,
isTaxSettingsRequesting,
generalSettings,
hasCompleteAddress,
isJetpackConnected: isJetpackConnected(),
isPending,
isResolving,
pluginsToActivate,
taxSettings,
tosAccepted,
};
} ),
withDispatch( ( dispatch ) => {
const { createNotice } = dispatch( 'core/notices' );
const { updateOptions } = dispatch( OPTIONS_STORE_NAME );
const { updateAndPersistSettingsForGroup } = dispatch(
SETTINGS_STORE_NAME
);
@ -544,6 +505,7 @@ export default compose(
return {
createNotice,
updateAndPersistSettingsForGroup,
updateOptions,
};
} )
)( Tax );

View File

@ -2,6 +2,7 @@
* External Dependencies
*/
import { __ } from '@wordpress/i18n';
import { apiFetch, select } from '@wordpress/data-controls';
import { concat } from 'lodash';
@ -65,11 +66,16 @@ export function* persistSettingsForGroup( group ) {
}
// get data slice for keys
const dirtyData = yield select( STORE_NAME, 'getSettingsForGroup', group, dirtyKeys );
const dirtyData = yield select(
STORE_NAME,
'getSettingsForGroup',
group,
dirtyKeys
);
const url = `${ NAMESPACE }/settings/${ group }/batch`;
const update = dirtyKeys.reduce( ( updates, key ) => {
const u = Object.keys( dirtyData[ key ] ).map( k => {
const u = Object.keys( dirtyData[ key ] ).map( ( k ) => {
return { id: k, value: dirtyData[ key ][ k ] };
} );
return concat( updates, u );
@ -80,16 +86,25 @@ export function* persistSettingsForGroup( group ) {
method: 'POST',
data: { update },
} );
yield setIsRequesting( group, false );
if ( ! results ) {
throw new Error( 'settings did not update' );
throw new Error(
__(
'There was a problem updating your settings.',
'woocommerce-admin'
)
);
}
// remove dirtyKeys from map - note we're only doing this if there is no error.
yield clearIsDirty( group );
} catch ( e ) {
yield updateErrorForGroup( group, null, e );
yield setIsRequesting( group, false );
throw e;
}
// finally set the persisting state
yield setIsRequesting( group, false );
}
export function clearSettings() {

View File

@ -3,9 +3,9 @@
*/
import { getResourceName, getResourcePrefix } from '../utils';
export const getSettingsGroupNames = state => {
export const getSettingsGroupNames = ( state ) => {
const groupNames = new Set(
Object.keys( state ).map( resourceName => {
Object.keys( state ).map( ( resourceName ) => {
return getResourcePrefix( resourceName );
} )
);
@ -14,11 +14,11 @@ export const getSettingsGroupNames = state => {
export const getSettings = ( state, group ) => {
const settings = {};
const settingIds = state[ group ] && state[ group ].data || [];
const settingIds = ( state[ group ] && state[ group ].data ) || [];
if ( settingIds.length === 0 ) {
return settings;
}
settingIds.forEach( id => {
settingIds.forEach( ( id ) => {
settings[ id ] = state[ getResourceName( group, id ) ].data;
} );
return settings;
@ -47,7 +47,7 @@ export const getSettingsForGroup = ( state, group, keys ) => {
}, {} );
};
export const isGetSettingsRequesting = ( state, group ) => {
export const isUpdateSettingsRequesting = ( state, group ) => {
return state[ group ] && Boolean( state[ group ].isRequesting );
};
@ -70,9 +70,16 @@ export const isGetSettingsRequesting = ( state, group ) => {
* @return {*} The value present in the settings state for the given
* name.
*/
export function getSetting( state, group, name, fallback = false, filter = val => val ) {
export function getSetting(
state,
group,
name,
fallback = false,
filter = ( val ) => val
) {
const resourceName = getResourceName( group, name );
const value = ( state[ resourceName ] && state[ resourceName ].data ) || fallback;
const value =
( state[ resourceName ] && state[ resourceName ].data ) || fallback;
return filter( value, fallback );
}
@ -86,7 +93,7 @@ export const getLastSettingsErrorForGroup = ( state, group ) => {
export const getSettingsError = ( state, group, id ) => {
if ( ! id ) {
return state[ group ] && state[ group ].error || false;
return ( state[ group ] && state[ group ].error ) || false;
}
return state[ getResourceName( group, id ) ].error || false;
};

View File

@ -1,4 +1,3 @@
/**
* External dependencies
*/
@ -7,18 +6,23 @@ import { STORE_NAME } from './constants';
import { useCallback } from '@wordpress/element';
export const useSettings = ( group, settingsKeys = [] ) => {
const { requestedSettings, settingsError, isRequesting, isDirty } = useSelect(
select => {
const {
requestedSettings,
settingsError,
isRequesting,
isDirty,
} = useSelect(
( select ) => {
const {
getLastSettingsErrorForGroup,
getSettingsForGroup,
getIsDirty,
isGetSettingsRequesting,
isUpdateSettingsRequesting,
} = select( STORE_NAME );
return {
requestedSettings: getSettingsForGroup( group, settingsKeys ),
settingsError: Boolean( getLastSettingsErrorForGroup( group ) ),
isRequesting: isGetSettingsRequesting( group ),
isRequesting: isUpdateSettingsRequesting( group ),
isDirty: getIsDirty( group, settingsKeys ),
};
},
@ -35,14 +39,11 @@ export const useSettings = ( group, settingsKeys = [] ) => {
},
[ group ]
);
const persistSettings = useCallback(
() => {
// this action would simply persist all settings marked as dirty in the
// store state and then remove the dirty record in the isDirtyMap
persistSettingsForGroup( group );
},
[ group ]
);
const persistSettings = useCallback( () => {
// this action would simply persist all settings marked as dirty in the
// store state and then remove the dirty record in the isDirtyMap
persistSettingsForGroup( group );
}, [ group ] );
const updateAndPersistSettings = useCallback(
( name, data ) => {
updateAndPersistSettingsForGroup( group, { [ name ]: data } );