feat: 🎸 add abandon confirm dialog

This commit is contained in:
Randall Morey committed 2020-10-15 16:45:38 -04:00
1 parent fbced5fa2a
commit bcedd8d16d
6 files changed
+117 -26

No files matched your search

+25
View File
@@ -18,6 +18,7 @@ export default class ApplicationRoute extends Route.extend(
// =services
@service session;
@service confirm;
// =attributes
@@ -90,4 +91,28 @@ export default class ApplicationRoute extends Route.extend(
}
return true;
}
/**
* If user attempts to navigate away from unsaved changes, the user is
* asked to confirm that they would like to discard the changes. If the
* user chooses discard, changes on the model are rolled back and the
* transition is retried. If the user cancels discard, the transition is
* aborted.
* @param {Transition} transition
*/
@action
async willTransition(transition) {
const maybeModel = transition?.from?.attributes;
if (maybeModel?.hasDirtyAttributes) {
transition.abort();
try {
await this.confirm.confirm('abandon', { isAbandonConfirm: true });
maybeModel?.rollbackAttributes();
transition.retry();
} catch (e) {
// if user denies, do nothing
}
}
}
}
+8 -6
View File
@@ -12,7 +12,7 @@ import config from '../config/environment';
* @augments Service
*
* @example
* const confirmation = confirmationService.getConfirmation();
* const confirmation = confirmationService.confirm('Text', { type: 'discard' });
*/
export default class ConfirmationsService extends Service {
@@ -42,11 +42,12 @@ export default class ConfirmationsService extends Service {
* accepts the confirmation, otherwise rejects. If the service is disabled,
* this method always returns a resolving promise.
* @param {string} text
* @param {object} options
* @return {Promise}
*/
confirm(text) {
confirm(text, options) {
if (this.enabled) {
const confirmation = new Confirmation(text);
const confirmation = new Confirmation(text, options);
this.confirmations.addObject(confirmation);
return confirmation;
} else {
@@ -65,7 +66,7 @@ export default class ConfirmationsService extends Service {
* to create confirmation instances.
*
* @example
* const confirmation = new Confirmation();
* const confirmation = new Confirmation('Text', { type: 'discard' });
* confirmation.confirm();
* // confirmation.done now equals true
* confirmation
@@ -74,7 +75,7 @@ export default class ConfirmationsService extends Service {
* .finally(() => { }); // do something
*
* @example
* const confirmation = new Confirmation();
* const confirmation = new Confirmation('Text', { type: 'discard' });
* confirmation.dismiss();
* // confirmation.done now equals true
* confirmation
@@ -100,8 +101,9 @@ class Confirmation {
* @param {String} type An optional type annotation for convenience.
* Has no effect on confirmation.
*/
constructor(text) {
constructor(text, options={}) {
this.text = text;
this.options = options;
this.#deferred = defer();
}
+43 -20
View File
@@ -85,24 +85,47 @@
</EmberNotify>
<PendingConfirmations as |confirmation accept deny|>
<Rose::Dialog
@heading={{t "actions.confirm"}}
@dismissButtonText={{t "actions.cancel"}}
@icon="alert-triangle"
@style="warning"
@modal={{true}}
@dismiss={{deny}}
as |dialog|>
<dialog.body>
<p>{{confirmation.text}}</p>
</dialog.body>
<dialog.footer>
<Rose::Button @style="primary" {{on "click" accept}}>
{{t "actions.ok"}}
</Rose::Button>
<Rose::Button @style="secondary" {{on "click" deny}}>
{{t "actions.cancel"}}
</Rose::Button>
</dialog.footer>
</Rose::Dialog>
{{#if confirmation.options.isAbandonConfirm}}
<Rose::Dialog
@heading={{t "titles.abandon-confirm"}}
@dismissButtonText={{t "actions.cancel"}}
@icon="alert-triangle"
@style="warning"
@modal={{true}}
@dismiss={{deny}}
as |dialog|>
<dialog.body>
<p>{{t "questions.abandon-confirm"}}</p>
</dialog.body>
<dialog.footer>
<Rose::Button @style="primary" {{on "click" accept}}>
{{t "actions.discard"}}
</Rose::Button>
<Rose::Button @style="secondary" {{on "click" deny}}>
{{t "actions.cancel"}}
</Rose::Button>
</dialog.footer>
</Rose::Dialog>
{{else}}
<Rose::Dialog
@heading={{t "actions.confirm"}}
@dismissButtonText={{t "actions.cancel"}}
@icon="alert-triangle"
@style="warning"
@modal={{true}}
@dismiss={{deny}}
as |dialog|>
<dialog.body>
<p>{{confirmation.text}}</p>
</dialog.body>
<dialog.footer>
<Rose::Button @style="primary" {{on "click" accept}}>
{{t "actions.ok"}}
</Rose::Button>
<Rose::Button @style="secondary" {{on "click" deny}}>
{{t "actions.cancel"}}
</Rose::Button>
</dialog.footer>
</Rose::Dialog>
{{/if}}
</PendingConfirmations>
+38
View File
@@ -136,6 +136,44 @@ module('Acceptance | projects', function (hooks) {
assert.equal(find('[name="name"]').value, existingProject.name);
});
test('can discard unsaved changes via dialog', async function (assert) {
assert.expect(5);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
assert.notEqual(existingProject.name, 'random string');
await visit(existingProjectURL);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
assert.equal(currentURL(), existingProjectURL);
try {
await visit(projectsURL);
} catch (e) {
assert.ok(find('.rose-dialog'));
await click('.rose-dialog-footer button:first-child');
assert.equal(currentURL(), projectsURL);
assert.notEqual(getFirstProjectScope().name, 'random string');
}
});
test('can cancel discard unsaved changes via dialog', async function (assert) {
assert.expect(5);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
assert.notEqual(existingProject.name, 'random string');
await visit(existingProjectURL);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
assert.equal(currentURL(), existingProjectURL);
try {
await visit(projectsURL);
} catch (e) {
assert.ok(find('.rose-dialog'));
await click('.rose-dialog-footer button:last-child');
assert.equal(currentURL(), existingProjectURL);
assert.notEqual(getFirstProjectScope().name, 'random string');
}
});
test('can delete project', async function (assert) {
assert.expect(1);
await visit(existingProjectURL);
+1
View File
@@ -2,6 +2,7 @@ add: Add
cancel: Cancel
delete: Delete
remove: Remove
discard: Discard
dismiss: Dismiss
save: Save
manage: Manage
+2
View File
@@ -17,8 +17,10 @@ titles:
global: Global
actions: Actions
documentation: Documentation
abandon-confirm: Discard unsaved changes?
descriptions:
empty-set: There are no items to display yet. You may be able to add items or try back later.
questions:
delete-confirm: Are you sure you want to delete this resource?
remove-confirm: Are you sure you want to remove this association?
abandon-confirm: Are you sure you want to navigate away? Your unsaved changes will be lost.