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

Commit

Permalink
🎨 Added confirmation dialogs when leaving screens with unsaved changes (
Browse files Browse the repository at this point in the history
#891)

closes TryGhost/Ghost#9119, refs TryGhost/Ghost#8483

- Apps - AMP
   - Added `leave-settings-modal` component to Settings - Apps - AMP
- Apps - Slack
   - Added `leave-settings-modal` component to Settings - Apps - Slack
   - Added a `triggerDirtyState` action that will uses a new Array with the input data to trigger the dirty state on the parent settings model
- Apps - Unsplash
   - Added `leave-settings-modal` component to Settings - Apps - Unsplash
   - Used manual tracking of changes with using a custom `dirtyAttributes` property and a `rollbackValue` to manually rollback the `isActive` attribute on the model
- Code injection
   - Added `leave-settings-modal` component to Settings - Code injection
- Design
   - Added `leave-settings-modal` component to Settings - Design (only for navigation model)
   - Used manual tracking of changes with using a custom `dirtyAttributes`
   - Added an additional `updateLabel` action to underlying `gh-navitem` component which gets fired on the `focusOut` event, to detect changes on the label
- Team - User
   - Added `leave-settings-modal` component to Team - User
   - Used manual tracking of changes with using a custom `dirtyAttributes` to track changes in slug and role properties
  • Loading branch information
aileen authored and kevinansfield committed Oct 31, 2017
1 parent 1e73e59 commit 6ef4c62
Show file tree
Hide file tree
Showing 27 changed files with 718 additions and 178 deletions.
4 changes: 4 additions & 0 deletions app/components/gh-navitem.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export default Component.extend(ValidationState, {
this.sendAction('updateUrl', value, this.get('navItem'));
},

updateLabel(value) {
this.sendAction('updateLabel', value, this.get('navItem'));
},

clearLabelErrors() {
this.get('navItem.errors').remove('label');
},
Expand Down
42 changes: 41 additions & 1 deletion app/controllers/settings/apps/amp.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export default Controller.extend({

model: alias('settings.amp'),

leaveSettingsTransition: null,

save: task(function* () {
let amp = this.get('model');
let settings = this.get('settings');
Expand All @@ -17,7 +19,6 @@ export default Controller.extend({

try {
return yield settings.save();

} catch (error) {
this.get('notifications').showAPIError(error);
throw error;
Expand All @@ -31,6 +32,45 @@ export default Controller.extend({

save() {
this.get('save').perform();
},

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 settings = this.get('settings');

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
settings.rollbackAttributes();

return transition.retry();
}
}
});
67 changes: 62 additions & 5 deletions app/controllers/settings/apps/slack.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Controller from '@ember/controller';
import {alias} from '@ember/object/computed';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import {empty} from '@ember/object/computed';
import {isInvalidError} from 'ember-ajax/errors';
import {inject as service} from '@ember/service';
Expand All @@ -11,18 +11,23 @@ export default Controller.extend({
notifications: service(),
settings: service(),

model: alias('settings.slack.firstObject'),
model: boundOneWay('settings.slack.firstObject'),
testNotificationDisabled: empty('model.url'),

leaveSettingsTransition: null,
slackArray: [],

save: task(function* () {
let slack = this.get('model');
let settings = this.get('settings');
let slackArray = this.get('slackArray');

try {
yield slack.validate();
settings.get('slack').clear().pushObject(slack);
// clear existing objects in slackArray to make sure we only push the validated one
slackArray.clear().pushObject(slack);
yield settings.set('slack', slackArray);
return yield settings.save();

} catch (error) {
if (error) {
this.get('notifications').showAPIError(error);
Expand All @@ -40,7 +45,6 @@ export default Controller.extend({
yield this.get('ajax').post(slackApi);
notifications.showNotification('Check your Slack channel for the test message!', {type: 'info', key: 'slack-test.send.success'});
return true;

} catch (error) {
notifications.showAPIError(error, {key: 'slack-test:send'});

Expand All @@ -58,6 +62,59 @@ export default Controller.extend({
updateURL(value) {
this.set('model.url', value);
this.get('model.errors').clear();
},

triggerDirtyState() {
let slack = this.get('model');
let slackArray = this.get('slackArray');
let settings = this.get('settings');

// Hack to trigger the `isDirty` state on the settings model by setting a new Array
// for slack rather that replacing the existing one which would still point to the
// same reference and therfore not setting the model into a dirty state
slackArray.clear().pushObject(slack);
settings.set('slack', slackArray);
},

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 settings = this.get('settings');
let slackArray = this.get('slackArray');

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
settings.rollbackAttributes();
slackArray.clear();

return transition.retry();
}
}
});
50 changes: 50 additions & 0 deletions app/controllers/settings/apps/unsplash.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,19 @@ export default Controller.extend({
settings: service(),

model: alias('settings.unsplash'),
dirtyAttributes: null,
rollbackValue: null,

leaveSettingsTransition: null,

save: task(function* () {
let unsplash = this.get('model');
let settings = this.get('settings');

try {
settings.set('unsplash', unsplash);
this.set('dirtyAttributes', false);
this.set('rollbackValue', null);
return yield settings.save();
} catch (error) {
if (error) {
Expand All @@ -30,7 +36,51 @@ export default Controller.extend({
},

update(value) {
if (!this.get('dirtyAttributes')) {
this.set('rollbackValue', this.get('model.isActive'));
}
this.set('model.isActive', value);
this.set('dirtyAttributes', true);
},

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');

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
this.set('model.isActive', this.get('rollbackValue'));
this.set('dirtyAttributes', false);
this.set('rollbackValue', null);

return transition.retry();
}
}
});
40 changes: 40 additions & 0 deletions app/controllers/settings/code-injection.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,46 @@ export default Controller.extend({
actions: {
save() {
this.get('save').perform();
},

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 settings = 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
settings.rollbackAttributes();

return transition.retry();
}

}
});
55 changes: 55 additions & 0 deletions app/controllers/settings/design.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export default Controller.extend({

newNavItem: null,

dirtyAttributes: false,

themes: null,
themeToDelete: null,
showDeleteThemeModal: notEmpty('themeToDelete'),
Expand Down Expand Up @@ -48,6 +50,7 @@ export default Controller.extend({

try {
yield RSVP.all(validationPromises);
this.set('dirtyAttributes', false);
return yield this.get('model').save();
} catch (error) {
if (error) {
Expand All @@ -63,6 +66,7 @@ export default Controller.extend({

newNavItem.set('isNew', false);
navItems.pushObject(newNavItem);
this.set('dirtyAttributes', true);
this.set('newNavItem', NavigationItem.create({isNew: true}));
$('.gh-blognav-line:last input:first').focus();
},
Expand Down Expand Up @@ -110,6 +114,16 @@ export default Controller.extend({
let navItems = this.get('model.navigation');

navItems.removeObject(item);
this.set('dirtyAttributes', true);
},

updateLabel(label, navItem) {
if (!navItem) {
return;
}

navItem.set('label', label);
this.set('dirtyAttributes', true);
},

updateUrl(url, navItem) {
Expand All @@ -118,6 +132,47 @@ export default Controller.extend({
}

navItem.set('url', url);
this.set('dirtyAttributes', true);
},

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();
this.set('dirtyAttributes', false);

return transition.retry();
},

activateTheme(theme) {
Expand Down
Loading

0 comments on commit 6ef4c62

Please sign in to comment.