feat: 🎸 apply confirmation modal to deletes

This commit is contained in:
Randall Morey committed 2020-10-14 09:33:44 -04:00
1 parent 8d668dddfa
commit 896b09bfff
9 files changed
+314 -30

No files matched your search

@@ -0,0 +1,6 @@
{{#each this.confirm.pending as |confirmation|}}
{{yield
confirmation.text
(fn this.accept confirmation)
(fn this.deny confirmation)}}
{{/each}}
@@ -0,0 +1,40 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
/**
* A component that yields for each pending confirmation in the confirmations
* service, passing the confirmation instance, a confirm action, and a dismiss
* action. Useful for rendering confirm modals and awaiting user action.
*/
export default class PendingConfirmationsComponent extends Component {
// =services
@service confirm;
// =actions
/**
* Accepts the passed confirmation.
* @param {Confirmation} confirmation
* @return {Confirmation}
*/
@action
accept(confirmation) {
confirmation.confirm();
return confirmation;
}
/**
* Denies the passed confirmation.
* @param {Confirmation} confirmation
* @return {Confirmation}
*/
@action
deny(confirmation) {
confirmation.dismiss();
return confirmation;
}
}
+117 -22
View File
@@ -1,10 +1,20 @@
import Service from '@ember/service';
import { getOwner } from '@ember/application';
import { bind } from '@ember/runloop';
import { Promise, resolve } from 'rsvp';
import { A } from '@ember/array';
import { tracked } from '@glimmer/tracking';
import { defer, resolve } from 'rsvp';
import { filterBy } from '@ember/object/computed';
import config from '../config/environment';
export default class ConfirmService extends Service {
/**
* A simple service that emits Confirmation instances and
* exposes a `pending` property to discover pending
* confirmations.
* @augments Service
*
* @example
* const confirmation = confirmationService.getConfirmation();
*/
export default class ConfirmationsService extends Service {
// =attributes
@@ -15,18 +25,15 @@ export default class ConfirmService extends Service {
enabled = config.enableConfirmService;
/**
* The window's confirm function, bound to the window scope.
* @type {Function}
* @type {Array}
*/
get confirmFn() {
// The Ember way of accessing globals...
const document =
getOwner(this).lookup('service:-document').documentElement;
// defaultView === window, but without using globals directly
const windowRef = document.parentNode.defaultView;
const { confirm } = windowRef;
return bind(windowRef, confirm);
}
confirmations = A();
/**
* An array of pending confirmations (those where `done === false`).
* @type {Array}
*/
@filterBy('confirmations', 'done', false) pending;
// =methods
@@ -39,15 +46,103 @@ export default class ConfirmService extends Service {
*/
confirm(text) {
if (this.enabled) {
return new Promise((resolve, reject) => {
if (this.confirmFn(text)) {
return resolve();
} else {
return reject();
}
});
const confirmation = new Confirmation(text);
this.confirmations.addObject(confirmation);
return confirmation;
} else {
return resolve();
}
}
}
/**
* A promise-like object that is either "confirmed" or "dismissed", represented
* internally by a promise that resolves or rejects. Confirmations may be used
* as promises via `then`, `catch`, and `finally`.
*
* Not intended for instantiation directly. Use the confirmation service
* to create confirmation instances.
*
* @example
* const confirmation = new Confirmation();
* confirmation.confirm();
* // confirmation.done now equals true
* confirmation
* .then(() => { }) // do something
* .catch(() => { }) // not called, since the confirmation is confirmed
* .finally(() => { }); // do something
*
* @example
* const confirmation = new Confirmation();
* confirmation.dismiss();
* // confirmation.done now equals true
* confirmation
* .then(() => { }) // not called, since the confirmation is dismissed
* .catch(() => { }) // do something
* .finally(() => { }); // do something
*/
class Confirmation {
// =properties
/**
* Indicates whether the confirmation has been confirmed or dismissed.
* Automatically set to `true` after `confirm` or `dismiss` are called.
* @type {Boolean}
*/
@tracked done = false;
#deferred = null;
/**
* Sets the type field to the specified type and sets up a defer.
* @param {String} type An optional type annotation for convenience.
* Has no effect on confirmation.
*/
constructor(text) {
this.text = text;
this.#deferred = defer();
}
/**
* Sets the confirmation to done and resolves the defer.
*/
confirm() {
this.done = true;
this.#deferred.resolve();
}
/**
* Sets the confirmation to done and rejects the defer.
*/
dismiss() {
this.done = true;
this.#deferred.reject();
}
/**
* Delegates to the internal defer promise, allowing the
* confirmation to be treated like a promise.
*/
then() {
return this.#deferred.promise.then(...arguments);
}
/**
* Delegates to the internal defer promise, allowing the
* confirmation to be treated like a promise.
*/
catch() {
return this.#deferred.promise.catch(...arguments);
}
/**
* Delegates to the internal defer promise, allowing the
* confirmation to be treated like a promise.
*/
finally() {
return this.#deferred.promise.finally(...arguments);
}
}
+20
View File
@@ -83,3 +83,23 @@
{{message.text}}
</Rose::Notification>
</EmberNotify>
<PendingConfirmations as |text accept deny|>
<Rose::Dialog
@heading={{t "actions.confirm"}}
@modal={{true}}
@dismiss={{deny}}
as |dialog|>
<dialog.body>
<p>{{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>
</PendingConfirmations>
+1
View File
@@ -62,6 +62,7 @@
"ember-exam": "^5.0.1",
"ember-export-application-global": "^2.0.1",
"ember-fetch": "^8.0.1",
"ember-focus-trap": "^0.4.0",
"ember-inline-svg": "^1.0.0",
"ember-intl": "^5.1.1",
"ember-load-initializers": "^2.1.1",
+5 -4
View File
@@ -4,6 +4,7 @@ import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { resolve, reject } from 'rsvp';
import {
authenticateSession,
// These are left here intentionally for future reference.
@@ -146,22 +147,22 @@ module('Acceptance | projects', function (hooks) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
window.confirm = sinon.fake.returns(true);
confirmService.confirm = sinon.fake.returns(resolve());
await visit(existingProjectURL);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1);
assert.ok(window.confirm.calledOnce);
assert.ok(confirmService.confirm.calledOnce);
});
test('cannot delete project with confirmation denial', async function (assert) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
window.confirm = sinon.fake.returns(false);
confirmService.confirm = sinon.fake.returns(reject());
await visit(existingProjectURL);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getProjectScopesCount(), initialProjectScopesCount);
assert.ok(window.confirm.calledOnce);
assert.ok(confirmService.confirm.calledOnce);
});
test('saving an existing project with invalid fields displays error messages', async function (assert) {
@@ -0,0 +1,59 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, findAll, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | pending-confirmations', function(hooks) {
setupRenderingTest(hooks);
test('it renders pending confirmations', async function(assert) {
assert.expect(2);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
await render(hbs`
<PendingConfirmations as |confirmations|>
<div class="pending">Pending</div>
</PendingConfirmations>
`);
assert.equal(findAll('.pending').length, 0, 'No pending confirmations');
service.confirm();
await render(hbs`
<PendingConfirmations as |confirmations|>
<div class="pending">Pending</div>
</PendingConfirmations>
`);
assert.equal(findAll('.pending').length, 1, 'One pending confirmations');
});
test('it exposes a confirm action that confirms the confirmation', async function(assert) {
assert.expect(3);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
const confirmation = service.confirm();
confirmation.then(() => assert.ok(true, 'confirmation.then was called'));
await render(hbs`
<PendingConfirmations as |confirmations confirm|>
<button {{on "click" confirm}}>Confirm</button>
</PendingConfirmations>
`);
assert.notOk(confirmation.done, 'Confirmation is not done');
await click('button');
assert.ok(confirmation.done, 'Confirmation is done');
});
test('it exposes a dismiss action that dismisses the confirmation', async function(assert) {
assert.expect(3);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
const confirmation = service.confirm();
confirmation.catch(() => assert.ok(true, 'confirmation.catch was called'));
await render(hbs`
<PendingConfirmations as |confirmations confirm dismiss|>
<button {{on "click" dismiss}}>Dismiss</button>
</PendingConfirmations>
`);
assert.notOk(confirmation.done, 'Confirmation is not done');
await click('button');
assert.ok(confirmation.done, 'Confirmation is done');
});
});
+63 -4
View File
@@ -4,9 +4,68 @@ import { setupTest } from 'ember-qunit';
module('Unit | Service | confirm', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:confirm');
assert.ok(service);
test('`confirm()` emits promise-like instances', function(assert) {
assert.expect(3);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
const confirmation = service.confirm();
assert.ok(confirmation.then, 'confirmation has a then method');
assert.ok(confirmation.catch, 'confirmation has a catch method');
assert.ok(confirmation.finally, 'confirmation has a finally method');
});
test('`pending` is an array of pending confirmations', function(assert) {
assert.expect(3);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
assert.equal(service.pending.length, 0, 'No pending confirmations yet');
const confirmation = service.confirm();
assert.equal(service.pending.length, 1, 'One pending confirmation created');
confirmation.confirm();
assert.equal(service.pending.length, 0, 'Confirmation was confirmed');
});
test('confirmations may be confirmed or dismissed', function(assert) {
assert.expect(4);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
let confirmation = service.confirm();
assert.notOk(confirmation.done, 'Confirmation is pending');
confirmation.confirm();
assert.ok(confirmation.done, 'Confirmation is done via confirm');
confirmation = service.confirm();
assert.notOk(confirmation.done, 'Confirmation is pending');
confirmation.dismiss();
assert.ok(confirmation.done, 'Confirmation is done via dismiss');
});
test('confirmations resolve on confirm', function(assert) {
assert.expect(1);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
const confirmation = service.confirm();
confirmation.then(() => assert.ok(true, 'Confirmation confirmed'));
confirmation.confirm();
});
test('confirmations reject on dismiss', function(assert) {
assert.expect(1);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
const confirmation = service.confirm();
confirmation.catch(() => assert.ok(true, 'Confirmation dismissed'));
confirmation.dismiss();
});
test('confirmations have a finally method', function(assert) {
assert.expect(2);
const service = this.owner.lookup('service:confirm');
service.enabled = true;
let confirmation = service.confirm();
confirmation.finally(() => assert.ok(true, 'Confirmation finally called'));
confirmation.confirm();
confirmation = service.confirm();
confirmation.finally(() => assert.ok(true, 'Confirmation finally called'));
confirmation.dismiss();
});
});
+3
View File
@@ -5,6 +5,9 @@ remove: Remove
dismiss: Dismiss
save: Save
manage: Manage
confirm: Confirm
ok: OK
deny: Deny
help: Need help?
back: Back
get-topic-help: Get help for this topic