Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
🎨 Added confirmation dialog when leaving settings screen with unsaved…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
aileen authored and kevinansfield committed Oct 4, 2017
1 parent 7f8d924 commit bbbe603
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 14 deletions.
12 changes: 12 additions & 0 deletions app/components/modal-leave-settings.js
Original file line number Diff line number Diff line change
@@ -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');
});
}
}
});
59 changes: 45 additions & 14 deletions app/controllers/settings/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 '
Expand Down Expand Up @@ -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 '
Expand Down
15 changes: 15 additions & 0 deletions app/routes/settings/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
}

}
});
17 changes: 17 additions & 0 deletions app/templates/components/modal-leave-settings.hbs
Original file line number Diff line number Diff line change
@@ -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>
7 changes: 7 additions & 0 deletions app/templates/settings/general.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
43 changes: 43 additions & 0 deletions tests/acceptance/settings/general-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
});
});
});

0 comments on commit bbbe603

Please sign in to comment.