feat: 🎸 add abandon confirm dialog
This commit is contained in:
6 files changed
+117
-26
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -2,6 +2,7 @@ add: Add
|
||||
cancel: Cancel
|
||||
delete: Delete
|
||||
remove: Remove
|
||||
discard: Discard
|
||||
dismiss: Dismiss
|
||||
save: Save
|
||||
manage: Manage
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user