refactor: 💡 port confirmation service and components into core

This commit is contained in:
Randall Morey committed 2020-11-18 13:19:46 -05:00
1 parent 23e83143a0
commit a19bdaa245
26 files changed
+59 -17

No files matched your search

@@ -1,6 +0,0 @@
{{#each this.confirm.pending as |confirmation|}}
{{yield
confirmation
(fn this.accept confirmation)
(fn this.deny confirmation)}}
{{/each}}
@@ -1,38 +0,0 @@
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;
}
}
-26
View File
@@ -1,26 +0,0 @@
import { getOwner } from '@ember/application';
/**
* Decorates a method whereby calls to the method will first request
* confirmation via the confirmation service. If the user accepts, the
* original method is executed. If the user denies, it is not.
*/
export function confirm(confirmationTextKey) {
return function (_target, _propertyKey, desc) {
const method = desc.value;
desc.value = function () {
const owner = getOwner(this);
const confirmService = owner.lookup('service:confirm');
const intl = owner.lookup('service:intl');
const text = intl.t(confirmationTextKey);
return (
confirmService
.confirm(text)
// If user confirmed, execute the decorated method
.then(() => method.apply(this, arguments))
// If the user denied, gracefully handle promise rejection with no-op
.catch((e) => e)
);
};
};
}
+1 -1
View File
@@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
import { A } from '@ember/array';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../decorators/notify';
export default class ScopesScopeRoute extends Route {
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeAuthMethodsRoute extends Route {
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeAuthMethodsAuthMethodAccountsRoute extends Route {
+1 -1
View File
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeGroupsRoute extends Route {
@@ -3,7 +3,7 @@ import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { hash, all } from 'rsvp';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeGroupsGroupMembersRoute extends Route {
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeHostCatalogsRoute extends Route {
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeHostCatalogsHostCatalogHostSetsRoute extends Route {
@@ -3,7 +3,7 @@ import { hash, all } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import {
notifySuccess,
notifyError,
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeHostCatalogsHostCatalogHostsRoute extends Route {
+1 -1
View File
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeRolesRoute extends Route {
@@ -3,7 +3,7 @@ import { all, hash } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeRolesRolePrincipalsRoute extends Route {
+1 -1
View File
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeTargetsRoute extends Route {
@@ -3,7 +3,7 @@ import { all, hash } from 'rsvp';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../../../decorators/notify';
export default class ScopesScopeTargetsTargetHostSetsRoute extends Route {
+1 -1
View File
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from '../../../decorators/confirm';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from '../../../decorators/notify';
export default class ScopesScopeUsersRoute extends Route {
-146
View File
@@ -1,146 +0,0 @@
import Service from '@ember/service';
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';
/**
* A simple service that emits Confirmation instances and
* exposes a `pending` property to discover pending
* confirmations.
* @augments Service
*
* @example
* const confirmation = confirmationService.confirm('Text', { type: 'discard' });
*/
export default class ConfirmationsService extends Service {
// =attributes
/**
* When the confirm service is disabled, it always returns resolving promises.
* @type {boolean}
*/
enabled = config.enableConfirmService;
/**
* @type {Array}
*/
confirmations = A();
/**
* An array of pending confirmations (those where `done === false`).
* @type {Array}
*/
@filterBy('confirmations', 'done', false) pending;
// =methods
/**
* When the service is enabled, returns a promise that resolves if the user
* 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, options) {
if (this.enabled) {
const confirmation = new Confirmation(text, options);
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('Text', { type: 'discard' });
* 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('Text', { type: 'discard' });
* 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, options = {}) {
this.text = text;
this.options = options;
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);
}
}
@@ -1,59 +0,0 @@
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');
});
});
@@ -1,71 +0,0 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | confirm', function (hooks) {
setupTest(hooks);
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();
});
});