🎨 Added confirmation dialog when leaving settings screen with unsaved changes (#871)

closes TryGhost/Ghost#8483

- Added a new modal component that gets rendered when leaving general/settings after changes have been done but not saved
- Removed independent saving logic for social URL for consistent UX
This commit is contained in:
Aileen Nowak 2017-10-04 17:49:30 +07:00 committed by Kevin Ansfield
parent 51c3b25bee
commit da38f0db19
6 changed files with 139 additions and 14 deletions

View File

@ -0,0 +1,12 @@
import ModalComponent from 'ghost-admin/components/modal-base';
import {invokeAction} from 'ember-invoke-action';
export default ModalComponent.extend({
actions: {
confirm() {
invokeAction(this, 'confirm').finally(() => {
this.send('closeModal');
});
}
}
});

View File

@ -134,6 +134,45 @@ export default Controller.extend({
}
},
toggleLeaveSettingsModal(transition) {
let leaveTransition = this.get('leaveSettingsTransition');
if (!transition && this.get('showLeaveSettingsModal')) {
this.set('leaveSettingsTransition', null);
this.set('showLeaveSettingsModal', false);
return;
}
if (!leaveTransition || transition.targetName === leaveTransition.targetName) {
this.set('leaveSettingsTransition', transition);
// if a save is running, wait for it to finish then transition
if (this.get('save.isRunning')) {
return this.get('save.last').then(() => {
transition.retry();
});
}
// we genuinely have unsaved data, show the modal
this.set('showLeaveSettingsModal', true);
}
},
leaveSettings() {
let transition = this.get('leaveSettingsTransition');
let model = this.get('model');
if (!transition) {
this.get('notifications').showAlert('Sorry, there was an error in the application. Please let the Ghost team know what happened.', {type: 'error'});
return;
}
// roll back changes on model props
model.rollbackAttributes();
return transition.retry();
},
validateFacebookUrl() {
let newUrl = this.get('_scratchFacebook');
let oldUrl = this.get('model.facebook');
@ -180,17 +219,13 @@ export default Controller.extend({
}
newUrl = `https://www.facebook.com/${username}`;
this.set('model.facebook', newUrl);
this.get('model.errors').remove('facebook');
this.get('model.hasValidated').pushObject('facebook');
// User input is validated
return this.get('save').perform().then(() => {
this.set('model.facebook', '');
run.schedule('afterRender', this, function () {
this.set('model.facebook', newUrl);
});
this.set('model.facebook', '');
run.schedule('afterRender', this, function () {
this.set('model.facebook', newUrl);
});
} else {
errMessage = 'The URL must be in a format like '
@ -243,17 +278,13 @@ export default Controller.extend({
}
newUrl = `https://twitter.com/${username}`;
this.set('model.twitter', newUrl);
this.get('model.errors').remove('twitter');
this.get('model.hasValidated').pushObject('twitter');
// User input is validated
return this.get('save').perform().then(() => {
this.set('model.twitter', '');
run.schedule('afterRender', this, function () {
this.set('model.twitter', newUrl);
});
this.set('model.twitter', '');
run.schedule('afterRender', this, function () {
this.set('model.twitter', newUrl);
});
} else {
errMessage = 'The URL must be in a format like '

View File

@ -26,6 +26,8 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
},
setupController(controller, models) {
// reset the leave setting transition
controller.set('leaveSettingsTransition', null);
controller.set('model', models.settings);
controller.set('themes', this.get('store').peekAll('theme'));
controller.set('availableTimezones', models.availableTimezones);
@ -38,6 +40,19 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, {
reloadSettings() {
return this.get('settings').reload();
},
willTransition(transition) {
let controller = this.get('controller');
let model = controller.get('model');
let modelIsDirty = model.get('hasDirtyAttributes');
if (modelIsDirty) {
transition.abort();
controller.send('toggleLeaveSettingsModal', transition);
return;
}
}
}
});

View File

@ -0,0 +1,17 @@
<header class="modal-header">
<h1>Are you sure you want to leave this page?</h1>
</header>
<a class="close" href="" title="Close" {{action "closeModal"}}>{{inline-svg "close"}}<span class="hidden">Close</span></a>
<div class="modal-body">
<p>
Hey there! It looks like you didn't save the changes you made.
</p>
<p>Save before you go!</p>
</div>
<div class="modal-footer">
<button {{action "closeModal"}} class="gh-btn" data-test-stay-button><span>Stay</span></button>
<button {{action "confirm"}} class="gh-btn gh-btn-red" data-test-leave-button><span>Leave</span></button>
</div>

View File

@ -6,6 +6,13 @@
</section>
</header>
{{#if showLeaveSettingsModal}}
{{gh-fullscreen-modal "leave-settings"
confirm=(action "leaveSettings")
close=(action "toggleLeaveSettingsModal")
modifier="action wide"}}
{{/if}}
<section class="view-container">
<div class="gh-setting-header">Publication info</div>

View File

@ -472,5 +472,48 @@ describe('Acceptance: Settings - General', function () {
expect(find('[data-test-twitter-error]').text().trim(), 'inline validation response')
.to.equal('');
});
it('warns when leaving without saving', async function () {
await visit('/settings/general');
expect(
find('[data-test-dated-permalinks-checkbox]').prop('checked'),
'date permalinks checkbox'
).to.be.false;
await click('[data-test-toggle-pub-info]');
await fillIn('[data-test-title-input]', 'New Blog Title');
await click('[data-test-dated-permalinks-checkbox]');
expect(
find('[data-test-dated-permalinks-checkbox]').prop('checked'),
'dated permalink checkbox'
).to.be.true;
await visit('/settings/team');
expect(find('.fullscreen-modal').length, 'modal exists').to.equal(1);
// Leave without saving
await(click('.fullscreen-modal [data-test-leave-button]'), 'leave without saving');
expect(currentURL(), 'currentURL').to.equal('/settings/team');
await visit('/settings/general');
expect(currentURL(), 'currentURL').to.equal('/settings/general');
// settings were not saved
expect(
find('[data-test-dated-permalinks-checkbox]').prop('checked'),
'date permalinks checkbox'
).to.be.false;
expect(
find('[data-test-title-input]').text().trim(),
'Blog title'
).to.equal('');
});
});
});