Prompt a modal to save any unsaved changes in OBW (https://github.com/woocommerce/woocommerce-admin/pull/8278)
* Add a modal to show when unsaved changes are detected * Add functions to track value changes from the steps * Warn unsaved changes for the store details * Add styles for the unsaved modal * Warn unsaved changes for the Industry * Warn unsaved changes for the Product Types * Warn unsaved changes for the Business Details * Add changelog * Add testing instructions. * Sort array values before comparison * Use only the array values to compare the diff * Catch rejected promise on continue * Set initial value to an empty object * Fix failing tests
This commit is contained in:
parent
c16a17a17b
commit
718bac6981
|
@ -1,5 +1,18 @@
|
|||
# Testing instructions
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Prompt a modal to save any unsaved changes in OBW
|
||||
|
||||
1. Start with a fresh install.
|
||||
2. Navigate to WooCommerce -> Home to start the OBW.
|
||||
3. Complete a few steps.
|
||||
4. Click any of the previous steps and make some changes.
|
||||
5. Click the next/previous step. You should be prompted by the modal to save your changes. Click the save button.
|
||||
6. Go back to the step and confirm the changes.
|
||||
7. Repeat the step, but click the disregard button for this time.
|
||||
8. Confirm the changes are not saved for this time.
|
||||
|
||||
## 3.2.0
|
||||
|
||||
### Fix category report query returns invalid net sales
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: Enhancement
|
||||
|
||||
Prompts a modal to save any unsaved changes when the users try to move to a different step #8278
|
|
@ -2,11 +2,68 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Component } from '@wordpress/element';
|
||||
import { filter } from 'lodash';
|
||||
import { filter, isEqual } from 'lodash';
|
||||
import { Stepper } from '@woocommerce/components';
|
||||
import { updateQueryString } from '@woocommerce/navigation';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import UnsavedChangesModal from './unsaved-changes-modal';
|
||||
|
||||
export default class ProfileWizardHeader extends Component {
|
||||
constructor( props ) {
|
||||
super( props );
|
||||
this.state = {
|
||||
showUnsavedChangesModal: false,
|
||||
};
|
||||
this.lastClickedStepKey = null;
|
||||
}
|
||||
|
||||
shouldWarnForUnsavedChanges( step ) {
|
||||
if ( typeof this.props.stepValueChanges[ step ] !== 'undefined' ) {
|
||||
const initialValues = this.props.stepValueChanges[ step ]
|
||||
.initialValues;
|
||||
const currentValues = this.props.stepValueChanges[ step ]
|
||||
.currentValues;
|
||||
|
||||
if (
|
||||
Array.isArray( initialValues ) &&
|
||||
Array.isArray( currentValues )
|
||||
) {
|
||||
initialValues.sort();
|
||||
currentValues.sort();
|
||||
}
|
||||
|
||||
return ! isEqual( initialValues, currentValues );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
findCurrentStep() {
|
||||
return this.props.steps.find(
|
||||
( s ) => s.key === this.props.currentStep
|
||||
);
|
||||
}
|
||||
|
||||
moveToLastClickedStep() {
|
||||
if ( this.lastClickedStepKey ) {
|
||||
updateQueryString( { step: this.lastClickedStepKey } );
|
||||
this.lastClickedStepKey = null;
|
||||
}
|
||||
}
|
||||
|
||||
saveCurrentStepChanges() {
|
||||
const currentStep = this.findCurrentStep();
|
||||
if ( ! currentStep ) {
|
||||
return null;
|
||||
}
|
||||
const stepValueChanges = this.props.stepValueChanges[ currentStep.key ];
|
||||
if ( typeof stepValueChanges.onSave === 'function' ) {
|
||||
stepValueChanges.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
renderStepper() {
|
||||
const { currentStep, steps } = this.props;
|
||||
const visibleSteps = filter( steps, ( step ) => !! step.label );
|
||||
|
@ -22,7 +79,14 @@ export default class ProfileWizardHeader extends Component {
|
|||
}
|
||||
|
||||
if ( ! previousStep || previousStep.isComplete ) {
|
||||
step.onClick = ( key ) => updateQueryString( { step: key } );
|
||||
step.onClick = ( key ) => {
|
||||
if ( this.shouldWarnForUnsavedChanges( currentStep ) ) {
|
||||
this.setState( { showUnsavedChangesModal: true } );
|
||||
this.lastClickedStepKey = key;
|
||||
} else {
|
||||
updateQueryString( { step: key } );
|
||||
}
|
||||
};
|
||||
}
|
||||
return step;
|
||||
} );
|
||||
|
@ -31,9 +95,7 @@ export default class ProfileWizardHeader extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const currentStep = this.props.steps.find(
|
||||
( s ) => s.key === this.props.currentStep
|
||||
);
|
||||
const currentStep = this.findCurrentStep();
|
||||
|
||||
if ( ! currentStep || ! currentStep.label ) {
|
||||
return null;
|
||||
|
@ -41,6 +103,19 @@ export default class ProfileWizardHeader extends Component {
|
|||
|
||||
return (
|
||||
<div className="woocommerce-profile-wizard__header">
|
||||
{ this.state.showUnsavedChangesModal && (
|
||||
<UnsavedChangesModal
|
||||
onClose={ () => {
|
||||
this.setState( { showUnsavedChangesModal: false } );
|
||||
this.moveToLastClickedStep();
|
||||
} }
|
||||
onSave={ () => {
|
||||
this.saveCurrentStepChanges();
|
||||
this.setState( { showUnsavedChangesModal: false } );
|
||||
this.moveToLastClickedStep();
|
||||
} }
|
||||
/>
|
||||
) }
|
||||
{ this.renderStepper() }
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -42,6 +42,11 @@ class ProfileWizard extends Component {
|
|||
super( props );
|
||||
this.cachedActivePlugins = props.activePlugins;
|
||||
this.goToNextStep = this.goToNextStep.bind( this );
|
||||
this.trackStepValueChanges = this.trackStepValueChanges.bind( this );
|
||||
this.updateCurrentStepValues = this.updateCurrentStepValues.bind(
|
||||
this
|
||||
);
|
||||
this.stepValueChanges = {};
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
|
@ -93,6 +98,36 @@ class ProfileWizard extends Component {
|
|||
document.body.classList.remove( 'is-wp-toolbar-disabled' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the initial and current values of a step to track the state of the step.
|
||||
* This is used to determine if the step has been changes or not.
|
||||
*
|
||||
* @param {string} step key of the step
|
||||
* @param {*} initialValues the initial values of the step
|
||||
* @param {*} currentValues the current values of the step
|
||||
* @param {Function} onSave a function to call when the step is saved
|
||||
*/
|
||||
trackStepValueChanges( step, initialValues, currentValues, onSave ) {
|
||||
this.stepValueChanges[ step ] = {
|
||||
initialValues,
|
||||
currentValues,
|
||||
onSave,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update currentValues of the given step.
|
||||
*
|
||||
* @param {string} step key of the step
|
||||
* @param {*} currentValues the current values of the step
|
||||
*/
|
||||
updateCurrentStepValues( step, currentValues ) {
|
||||
if ( ! this.stepValueChanges[ step ] ) {
|
||||
return;
|
||||
}
|
||||
this.stepValueChanges[ step ].currentValues = currentValues;
|
||||
}
|
||||
|
||||
getSteps() {
|
||||
const { profileItems } = this.props;
|
||||
const steps = [];
|
||||
|
@ -256,6 +291,8 @@ class ProfileWizard extends Component {
|
|||
skipProfiler: () => {
|
||||
this.skipProfiler();
|
||||
},
|
||||
trackStepValueChanges: this.trackStepValueChanges,
|
||||
updateCurrentStepValues: this.updateCurrentStepValues,
|
||||
} );
|
||||
const steps = this.getSteps().map( ( _step ) =>
|
||||
pick( _step, [ 'key', 'label', 'isComplete' ] )
|
||||
|
@ -264,7 +301,11 @@ class ProfileWizard extends Component {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ProfileWizardHeader currentStep={ stepKey } steps={ steps } />
|
||||
<ProfileWizardHeader
|
||||
currentStep={ stepKey }
|
||||
steps={ steps }
|
||||
stepValueChanges={ this.stepValueChanges }
|
||||
/>
|
||||
<div className={ classNames }>{ container }</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -143,7 +143,7 @@ export const isSellingOtherPlatformInPerson = ( selectedOption ) =>
|
|||
[ 'other', 'brick-mortar-other' ].includes( selectedOption );
|
||||
|
||||
class BusinessDetails extends Component {
|
||||
constructor() {
|
||||
constructor( props ) {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
|
@ -155,6 +155,12 @@ class BusinessDetails extends Component {
|
|||
|
||||
this.onContinue = this.onContinue.bind( this );
|
||||
this.validate = this.validate.bind( this );
|
||||
props.trackStepValueChanges(
|
||||
props.step.key,
|
||||
{ ...( this.state.savedValues || props.initialValues ) },
|
||||
this.savedValues || props.initialValues,
|
||||
this.persistProfileItems.bind( this )
|
||||
);
|
||||
}
|
||||
|
||||
async onContinue(
|
||||
|
@ -436,6 +442,10 @@ class BusinessDetails extends Component {
|
|||
} }
|
||||
onChange={ ( _, values, isValid ) => {
|
||||
this.setState( { savedValues: values, isValid } );
|
||||
this.props.updateCurrentStepValues(
|
||||
this.props.step.key,
|
||||
values
|
||||
);
|
||||
} }
|
||||
validate={ this.validate }
|
||||
>
|
||||
|
|
|
@ -66,6 +66,24 @@ class Industry extends Component {
|
|||
this.onContinue = this.onContinue.bind( this );
|
||||
this.onIndustryChange = this.onIndustryChange.bind( this );
|
||||
this.onDetailChange = this.onDetailChange.bind( this );
|
||||
const selectedSlugs = this.getSelectedSlugs();
|
||||
props.trackStepValueChanges(
|
||||
props.step.key,
|
||||
selectedSlugs,
|
||||
selectedSlugs,
|
||||
this.onContinue
|
||||
);
|
||||
}
|
||||
|
||||
getSelectedSlugs() {
|
||||
return this.state.selected.map( ( industry ) => industry.slug );
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.updateCurrentStepValues(
|
||||
this.props.step.key,
|
||||
this.getSelectedSlugs()
|
||||
);
|
||||
}
|
||||
|
||||
async onContinue() {
|
||||
|
@ -74,12 +92,7 @@ class Industry extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
createNotice,
|
||||
goToNextStep,
|
||||
isError,
|
||||
updateProfileItems,
|
||||
} = this.props;
|
||||
const { createNotice, isError, updateProfileItems } = this.props;
|
||||
const selectedIndustriesList = this.state.selected.map(
|
||||
( industry ) => industry.slug
|
||||
);
|
||||
|
@ -96,9 +109,7 @@ class Industry extends Component {
|
|||
} );
|
||||
await updateProfileItems( { industry: this.state.selected } );
|
||||
|
||||
if ( ! isError ) {
|
||||
goToNextStep();
|
||||
} else {
|
||||
if ( isError ) {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
|
@ -106,7 +117,11 @@ class Industry extends Component {
|
|||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async validateField() {
|
||||
|
@ -251,7 +266,11 @@ class Industry extends Component {
|
|||
<CardFooter isBorderless justify="center">
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ this.onContinue }
|
||||
onClick={ () => {
|
||||
this.onContinue().then(
|
||||
this.props.goToNextStep
|
||||
);
|
||||
} }
|
||||
isBusy={ isProfileItemsRequesting }
|
||||
disabled={
|
||||
! selected.length || isProfileItemsRequesting
|
||||
|
|
|
@ -61,16 +61,33 @@ export class ProductTypes extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidUpdate( prevProps ) {
|
||||
componentDidUpdate( prevProps, prevState ) {
|
||||
const { profileItems, productTypes } = this.props;
|
||||
|
||||
if ( this.state.selected !== prevState.selected ) {
|
||||
this.props.updateCurrentStepValues(
|
||||
this.props.step.key,
|
||||
this.state.selected
|
||||
);
|
||||
}
|
||||
|
||||
if ( prevProps.productTypes !== productTypes ) {
|
||||
const defaultProductTypes = Object.keys( productTypes ).filter(
|
||||
( key ) => !! productTypes[ key ].default
|
||||
);
|
||||
this.setState( {
|
||||
selected: profileItems.product_types || defaultProductTypes,
|
||||
} );
|
||||
this.setState(
|
||||
{
|
||||
selected: profileItems.product_types || defaultProductTypes,
|
||||
},
|
||||
() => {
|
||||
this.props.trackStepValueChanges(
|
||||
this.props.step.key,
|
||||
[ ...this.state.selected ],
|
||||
this.state.selected,
|
||||
this.onContinue
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +102,7 @@ export class ProductTypes extends Component {
|
|||
return ! error;
|
||||
}
|
||||
|
||||
onContinue() {
|
||||
onContinue( onSuccess ) {
|
||||
const { selected } = this.state;
|
||||
const { installedPlugins = [] } = this.props;
|
||||
|
||||
|
@ -96,7 +113,6 @@ export class ProductTypes extends Component {
|
|||
const {
|
||||
countryCode,
|
||||
createNotice,
|
||||
goToNextStep,
|
||||
installAndActivatePlugins,
|
||||
updateProfileItems,
|
||||
} = this.props;
|
||||
|
@ -144,7 +160,9 @@ export class ProductTypes extends Component {
|
|||
'storeprofiler_store_product_type_continue',
|
||||
eventProps
|
||||
);
|
||||
goToNextStep();
|
||||
if ( typeof onSuccess === 'function' ) {
|
||||
onSuccess();
|
||||
}
|
||||
} )
|
||||
.catch( () =>
|
||||
createNotice(
|
||||
|
@ -260,7 +278,9 @@ export class ProductTypes extends Component {
|
|||
<CardFooter isBorderless justify="center">
|
||||
<Button
|
||||
isPrimary
|
||||
onClick={ this.onContinue }
|
||||
onClick={ () => {
|
||||
this.onContinue( this.props.goToNextStep );
|
||||
} }
|
||||
isBusy={
|
||||
isProfileItemsRequesting ||
|
||||
isInstallingActivating
|
||||
|
|
|
@ -71,6 +71,27 @@ export class StoreDetails extends Component {
|
|||
this.onContinue = this.onContinue.bind( this );
|
||||
this.onSubmit = this.onSubmit.bind( this );
|
||||
this.validateStoreDetails = this.validateStoreDetails.bind( this );
|
||||
this.onFormValueChange = this.onFormValueChange.bind( this );
|
||||
this.changedFormValues = {};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (
|
||||
this.props.isLoading === false &&
|
||||
Object.keys( this.changedFormValues ).length === 0
|
||||
) {
|
||||
// Make a copy of the initialValues.
|
||||
// The values in this object gets updated on onFormValueChange.
|
||||
this.changedFormValues = { ...this.props.initialValues };
|
||||
this.props.trackStepValueChanges(
|
||||
this.props.step.key,
|
||||
this.props.initialValues,
|
||||
this.changedFormValues,
|
||||
() => {
|
||||
this.onContinue( this.changedFormValues );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
deriveCurrencySettings( countryState ) {
|
||||
|
@ -98,10 +119,14 @@ export class StoreDetails extends Component {
|
|||
} );
|
||||
}
|
||||
|
||||
onFormValueChange( changedFormValue ) {
|
||||
this.changedFormValues[ changedFormValue.name ] =
|
||||
changedFormValue.value;
|
||||
}
|
||||
|
||||
async onContinue( values ) {
|
||||
const {
|
||||
createNotice,
|
||||
goToNextStep,
|
||||
updateProfileItems,
|
||||
updateAndPersistSettingsForGroup,
|
||||
profileItems,
|
||||
|
@ -184,20 +209,19 @@ export class StoreDetails extends Component {
|
|||
! Boolean( errorsRef.current.settings ) &&
|
||||
! errorMessages.length
|
||||
) {
|
||||
goToNextStep();
|
||||
} else {
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem saving your store details',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
|
||||
errorMessages.forEach( ( message ) =>
|
||||
createNotice( 'error', message )
|
||||
);
|
||||
return true;
|
||||
}
|
||||
createNotice(
|
||||
'error',
|
||||
__(
|
||||
'There was a problem saving your store details',
|
||||
'woocommerce-admin'
|
||||
)
|
||||
);
|
||||
|
||||
errorMessages.forEach( ( message ) =>
|
||||
createNotice( 'error', message )
|
||||
);
|
||||
}
|
||||
|
||||
validateStoreDetails( values ) {
|
||||
|
@ -302,6 +326,7 @@ export class StoreDetails extends Component {
|
|||
initialValues={ initialValues }
|
||||
onSubmit={ this.onSubmit }
|
||||
validate={ this.validateStoreDetails }
|
||||
onChange={ this.onFormValueChange }
|
||||
>
|
||||
{ ( {
|
||||
getInputProps,
|
||||
|
@ -317,7 +342,11 @@ export class StoreDetails extends Component {
|
|||
if ( skipping ) {
|
||||
skipProfiler();
|
||||
} else {
|
||||
this.onContinue( values );
|
||||
this.onContinue(
|
||||
values
|
||||
).then( () =>
|
||||
this.props.goToNextStep()
|
||||
);
|
||||
}
|
||||
} }
|
||||
onClose={ () =>
|
||||
|
|
|
@ -402,3 +402,36 @@
|
|||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-obw-unsaved-changes {
|
||||
width: 565px;
|
||||
max-width: 100%;
|
||||
|
||||
.components-modal__header {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.woocommerce-usage-modal__message {
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 0 32px;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
margin: 0 -32px 24px;
|
||||
}
|
||||
|
||||
.woocommerce-usage-modal__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $gap;
|
||||
|
||||
button {
|
||||
margin-left: $gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
const UnsavedChangesModal = ( { onClose, onSave } ) => {
|
||||
const title = __( 'Save changes?', 'woocommerce-admin' );
|
||||
const message = __(
|
||||
"You're about to go to a different step. Do you want to save the changes you've made here so far?",
|
||||
'woocommerce-admin'
|
||||
);
|
||||
const discardText = __( 'Discard', 'woocommerce-admin' );
|
||||
const saveText = __( 'Save', 'woocommerce-admin' );
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={ title }
|
||||
className="woocommerce-obw-unsaved-changes"
|
||||
onRequestClose={ onClose }
|
||||
>
|
||||
<div className="woocommerce-obw-unsaved-changes-modal__wrapper">
|
||||
<div className="woocommerce-usage-modal__message">
|
||||
{ message }
|
||||
</div>
|
||||
<div className="woocommerce-usage-modal__actions">
|
||||
<Button onClick={ () => onClose() }>
|
||||
{ discardText }
|
||||
</Button>
|
||||
<Button isPrimary onClick={ onSave }>
|
||||
{ saveText }
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsavedChangesModal;
|
|
@ -412,6 +412,7 @@ const testSubscriptionsInclusion = () => {
|
|||
} );
|
||||
|
||||
it( 'should display the task "Add Subscriptions to my store"', async () => {
|
||||
await profileWizard.navigate();
|
||||
await profileWizard.goToOBWStep( 'Store Details' );
|
||||
await profileWizard.skipStoreSetup();
|
||||
const homescreen = new WcHomescreen( page );
|
||||
|
@ -495,6 +496,7 @@ const testSubscriptionsInclusion = () => {
|
|||
} );
|
||||
|
||||
it( 'should not display the task "Add Subscriptions to my store"', async () => {
|
||||
await profileWizard.navigate();
|
||||
await profileWizard.goToOBWStep( 'Store Details' );
|
||||
await profileWizard.skipStoreSetup();
|
||||
const homescreen = new WcHomescreen( page );
|
||||
|
|
Loading…
Reference in New Issue